Loading a UIImage in WatchKit from a different Bundle - uiimage

My app is currently structured to use dynamic library to enable code reuse. I have images stored within my dynamic library so they can easily be shared between different app targets. This works fine in iOS because I can use the UIImage(named:, in:, compatibleWith:) initializer to load the image from my dynamic library. However, this initializer doesn't seem to be available on watchOS. Is there any other method of loading images from a dynamic library (with a different bundle) on watchOS. By the way, the image is stored within an Asset Catalog.

The workaround I discovered was to use the Bundle's resourceURL to get the root directory of the Bundle's resource folder and then loading the images manually from the filesystem using Data(contentsOf:) and UIImage(data:). This does not seem to work with Asset Catalogs though.

I'm sharing a piece of code that will load image from bundle and it is compatible with OSX, watchOS and iOS. if you only want the solution for watchOS only then take #elseif os(watchOS) part out
#if os(OSX)
import AppKit
public typealias Image = NSImage
#elseif os(watchOS)
import WatchKit
public typealias Image = UIImage
#else
import UIKit
public typealias Image = UIImage
#endif
public extension String
{
func image(in bundle: Bundle? = Bundle.main) -> Image?
{
#if os(OSX)
guard let img = bundle?.image(forResource: self) else {
return nil
}
#elseif os(watchOS)
guard let resource = bundle?.resourceURL, let img = try? Image(data: Data(contentsOf: resource)) else {
return nil
}
#else
guard let img = Image(named: self, in: bundle, compatibleWith: nil) else {
return nil
}
#endif
return img
}
}

Related

How to implement drag and drop to 3rd party apps in SwiftUI on macOS

I've tried to Google for the answer but all I find are how to accept drop from other apps and drag and drop in a single app, also mostly iOS focus and not many macOS related resources.
I writing a macOS toolbar app that I'm trying to implement drag and drop for the image the app create programatically and only in memory as NSImage. To allow the image to be draggable, I've implemented the a DraggableImage struct to be used in place of the Image view in the SwiftUI view:
struct DraggableImage: View {
let image: NSImage
var body: some View {
Image(nsImage: image)
.resizable()
.scaledToFit()
.padding()
.onDrag {
guard let tiffRepresentation = image.tiffRepresentation,
let bitmapImage = NSBitmapImageRep(data: tiffRepresentation),
let bitmapRepresentation = bitmapImage.representation(using: .png, properties: [:]) else {
return NSItemProvider(item: image.tiffRepresentation as NSSecureCoding?, typeIdentifier: kUTTypeTIFF as String)
}
let provider = NSItemProvider(item: bitmapRepresentation as NSSecureCoding?,
typeIdentifier: kUTTypePNG as String)
provider.previewImageHandler = { (handler, _, _) -> Void in
handler?(bitmapRepresentation as NSSecureCoding?, nil)
}
return provider
}
}
}
The DraggableImage struct works as expected if I am dragging the image onto an app like TextEdit or Email app. See below:
However, I could not get the picture to be draggable onto anything file based apps like the Finder window. See below:
Same thing dragging to the Desktop (which technically is a fullscreen Finder window):
What is missing in the implementation?
PS: The full demo project is hosted on GitHub for those interested in the implementation.
You need to export file URL instead of Data, so this file can be copied by MacOS.
Basic example:
let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("dragExport.png")
try! bitmapRepresentation.write(to: url)
let provider = NSItemProvider(item: url as NSSecureCoding?, typeIdentifier: kUTTypeFileURL as String)
provider.suggestedName = url.lastPathComponent

How do you Load a TextView with data from an RTF file in bundle, using Swift 5?

