I am using scheme in my app to pass data from a second app to my app.
I am already able to open my app using it's scheme from second app.
But now I will like to parse the URL data that was sent from the second app. I found a tutorial online about implementing a method as seen below in my viewController class but this method is never triggered. Do i need to place it somewhere specific ? right now it's just in the viewController class.
I just started coding in Swift this week to create a POC, I have more of an Android background.
func application(app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:] ) -> Bool {
// Determine who sent the URL.
let sendingAppID = options[.sourceApplication]
print("source application = \(sendingAppID ?? "Unknown")")
// Process the URL.
guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
// let ro_response = components.path,
let params = components.queryItems else {
print("Invalid")
return false
}
if let serial = params.first(where: { $0.name == "serial" })?.value {
self.SERIAL = serial as String
} else {
return false;
}
if let otp = params.first(where: { $0.name == "otp" })?.value {
self.OTP = otp as String
} else {
return false;
}
return true
}
This is a method from UIApplicationDelegate so you have to implement it in your AppDelegate.
Related
Here is the code I currently have however, it does not seem to be working. This example says I want to open the calc app. My goal is to open an app once a widget is clicked.
#main App Code:
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
print("Received deep link: \(url)")
}
}
}
Widget Code:
Gauge(value: 50), in: 0.0...100.0) {
} currentValueLabel: {
Text(Open App)
}
.gaugeStyle(.accessoryCircularCapacity)
.widgetURL(URL(string: "calc://")!)
Then you need to do this in a 2 step process. First, you need to set up your app to receive custom URLs from your widget. This is shockingly well explained by Apple here. Once you have your app's custom url scheme set up, it is time to set up your widget. Essentially what you are going to do is send a URL with a query that is the URL you want to open. Back in your app, you receive that URL, parse it out, and then call openUrl() with the URL you want to open, and that app will open.
Your code above is close. Following Apple's example above, try this:
In your widget create a deep link URL:
func createDeeplinkForCalc() -> URL {
var components = URLComponents()
components.scheme = "myphotoapp"
components.host = "com.example.myphotoapp"
components.path = "/calc"
components.queryItems = [
URLQueryItem(name: "open", value: "calc://")
]
return components.url!
}
Then, in .widgetURL, pass this:
.widgetURL(createDeeplinkForCalc())
In your main app:
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
handleURL(url: URL)
}
}
}
func handleURL(_ url:URL) {
// This makes sure you got the correct URL
guard url.scheme == "myphotoapp",
url.host == "com.example.myphotoapp"
else { return }
let query = parseQuery(url: url)
guard let urlString = query["open"],
!urlString.isEmpty else { return } // make sure something exists in the value
if let calcURL = URL(string: urlString) {
openURL(calcURL) // this calls "calc://" and opens the calculator
}
private func parseQuery(url: URL) -> Query {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let queryItems = components.queryItems
else { return ["":""] }
return queryItems.reduce(into: Query()) { (result, item) in
result[item.name] = item.value
}
}
}
The above has not been tested, but should work.
I've been trying to figure how how to detect the url I retrieve from Firebase Storage is a Photo or a Video,
I followed this question but it doesn't seem to work with Firebase Storage URLs.
The solution implemented in that question work only when the url has the extension at the end.
This is the code I implemented:
extension String {
public func isImageType() -> Bool {
// image formats which you want to check
let imageFormats = ["jpg", "png", "gif"]
if URL(string: self) != nil {
let extensi = (self as NSString).pathExtension
return imageFormats.contains(extensi)
}
return false
}
}
To be clear, URLs retrieved from Firebase have this form.
Photo url example:
https://firebasestorage.googleapis.com/v0/b/myApp-1e48d.appspot.com/o/Images%2FD118DA58-C128-4E5E-BF24-AA820BEE5590.jpg?alt=media&token=49eie236-f9d1-45f8-bd0b-887382c61ccd
Video url example:
https://firebasestorage.googleapis.com/v0/b/myApp-1e48d.appspot.com/o/Videos%2F1E4B7CA0-4D0E-4AC5-9856-2F59D0811C47.mp4?alt=media&token=615teacf-0d20-48aa-bb8f-cew84a14d76d
As you can see the extension is not at the end (because there is the need of the token), hence the extension methods linked before doesn't work.
How can I still understand if the url retrieved from Firebase is of a Photo or a Video?
You can simply initialize an url with your link and get the path extension:
let link1 = "https://firebasestorage.googleapis.com/v0/b/myApp-1e48d.appspot.com/o/Images%2FD118DA58-C128-4E5E-BF24-AA820BEE5590.jpg?alt=media&token=49eie236-f9d1-45f8-bd0b-887382c61ccd"
if let url = URL(string: link1) {
let fileType = url.pathExtension // "jpg"
}
let link2 = "https://firebasestorage.googleapis.com/v0/b/myApp-1e48d.appspot.com/o/Videos%2F1E4B7CA0-4D0E-4AC5-9856-2F59D0811C47.mp4?alt=media&token=615teacf-0d20-48aa-bb8f-cew84a14d76d"
if let url = URL(string: link2) {
let fileType = url.pathExtension // "mp4"
}
I implemented a code that works:
First Remove all after "?":
extension String {
public func removeAfterExtension() -> String?{
if let index = (self.range(of: "?")?.lowerBound){
let beforeEqualsTo = String(self.prefix(upTo: index))
return beforeEqualsTo
}else{
return self
}
}
Then check if it is image Type:
public func isImageType() -> Bool {
// image formats which you want to check
let imageFormats = ["jpg", "png", "gif"]
if URL(string: self) != nil {
let extensi = (self as NSString).pathExtension
return imageFormats.contains(extensi)
}
return false
}
So I have a dynamic link that is working in that it opens up the app when I click on it but the handling of the dynamic link doesn't happen. This is because application function seen below is never entered and I'm not sure why...
func handleIncomingDynamicLink(_ dynamicLink: DynamicLink){
guard let url = dynamicLink.url else {
print("That's weird. My dynamic link object has no url")
return
}
print("Your incoming link parameter is \(url.absoluteString)")
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if let incomingURL = userActivity.webpageURL {
print("Incoming URL is \(incomingURL)")
let linkHandled = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
guard error == nil else{
print("Found an error! \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
self.handleIncomingDynamicLink(dynamicLink)
}
}
if linkHandled {
return true
} else {
return false
}
}
return false
}
Any help would be greatly appreciated--is it possible this is a problem with Firebase itself or no?
SwiftUI 2.0 / #main App
For the new SwiftUI 2.0 you can make an app only using SwiftUI. This means you don't use AppDelegate or SceneDelegate. You then have to use .onOpenURL to get any action called when you click on your link. I am usling this for Firebase Dynamic Links.
WindowGroup {
ContentView()
.onOpenURL { url in
// handle the URL that must be opened
let incomingURL = url
print("Incoming URL is: \(incomingURL)")
let linkHandled = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
guard error == nil else {
print("Error found!: \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
self.handleDynamicLink(dynamicLink)
}
}
if linkHandled{
print("Link Handled")
return
}else{
print("NO linkHandled")
return
}
}
}
Configuring Firebase Dynamic Links on iOS has several steps that must be followed. So, I recommend referring to the official documentation here. Additionally, there is a Firecasts series that walks through all of the steps here.
I followed the Firecasts series, but my setup is slightly different; so, I ran into a couple of issues. First, I am using a custom domain. Second, I'm using SwiftUI. I'll just address the issues that I had, so please refer to the full Firecasts series for all of the gruesome details.
SwiftUI 2.0
There are two different types of links that your app needs to handle. First, your app needs to handle links when your app is already installed (i.e. https://app.yourcustomdomain.com/...). Second, your app needs to handle links copied into the clipboard before your app was installed (i.e. com.your.bundle.id://google/link/...).
Also, you probably want to handle links originating from a web browser (Safari). In my case, I process a link from a QR code scan. Apparently NFC tag scan links also originate from Safari, but I can't confirm this.
After some trial and error, I found a way to handle all of these concerns with SwiftUI; it's unclear if this is the officially recommended approach, so proceed with caution.
First, use onOpenURL(perform:) and .onContinueUserActivity(_,perform:):
var body: some Scene {
WindowGroup {
ContentView()
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb, perform: onWebBrowserActivity)
.onOpenURL(perform: onOpenURL)
}
}
Second, define the onContinueUserActivity handler:
// Handles links originating from the web browser
func onWebBrowserActivity(_ userActivity: NSUserActivity) {
guard let url = userActivity.webpageURL else {
return
}
onOpenURL(url)
}
Third, define the onOpenURL handler:
func onOpenURL(_ url: URL) {
let dynamicLinks = DynamicLinks.dynamicLinks()
if dynamicLinks.shouldHandleDynamicLink(fromCustomSchemeURL: url),
let dynamicLink = dynamicLinks.dynamicLink(fromCustomSchemeURL: url) {
handle(dynamicLink: dynamicLink)
} else {
handleUniversalLink(url: url)
}
}
Both types of links mentioned above are passed into the onOpenURL handler, so this if statement handles the two cases.
Finally, (as others have mentioned) handle universal links:
func handleUniversalLink(url: URL) {
let linkHandled = DynamicLinks.dynamicLinks().handleUniversalLink(url) { dynamicLink, error in
if let error = error {
print("Could not follow the dynamic link \(error.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
self.handle(dynamicLink: dynamicLink)
}
}
if linkHandled {
print("Link handled")
} else {
print("Link not handled")
}
}
Custom Domain
I had to follow the custom domain setup instructions here. Specifically, I had to set up Firebase Hosting, which requires adding an A DNS record (In addition to setting up a TXT DNS record for the Dynamic Links URL Prefix setup). Also, I had to specify the custom domain in my Info.plist
<key>FirebaseDynamicLinksCustomDomains</key>
<array>
<string>https://app.yourcustomdomain.com</string>
</array>
SwiftUI 1.0 / SceneDelegate
You do the same but in your SceneDelegate, following callback:
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if let incomingURL = userActivity.webpageURL {
print("Incoming URL is \(incomingURL)")
let linkHandled = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
guard error == nil else{
print("Found an error! \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
self.handleIncomingDynamicLink(dynamicLink)
}
}
}
SwiftUI 2.0 / WindowGroup
With SwiftUI 2.0 life-cycle you need to use instead .onContinueUserActivity, like
WindowGroup {
ContentView()
.onContinueUserActivity(<your_activity_type_here>) { userActivity in
if let incomingURL = userActivity.webpageURL {
print("Incoming URL is \(incomingURL)")
let linkHandled = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
guard error == nil else{
print("Found an error! \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
self.handleIncomingDynamicLink(dynamicLink)
}
}
}
}
}
I am building a test app using MailCore2 and GTMAppAuth, and I bumped into this error:
A stable connection to the server could not be established.
I followed various SO posts, like this, this and this, and so I think my code should be correct. My implementation are as follows:
//At AppDelegate
var currentAuthorizationFlow: OIDAuthorizationFlowSession?
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool
{
if currentAuthorizationFlow!.resumeAuthorizationFlow(with: url) {
self.currentAuthorizationFlow = nil
return true
}
return false
}
class ViewController: UIViewController {
let kIssuer = "https://accounts.google.com"
let kClientID = "\(MY_CLIENTID).apps.googleusercontent.com"
let kRedirectURI = "com.googleusercontent.apps\(MY_CLIENTID):/oauthredirect"
let kExampleAuthorizerKey = "googleOAuthCodingKey"
override func viewDidLoad() {
super.viewDidLoad()
authenticateGmail()
}
func authenticateGmail() {
let issuer = URL(string: kIssuer)!
let redirectURI = URL(string: kRedirectURI)!
let appDelegate = UIApplication.shared.delegate as! AppDelegate
OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { (configuration, error) in
//handleError
if let configuration = configuration {
let scopes = [OIDScopeOpenID, OIDScopeProfile, "https://mail.google.com/"]
let request = OIDAuthorizationRequest(configuration: configuration, clientId: self.kClientID, scopes: scopes, redirectURL: redirectURI, responseType: OIDResponseTypeCode, additionalParameters: nil)
appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: self, callback: { (authState, error) in
//handleError
if let authState = authState {
if let accessToken = authState.lastTokenResponse?.accessToken {
NSLog("Successfully authenticated: %#", accessToken)
self.fetchEmailsFromGmail(accessToken: accessToken)
}
}
})
}
}
}
func fetchEmailsFromGmail(accessToken: String) {
let session = MCOIMAPSession()
session.hostname = "imap.gmail.com"
session.port = 993
session.username = "XXX#\(COMPANY_DOMAIN)"
session.authType = .xoAuth2
session.connectionType = .TLS
session.oAuth2Token = accessToken
let fetchFolderOperation = session.fetchAllFoldersOperation()
fetchFolderOperation?.start({ (error, folders) in
//handleError
if let folders = folders, !folders.isEmpty {
print(folders)
}
})
}
My implementation allows me to authenticate successfully, ie there is an accessToken printed. But when it attempts to fetch folders, it throws:
A stable connection to the server could not be established.
The thing is, it is possible to solve this by doing both the following:
allowing less secure app access via the Googles Accounts page
follow this solution here
However, these methods are not really safe methods in my opinion. I do not recall needing to do any of these when I connect up my email to other email clients, ie Outlook etc, so I do not view the above as real solutions.
Could anyone advice? Is there something wrong with my code, or I really have to resort to doing both steps above?
I am trying to capture the image that the webview is displaying to the user, so I can some color analysis of the web page. When I try to get the image from it's parent, I am basically getting a white box, even though the page has rendered:
func makeImageSnapshot()-> (NSImage)
{
let imgSize = self.view.bounds.size
let bir = self.viewbitmapImageRepForCachingDisplayInRect(self.webView!.view.bounds)
bir.size = imgSize
self.webView.cacheDisplayInRect(self.view.bounds, toBitmapImageRep:bir)
let image = NSImage(size:imgSize)
image.addRepresentation(bir)
self.image = image
return image
}
func saveSnapshot()
{
let imgRep = self.image!.representations[0]
let data = imgRep.representationUsingType(NSBitmapImageFileType.NSPNGFileType, properties: nil)
data.writeToFile("/tmp/file.png", atomically: false)
}
It looks to me like I can't get access to the properties of the actual view (in this case the bounds) inside of the webView. When I try to access it, the compiler barfs:
/Users/josh/Canary/MacOsCanary/canary/canary/Modules/Overview/Overview.swift:55:37: '(NSView!, stringForToolTip: NSToolTipTag, point: NSPoint, userData: UnsafePointer<()>) -> String!' does not have a member named 'bounds'
My guess is that this is happening due to the extensions approach used by OS X and iOS. Any ideas, or should I just go back to using the legacy WebView?
I realise the question was for Mac OS X, but I found this page whilst searching for an iOS solution. My answer below doesn't work on Mac OS X as the drawViewHierarchyInRect() API call is currently iOS only, but I put it here for reference for other iOS searchers.
This Stackoverflow answer solved it for me on iOS 8 with a WKWebView. That answer's sample code is in Objective-C but the Swift equivalent to go in a UIView sub-class or extension would be along the lines of the code below. The code ignores the return value of drawViewHierarchyInRect(), but you may want to pay attention to it.
func imageSnapshot() -> UIImage
{
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0);
self.drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true);
let snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return snapshotImage;
}
Swift 3
extension WKWebView {
func screenshot() -> UIImage? {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0);
self.drawHierarchy(in: self.bounds, afterScreenUpdates: true);
let snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return snapshotImage;
}
}
Note: This solution only works on iOS.
Found myself in the same boat today but found a solution (by using private APIs).
If you're not targeting the App Store and generally are not afraid of using private APIs, here's a way to capture screenshots of WKWebView's on OS X:
https://github.com/lemonmojo/WKWebView-Screenshot
You will need to have access to a target writeable place - the snapshotURL ie.., such as the desktop, so we provide a handler for that:
func registerSnaphotsURL(_ sender: NSMenuItem, handler: #escaping (URL) -> Void) {
var targetURL : URL
// 1st around authenticate and cache sandbox data if needed
if isSandboxed, desktopData == nil {
targetURL =
UserSettings.SnapshotsURL.value.count == 0
? getDesktopDirectory()
: URL.init(fileURLWithPath: UserSettings.SnapshotsURL.value, isDirectory: true)
let openPanel = NSOpenPanel()
openPanel.message = "Authorize access to "
openPanel.prompt = "Authorize"
openPanel.canChooseFiles = false
openPanel.canChooseDirectories = true
openPanel.canCreateDirectories = true
openPanel.directoryURL = targetURL
openPanel.begin() { (result) -> Void in
if (result == .OK) {
targetURL = openPanel.url!
// Since we do not have data, clear any bookmark
if self.storeBookmark(url: targetURL, options: self.rwOptions) {
self.desktopData = self.bookmarks[targetURL]
UserSettings.SnapshotsURL.value = targetURL.absoluteString
if !self.saveBookmarks() {
print("Yoink, unable to save snapshot bookmark")
}
self.desktopData = self.bookmarks[targetURL]
handler(targetURL)
}
}
else
{
return
}
}
}
else
{
targetURL =
UserSettings.SnapshotsURL.value.count == 0
? getDesktopDirectory()
: URL.init(fileURLWithPath: UserSettings.SnapshotsURL.value, isDirectory: true)
handler(targetURL)
}
}
we wanted to allow single (view controller) and all current views (app delegate) so two actions in their respective files, both making use of the register handler.
App Delegate
#objc #IBAction func snapshotAllPress(_ sender: NSMenuItem) {
registerSnaphotsURL(sender) { (snapshotURL) in
// If we have a return object just call them, else notify all
if let wvc : WebViewController = sender.representedObject as? WebViewController {
sender.representedObject = snapshotURL
wvc.snapshot(sender)
}
else
{
sender.representedObject = snapshotURL
let notif = Notification(name: Notification.Name(rawValue: "SnapshotAll"), object: sender)
NotificationCenter.default.post(notif)
}
}
}
View Controller
func viewDidLoad() {
NotificationCenter.default.addObserver(
self,
selector: #selector(WebViewController.snapshotAll(_:)),
name: NSNotification.Name(rawValue: "SnapshotAll"),
object: nil)
}
#objc func snapshotAll(_ note: Notification) {
snapshot(note.object as! NSMenuItem)
}
view singleton action
#objc #IBAction func snapshotPress(_ sender: NSMenuItem) {
guard let url = webView.url, url != webView.homeURL else { return }
guard let snapshotURL = sender.representedObject as? URL else {
// Dispatch to app delegate to handle a singleton
sender.representedObject = self
appDelegate.snapshotAllPress(sender)
return
}
sender.representedObject = snapshotURL
snapshot(sender)
}
the webView interaction to capture an image
#objc func snapshot(_ sender: NSMenuItem) {
guard let url = webView.url, url != webView.homeURL else { return }
guard var snapshotURL = sender.representedObject as? URL else { return }
// URL has only destination, so add name and extension
let filename = String(format: "%# Shapshot at %#",
(url.lastPathComponent as NSString).deletingPathExtension,
String.prettyStamp())
snapshotURL.appendPathComponent(filename)
snapshotURL = snapshotURL.appendingPathExtension("png")
webView.takeSnapshot(with: nil) { image, error in
if let image = image {
self.webImageView.image = image
DispatchQueue.main.async {
self.processSnapshotImage(image, to: snapshotURL)
}
}
else
{
self.userAlertMessage("Failed taking snapshot", info: error?.localizedDescription)
self.webImageView.image = nil
}
}
}
and the capture to the targeted area
func processSnapshotImage(_ image: NSImage, to snapshotURL: URL) {
guard let tiffData = image.tiffRepresentation else { NSSound(named: "Sosumi")?.play(); return }
let bitmapImageRep = NSBitmapImageRep(data: tiffData)
do
{
try bitmapImageRep?.representation(using: .png, properties: [:])?.write(to: snapshotURL)
// https://developer.apple.com/library/archive/qa/qa1913/_index.html
if let asset = NSDataAsset(name:"Grab") {
do {
// Use NSDataAsset's data property to access the audio file stored in Sound.
let player = try AVAudioPlayer(data:asset.data, fileTypeHint:"caf")
// Play the above sound file.
player.play()
} catch {
print("no sound for you")
}
}
if snapshotURL.hideFileExtensionInPath(), let name = snapshotURL.lastPathComponent.removingPercentEncoding {
print("snapshot => \(name)")
}
} catch let error {
appDelegate.userAlertMessage("Snapshot failed", info: error.localizedDescription)
}
}