import UIKit
class TermsViewController: PopViewController {
var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
let fileName = "file-sample_100kB"
//https://file-examples.com/index.php/sample-documents-download/sample-rtf-download/
if let rtfPath = Bundle.main.url(forResource: fileName, withExtension: "rtf") {
do {
let attributedStringWithRtf: NSAttributedString = try NSAttributedString(url: rtfPath, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil)
self.textView.attributedText = attributedStringWithRtf
} catch let error {
print("Got an error \(error)")
}
}
view.addSubview(textView)
}
}
There's my code to display an RTF file in a textView, but attributedStringWithRtf is nil. The url has the third RTF file that I've tried.
It is also possible to import an attributed string from text in some other standard format, such as HTML or RTF. (There are also corresponding export methods.) To import, get the target text into a Data object and call init(data:options:documentAttributes:) ; alternatively, start with a file and call init(url:options:documentAttributes:). The options: allow you to specify the source text’s format.
And that was related text from Matt Neuburg's book with his posting here: Load Text View with data from RTF file in bundle using Swift 4 and Xcode, but I wasn't able to post anything there, hence the new question.

Simple Swift Cocoa app with WebKit: Upload picture doesn't work

I decided to make my own FB chat app that simply shows https://messenger.com on a WebView after trying other 'freemium' apps.
My ViewController.swift has just a few lines of code that loads URL on the web view
import Cocoa
import WebKit
class ViewController: NSViewController {
#IBOutlet weak var webView: WebView!
override func viewDidLoad() {
super.viewDidLoad()
let url = NSURL(string: "https://messenger.com")
let request = NSURLRequest(URL: url!);
webView.mainFrame.loadRequest(request);
}
override var representedObject: AnyObject? {
didSet {
// do nothing
}
}
}
Besides adding NSAppTransportSecurity key to info.plist to unblock HTTP traffic via HTTPS connection, I have not done any other settings.
Question
Please take a look at this image first.
Everything looks fine & working except two things.
Uploading image does not work - I labeled as 1 in the picture.
normally (as in other released apps or from web browsers) if you click that icon, it shows an explorer to upload a picture like below.
My app completely ignores user's click on that icon so I cannot upload any pictures to the chat. Interestingly, if I drag and drops the picture to the webview, it uploads fine.
Shared picture does not show up - I labeled as 2 in the picture.
again, from other browsers or released apps, it shows the pictures that I shared with participants like below. (of course I censored the pictures)
my app tries to load the pics, but does not display them. I can see it trying to load because I see circular progress indicator while loading.
Why?
I suspect that there might be a way to listen to the JavaScript that's triggered within the WebView and link to a file explorer or something like that?
This I have no idea. I'm logged into Messenger (basically Facebook), so I think session is not a problem here. Maybe some jQuery loading issue??
What should I do to solve these issues?
There is indeed a delegate method to open a new panel called runOpenPanelForFileButtonWithResultListener, documentation here.
In the delegate method, just create a new NSOpenPanel like this:
func webView(sender: WebView!, runOpenPanelForFileButtonWithResultListener resultListener: WebOpenPanelResultListener!, allowMultipleFiles: Bool) {
let openDialog = NSOpenPanel()
if (openDialog.runModal() == NSOKButton) {
let fileName: String = (openDialog.URL?.path)!
resultListener.chooseFilename(fileName) // Use chooseFilenames for multiple files
}
}
I just tried to create a WebView from Messagers App and images are loading well.
You should try to enable WebView options like "Autoload Images" or "Enable Animated Images" from interface builder (or by code).
This code works for me and what is nice here is that you make the download of image asynchronous. You can find more about this concept here: http://www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1 and: http://www.raywenderlich.com/79150/grand-central-dispatch-tutorial-swift-part-2
Edited
I. You have to create new Class or a new iOS Swift File named ImageLoader with this content:
class ImageLoader {
var cache = NSCache()
class var sharedLoader : ImageLoader {
struct Static {
static let instance : ImageLoader = ImageLoader()
}
return Static.instance
}
func imageForUrl(urlString: String, completionHandler:(image: UIImage?, url: String) -> ()) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), {()in
var data: NSData? = self.cache.objectForKey(urlString) as? NSData
if let goodData = data {
let image = UIImage(data: goodData)
dispatch_async(dispatch_get_main_queue(), {() in
completionHandler(image: image, url: urlString)
})
return
}
var downloadTask: NSURLSessionDataTask = NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: urlString)!, completionHandler: {(data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
if (error != nil) {
completionHandler(image: nil, url: urlString)
return
}
if data != nil {
let image = UIImage(data: data)
self.cache.setObject(data, forKey: urlString)
dispatch_async(dispatch_get_main_queue(), {() in
completionHandler(image: image, url: urlString)
})
return
}
})
downloadTask.resume()
})
}
}
II. In your actual viewController you call the method 'imageForUrl' from ImageLoaded following this lines of code:
ImageLoader.sharedLoader.imageForUrl("http://upload.wikimedia.org/wikipedia/en/4/43/Apple_Swift_Logo.png", completionHandler:{(image: UIImage?, url: String) in
self.myImage.image = image!
})
I took the code from this link: https://teamtreehouse.com/community/does-anyone-know-how-to-show-an-image-from-url-with-swift
Edited for image loaded on webview
Here is the code. It works perfect for me:
override func viewDidLoad() {
super.viewDidLoad()
let myWebView:UIWebView = UIWebView(frame: CGRectMake(0, 0, UIScreen.mainScreen().bounds.width, UIScreen.mainScreen().bounds.height))
myWebView.loadRequest(NSURLRequest(URL: NSURL(string: "https://scontent-vie1-1.xx.fbcdn.net/hphotos-xap1/v/t1.0-9/12144725_10204647881668565_4367944825116750386_n.jpg?oh=5ecdae91f5258ffe0e0355e176f8eb8a&oe=56B007CA")!))
self.view.addSubview(myWebView)
}

Can I mix UIKit and TVMLKit within one app?

I'm exploring tvOS and I found that Apple offers nice set of templates written using TVML. I'd like to know if a tvOS app that utilises TVML templates can also use UIKit.
Can I mix UIKit and TVMLKit within one app?
I found a thread on Apple Developer Forum but it does not fully answer this question and I am going through documentation to find an answer.
Yes, you can. Displaying TVML templates requires you to use an object that controls the JavaScript Context: TVApplicationController.
var appController: TVApplicationController?
This object has a UINavigationController property associated with it. So whenever you see fit, you can call:
let myViewController = UIViewController()
self.appController?.navigationController.pushViewController(myViewController, animated: true)
This allows you to push a Custom UIKit viewcontroller onto the navigation stack. If you want to go back to TVML Templates, just pop the viewController off of the navigation stack.
If what you would like to know is how to communicate between JavaScript and Swift, here is a method that creates a javascript function called pushMyView()
func createPushMyView(){
//allows us to access the javascript context
appController?.evaluateInJavaScriptContext({(evaluation: JSContext) -> Void in
//this is the block that will be called when javascript calls pushMyView()
let pushMyViewBlock : #convention(block) () -> Void = {
() -> Void in
//pushes a UIKit view controller onto the navigation stack
let myViewController = UIViewController()
self.appController?.navigationController.pushViewController(myViewController, animated: true)
}
//this creates a function in the javascript context called "pushMyView".
//calling pushMyView() in javascript will call the block we created above.
evaluation.setObject(unsafeBitCast(pushMyViewBlock, AnyObject.self), forKeyedSubscript: "pushMyView")
}, completion: {(Bool) -> Void in
//done running the script
})
}
Once you call createPushMyView() in Swift, you are free to call pushMyView() in your javascript code and it will push a view controller onto the stack.
SWIFT 4.1 UPDATE
Just a few simple changes to method names and casting:
appController?.evaluate(inJavaScriptContext: {(evaluation: JSContext) -> Void in
and
evaluation.setObject(unsafeBitCast(pushMyViewBlock, to: AnyObject.self), forKeyedSubscript: "pushMyView" as NSString)
As mentioned in the accepted answer, you can call pretty much any Swift function from within the JavaScript context. Note that, as the name implies, setObject:forKeyedSubscript: will also accept objects (if they conform to a protocol that inherits from JSExport) in addition to blocks, allowing you to access methods and properties on that object. Here's an example
import Foundation
import TVMLKit
// Just an example, use sessionStorage/localStorage JS object to actually accomplish something like this
#objc protocol JSBridgeProtocol : JSExport {
func setValue(value: AnyObject?, forKey key: String)
func valueForKey(key: String) -> AnyObject?
}
class JSBridge: NSObject, JSBridgeProtocol {
var storage: Dictionary<String, String> = [:]
override func setValue(value: AnyObject?, forKey key: String) {
storage[key] = String(value)
}
override func valueForKey(key: String) -> AnyObject? {
return storage[key]
}
}
Then in your app controller:
func appController(appController: TVApplicationController, evaluateAppJavaScriptInContext jsContext: JSContext) {
let bridge:JSBridge = JSBridge();
jsContext.setObject(bridge, forKeyedSubscript:"bridge");
}
Then you can do this in your JS: bridge.setValue(['foo', 'bar'], "baz")
Not only that, but you can override views for existing elements, or define custom elements to use in your markup, and back them with native views:
// Call lines like these before you instantiate your TVApplicationController
TVInterfaceFactory.sharedInterfaceFactory().extendedInterfaceCreator = CustomInterfaceFactory()
// optionally register a custom element. You could use this in your markup as <loadingIndicator></loadingIndicator> or <loadingIndicator /> with optional attributes. LoadingIndicatorElement needs to be a TVViewElement subclass, and there are three functions you can optionally override to trigger JS events or DOM updates
TVElementFactory.registerViewElementClass(LoadingIndicatorElement.self, forElementName: "loadingIndicator")
Quick custom element example:
import Foundation
import TVMLKit
class LoadingIndicatorElement: TVViewElement {
override var elementName: String {
return "loadingIndicator"
}
internal override func resetProperty(resettableProperty: TVElementResettableProperty) {
super.resetProperty(resettableProperty)
}
// API's to dispatch events to JavaScript
internal override func dispatchEventOfType(type: TVElementEventType, canBubble: Bool, cancellable isCancellable: Bool, extraInfo: [String : AnyObject]?, completion: ((Bool, Bool) -> Void)?) {
//super.dispatchEventOfType(type, canBubble: canBubble, cancellable: isCancellable, extraInfo: extraInfo, completion: completion)
}
internal override func dispatchEventWithName(eventName: String, canBubble: Bool, cancellable isCancellable: Bool, extraInfo: [String : AnyObject]?, completion: ((Bool, Bool) -> Void)?) {
//...
}
}
And here's how to set up a custom interface factory:
class CustomInterfaceFactory: TVInterfaceFactory {
let kCustomViewTag = 97142 // unlikely to collide
override func viewForElement(element: TVViewElement, existingView: UIView?) -> UIView? {
if (element.elementName == "title") {
if (existingView != nil) {
return existingView
}
let textElement = (element as! TVTextElement)
if (textElement.attributedText!.length > 0) {
let label = UILabel()
// Configure your label here (this is a good way to set a custom font, for example)...
// You can examine textElement.style or textElement.textStyle to get the element's style properties
label.backgroundColor = UIColor.redColor()
let existingText = NSMutableAttributedString(attributedString: textElement.attributedText!)
label.text = existingText.string
return label
}
} else if element.elementName == "loadingIndicator" {
if (existingView != nil && existingView!.tag == kCustomViewTag) {
return existingView
}
let view = UIImageView(image: UIImage(named: "loading.png"))
return view // Simple example. You could easily use your own UIView subclass
}
return nil // Don't call super, return nil when you don't want to override anything...
}
// Use either this or viewForElement for a given element, not both
override func viewControllerForElement(element: TVViewElement, existingViewController: UIViewController?) -> UIViewController? {
if (element.elementName == "whatever") {
let whateverStoryboard = UIStoryboard(name: "Whatever", bundle: nil)
let viewController = whateverStoryboard.instantiateInitialViewController()
return viewController
}
return nil
}
// Use this to return a valid asset URL for resource:// links for badge/img src (not necessary if the referenced file is included in your bundle)
// I believe you could use this to cache online resources (by replacing resource:// with http(s):// if a corresponding file doesn't exist (then starting an async download/save of the resource before returning the modified URL). Just return a file url for the version on disk if you've already cached it.
override func URLForResource(resourceName: String) -> NSURL? {
return nil
}
}
Unfortunately, view/viewControllerForElement: will not be called for all elements. Some of the existing elements (like collection views) will handle the rendering of their child elements themselves, without involving your interface factory, which means you'll have to override a higher level element, or maybe use a category/swizzling or UIAppearance to get the effect you want.
Finally, as I just implied, you can use UIAppearance to change the way certain built-in views look. Here's the easiest way to change the appearance of your TVML app's tab bar, for example:
// in didFinishLaunching...
UITabBar.appearance().backgroundImage = UIImage()
UITabBar.appearance().backgroundColor = UIColor(white: 0.5, alpha: 1.0)
If you already have a native UIKit app for tvOS, but would like to extend it by using TVMLKit for some part of it, You can.
Use the TVMLKit as a sub app in your native tvOS app. The following app shows how to do this, by retaining the TVApplicationController and present the navigationController from the TVApplicationController. The TVApplicationControllerContext is used to transfer data to the JavaScript app, as the url is transferred here :
class ViewController: UIViewController, TVApplicationControllerDelegate {
// Retain the applicationController
var appController:TVApplicationController?
static let tvBaseURL = "http://localhost:9001/"
static let tvBootURL = "\(ViewController.tvBaseURL)/application.js"
#IBAction func buttonPressed(_ sender: UIButton) {
print("button")
// Use TVMLKit to handle interface
// Get the JS context and send it the url to use in the JS app
let hostedContContext = TVApplicationControllerContext()
if let url = URL(string: ViewController.tvBootURL) {
hostedContContext.javaScriptApplicationURL = url
}
// Save an instance to a new Sub application, the controller already knows what window we are running so pass nil
appController = TVApplicationController(context: hostedContContext, window: nil, delegate: self)
// Get the navigationController of the Sub App and present it
let navc = appController!.navigationController
present(navc, animated: true, completion: nil)
}
Yes. See the TVMLKit Framework, whose docs start with:
The TVMLKit framework enables you to incorporate JavaScript and TVML files in your binary apps to create client-server apps.
From a quick skim of those docs, it looks like you use the various TVWhateverFactory classes to create UIKit views or view controllers from TVML, after which you can insert them into a UIKit app.

How come I can't successfully load an NSImage from it's full path? Swift 2

I'm trying to load an image from an absolute path into an NSImage and even though the same full path works in other scenarios when I'm using it in this context, the variable just ends up being nil. I've tried using both the file path and an NSURL to achieve it.
//: Playground - noun: a place where people can play
import Cocoa
import AppKit
print ("Starting")
/**
Attempt to do via NSURL
**/
// The workspace
var workspace = NSWorkspace.sharedWorkspace()
// Main screen
var screen = NSScreen.mainScreen()
let filemgr = NSFileManager.defaultManager()
print(filemgr.currentDirectoryPath)
let filePath1 = "/Users/daniel/Google Drive/elementarian wallpapers/Bicycle by midnighttokerkate.png"
let filePath2 = "/Users/daniel/Google Drive/elementarian wallpapers/Buildings Foggy Sky by solutionall.png"
let file1 = NSURL(fileURLWithPath: filePath1, isDirectory: false)
let file2 = NSURL(fileURLWithPath: filePath2, isDirectory: false)
do {
try workspace.setDesktopImageURL(file2, forScreen: screen!, options: [:])
print("Successfully set background from NSURL")
} catch {
print("Failed to set")
}
print("Attempting to set from NSData...")
/**
Attempt to do via NSData
**/
// Load image from URL first
if filemgr.fileExistsAtPath(filePath2) {
print("File exists")
var image = NSImage(contentsOfURL: file2)
if image != nil {
print ("Not nil")
} else {
print ("Nil")
}
} else {
print("File not found")
}
My final goal is to load the image into an NSData object so I can run a transform on it and then set the manipulated in memory image as the desktop background.
As per Droppy's comment, this was due to the Swift Playground running in a sandbox and not allowing access to files outside of it's workplace. I've moved the images I need into the playground itself to test and it now works as expected. Details on how to do this can be found here.

Resources