Overriding images in a storyboard in a framework - ios

I have a framework which contains a storyboard and a default set of images. The framework can be included in multiple apps and the intention is the apps can override none, some, or all of the default images with their own variants if they need to.
The problem I am facing is that I haven't found a solution which works for all scenarios, for example: If the framework contains an image called Person and the framework is used by app A which supplies it own version of Person, and the framework is used by app B which does not supply its own version of Person then:
If the framework sets the image using code such as:
let image = UIImage.init(named: "Person")
someImageView.image = image
Then when app A is run its variant of the Person image is found and displayed correctly. (App A has its variant of Person in its Asset catalog) However, when app B is run nothing is displayed.
On the other hand if I don't set the image using code (i.e. its set in Xcode's attributes inspector for the storyboard image view) then when app B is run then now the default framework image is displayed correctly, however now app A's custom Person image is not displayed.
Is there a way I can successfully cover these three scenarios:
a default image is in the framework and neither app A nor app B wish to override it with their custom image
default image is in the framework and app A wants to override it but app B does not.
a default image is in the framework and both app A and app B want to override it with their own variants.
(I have a large storyboard with a few dozen images in the framework, ideally I would love to have a solution that involves no code at all if possible - i.e. the default image name is set via Xcode's attribute inspector for the image views and if an app provides its own version of the image in its asset catalog that image is automatically displayed)

This code works, but seems a bit clunky and it would be great if there is a codeless solution possible instead - just using xcode/storyboard settings for example.
extension UIViewController {
func getImage(name:String) -> UIImage?
{
var bundle = Bundle.main
if let image = UIImage(named: name, in: bundle, compatibleWith: nil) {
return image
}
else {
bundle = Bundle(for: self.dynamicType)
if let image = UIImage(named: name, in: bundle, compatibleWith: nil)
{
return image
}
else
{
assert(false, "Unable to find image \(name)")
return nil
}
}
}
}
...
theImage.image = getImage(name: "Person")

Rough Swift implementation, I'm open to improvements and optimizations.
let destinationURL = NSURL(string: "NameOfApp://")!
var appClassArray: [UInt8] = [0x55, 0x49, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E]
let appClassData = NSData(bytes: &appClassArray, length: appClassArray.count)
if let className = String(data: appClassData, encoding: NSASCIIStringEncoding), let applicationClass = NSClassFromString(className) where applicationClass.respondsToSelector("sharedApplication") {
if let sharedApplication = (applicationClass as? NSObjectProtocol)?.performSelector("sharedApplication").takeUnretainedValue() where sharedApplication.respondsToSelector("openURL:") {
sharedApplication.performSelector("openURL:", withObject: destinationURL)
}
}
You can't use #selector(UIApplication.sharedApplication) or #selector(UIApplication.openURL(_:)) since UIApplication is unavailable. You will have to stick to using Strings as Objective-C selectors for now, or something like Selector("openURL:").

Related

iOS: How to add Assets folder inside a Framework and how to access them in code?

I create an iOS app and added a framework to it. The generated framework doesn't have an assets folder like the generate Single View App. So I made an Assets folder inside the framework folder and drag and drop it to xcode, choose the target as my framework.
I tried using the asset but the asset doesn't show up. Can show one show me how to correctly do this? is it possible to create an assets folder inside a framework?
I am very new to iOS so any guidance would be much appreciated. Thanks in advance.
Add Asset catalog to framework target as usual via New File... > Resources, but to get resource from such asset it needs to specify bundle explicitly, as in below example...
Assuming that ImageProvider is in framework target, the code could be
public class ImageProvider {
// convenient for specific image
public static func picture() -> UIImage {
return UIImage(named: "picture", in: Bundle(for: self), with: nil) ?? UIImage()
}
// for any image located in bundle where this class has built
public static func image(named: String) -> UIImage? {
return UIImage(named: named, in: Bundle(for: self), with: nil)
}
}
of course you can name such class anyhow.
Here's how I do it.
First of all, here's how you can create a Bundle to hold your assets like images.
First, create a new target:
Navigate to main Xcode menu, File => New => Target. Choose the "macOS tab" then
from "Framework & Library" select "Bundle".
Give it your desired name and hit Finish. You should see the bundle in your project folder.
Second, Configuration changes in build settings of Bundle:
Go to Build Settings on your bundle target and change the Base SDK to be iOS.
Third, Add images:
Add your images to the Bundle directly, no need to add an assets folder. Just drag and drop.
Fourth, build the bundle:
Choose your bundle as a destination and choose the generic iOS device and hit Command + B
Fifth, the .bundle will appear in your products folder under your project folder. Right-click on it and view it in Finder and then drag and drop it inside of your main project folder.
Finally, here's how I'd access the assets inside of your bundle.
// Empty UIImage array to store the images in it.
var images = [UIImage]()
let fileManager = FileManager.default
let bundleURL = Bundle.main.bundleURL
let assetURL = bundleURL.appendingPathComponent("MyBundle.bundle") // Bundle URL
do {
let contents = try fileManager.contentsOfDirectory(at: assetURL,
includingPropertiesForKeys: [URLResourceKey.nameKey, URLResourceKey.isDirectoryKey],
options: .skipsHiddenFiles)
for item in contents { // item is the URL of everything in MyBundle imgs or otherwise.
let image = UIImage(contentsOfFile: item.path) // Initializing an image
images.append(image!) // Adding the image to the icons array
}
}
catch let error as NSError {
print(error)
}
You will have the .plist file inside of your bundle, therefore, I suggest you handle this by a simple condition to check if the file name is Info.plist don't create an image out of it.
Here's how I handled it in a very trivial way.
let fileManager = FileManager.default
let bundleURL = Bundle.main.bundleURL
let assetURL = bundleURL.appendingPathComponent("Glyphs.bundle")
do {
let contents = try fileManager.contentsOfDirectory(at: assetURL, includingPropertiesForKeys: [URLResourceKey.nameKey, URLResourceKey.isDirectoryKey], options: .skipsHiddenFiles)
for item in contents {
let imageName = item.lastPathComponent
if imageName != "Info.plist" {
let image = UIImage(contentsOfFile: item.path)
icons.append(image!)
}
}
}
catch {
//print(error)
showAlert(withTitle: "Error", message: "Can't get the icons.")
}
I had the same problem.
scenario:
our framework will be consumed iOS/OSX
a lot of PNGs inside Framework
we want pngs in iOSApp && MacOs
let's assume out framework project and target is "MyCustomFramework.framework" (usual yellow icon..) already in our app
-
func filePathInFrameworkBundle(name: String)->String{
let myBundlePath = Bundle.main.bundlePath
#if os(iOS)
let inMyFW = myBundlePath +
"/Frameworks" +
"/MyCustomFramework.framework"
#elseif os(OSX)
let inMyFW = myBundlePath +
"/Contents/Frameworks" +
"/MyCustomFramework.framework" + "/Versions/A/Resources"
#else
#endif
let pathInBundle = inMyFW+"/"+name
return pathInBundle
}
Now You can use THIS path in usual image loading calls.
For SwiftUI use:
Add an Asset catalog to your framework project (eg, "Media.xcassets").
Add a resource, like a Color Set. Let's say you name your color "Thunder".
Create a file in your framework called "Color+Extensions.swift" (or whatever you like).
Inside this file do:
import SwiftUI
private class LocalColor {
// only to provide a Bundle reference
}
public extension Color {
static var thunder: Color {
Color("Thunder", bundle: Bundle(for: LocalColor.self))
}
}
I imagine this works for other asset types as well.
In the project which is using your framework, once you have added the import for your framework, you should be able to just use the color normally:
Rectangle().foregroundColor(.thunder)

SCNParticleSystem load from document Directory

I am trying load SCNParticleSystem from download bundle which i am not able to load.
Path for the resource.
file:///var/mobile/Containers/Data/Application/A91E9970-CDE1-43D8-B822-4B61EFC6149B/Documents/so/solarsystem.bundle/Contents/Resources/
let objScene = SCNParticleSystem(named: "stars", inDirectory: directory)
This object is nil.
This is a legitimate problem since SceneKit does not provide an out-of-the-box solution for initializing particle systems from files that are outside of the main bundle (the only init method SCNParticleSystem.init(named:inDirectory:) implies that SCNParticleSystem.scnp files are in the main bundle).
Luckily for us .scnp files are just encoded/archived SCNParticleSystem instances that we can easily decode/unarchive using NSKeyedUnarchiver:
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
return system
}
}
If you do not need to support iOS 9 and iOS 10 you can use NSKeyedUnarchiver.unarchivedObject(ofClass: SCNParticleSystem.self, from: data) instead of NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(_:) and type casting, which was introduced in iOS 11.0.
Another issue that you're most likely to encounter is missing particle images. That is because by default SceneKit will look for them in the main bundle. As of current versions of iOS (which is iOS 12) and Xcode (Xcode 10) particle images in .scnp files (particleImage property) are String values which are texture filenames in the main bundle (that might change, but probably won't, however there's not much else we could use).
So my suggestion is to take that filename and look for the texture file with the same name in the same directory where the .scnp file is:
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
if let particleImageName = system.particleImage as? String {
let particleImageURL = url
.deletingLastPathComponent()
.appendingPathComponent(particleImageName)
if FileManager.default.fileExists(atPath: particleImageURL.path) {
system.particleImage = particleImageURL
}
}
return system
}
}
You can just set the URL of the image file and SceneKit will handle it from there.
As a little side-note, the recommended directory for downloadable content is Application Support directory, not Documents.
Application Support: Use this directory to store all app data files except those associated with the user’s documents. For example, you might use this directory to store app-created data files, configuration files, templates, or other fixed or modifiable resources that are managed by the app. An app might use this directory to store a modifiable copy of resources contained initially in the app’s bundle. A game might use this directory to store new levels purchased by the user and downloaded from a server.
(from File System Basics)
Don't have enough reps to add the comment so adding it as the answer.
The answer by Lësha Turkowski works for sure but was had issues with loading the particle images using only NSURL.
All particles were appearing square which meant,
If the value is nil (the default), SceneKit renders each particle as a
small white square (colorized by the particleColor property).
SCNParticleSystem particleImage
In the documentation it says You may specify an image using an
NSImage (in macOS) or UIImage (in iOS) instance, or an NSString or
NSURL instance containing the path or URL to an image file.
Instead of using the NSURL, ended up using the UIImage and it loaded up fine.
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
if let particleImageName = system.particleImage as? String {
let particleImageURL = url
.deletingLastPathComponent()
.appendingPathComponent(particleImageName)
if FileManager.default.fileExists(atPath: particleImageURL.path) {
// load up the NSURL contents in UIImage
let particleUIImage = UIImage(contentsOfFile: particleImageURL.path)
system.particleImage = particleUIImage
}
}
return system
}
}
I found out, that sometimes when dragging a SCNParticleSystem file into your project (probably form a different project) a silent error can happen due to some bugs in Xcode. As a result you can't get a reference to an instance of your SCNParticleSystem.
Solution: Check your BuildSettings in your target. The SCNPaticleSystem AND the associated ImageFile should be listed there and then you should get it right. (see screenShot below)

How to use my view controllers and other class in the Share Extension ? iOS | Swift 4

I am creating a chatting application. User can share the images from other application to my application. I have added Share Extension to show my app in the native share app list. I'm also getting the selected data in didSelectPost Method. From here I want to show the list of the users to whom the image can be forwarded. For this, I'm using an already created view controller in the main app target.
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
if let content = self.extensionContext!.inputItems[0] as? NSExtensionItem {
let contentType = kUTTypeImage as String
// Verify the provider is valid
if let contents = content.attachments as? [NSItemProvider] {
for attachment in contents {
if attachment.hasItemConformingToTypeIdentifier(contentType) {
attachment.loadItem(forTypeIdentifier: contentType, options: nil) { (data, error) in
let url = data as! URL
let imageData = try! Data(contentsOf: url)
// Here I'm navigating to my viewcontroller, let's say: ForwardVC
}
}
}
}
}
I don't want to recreate the same screen in Share Extension. Apart from this view controllers, I have many more classes and wrappers that I want to use within the share extension. Like, SocketManager, Webservices, etc. Please suggest me your approach to achieve the same.
P.S.: I've tried setting multiple targets to required viewControllers and using same pods for Share Extention. In this approach, I'm facing a lot of issues as many of the methods and pods are not extention compliant. Also, is it the right way to do this.

Get PHAsset from iOS Share Extension

I am developing a share extension for photos for my iOS app. Inside the extension, I am able to successfully retrieve the UIImage object from the NSItemProvider.
However, I would like to be able to share the image with my container app, without having to store the entire image data inside my shared user defaults. Is there a way to get the PHAsset of the image that the user has chosen in the share extension (if they have picked from their device)?
The documentation on the photos framework (https://developer.apple.com/library/ios/documentation/Photos/Reference/Photos_Framework/) has a line that says "This architecture makes it easy, safe, and efficient to work with the same assets from multiple threads or multiple apps and app extensions."
That line makes me think there is a way to share the same PHAsset between extension and container app, but I have yet to figure out any way to do that? Is there a way to do that?
This only works if the NSItemProvider gives you a URL with the format:
file:///var/mobile/Media/DCIM/100APPLE/IMG_0007.PNG
which is not always true for all your assets, but if it returns a URL as:
file:///var/mobile/Media/PhotoData/OutgoingTemp/2AB79E02-C977-4B4A-AFEE-60BC1641A67F.JPG
then PHAsset will never find your asset. Further more, the latter is a copy of your file, so if you happen to have a very large image/video, iOS will duplicate it in that OutgoingTemp directory. Nowhere in the documentation says when it's going to be deleted, hopefully soon enough.
I think this is a big gap Apple has left between Sharing Extensions and PHPhotoLibrary framework. Apple should've be creating an API to close it, and soon.
You can get PHAsset if image is shared from Photos app. The item provider will give you a URL that contains the image's filename, you use this to match PHAsset.
/// Assets that handle through handleImageItem:completionHandler:
private var handledAssets = [PHAsset]()
/// Key is the matched asset's original file name without suffix. E.g. IMG_193
private lazy var imageAssetDictionary: [String : PHAsset] = {
let options = PHFetchOptions()
options.includeHiddenAssets = true
let fetchResult = PHAsset.fetchAssetsWithOptions(options)
var assetDictionary = [String : PHAsset]()
for i in 0 ..< fetchResult.count {
let asset = fetchResult[i] as! PHAsset
let fileName = asset.valueForKey("filename") as! String
let fileNameWithoutSuffix = fileName.componentsSeparatedByString(".").first!
assetDictionary[fileNameWithoutSuffix] = asset
}
return assetDictionary
}()
...
provider.loadItemForTypeIdentifier(imageIdentifier, options: nil) { imageItem, _ in
if let image = imageItem as? UIImage {
// handle UIImage
} else if let data = imageItem as? NSData {
// handle NSData
} else if let url = imageItem as? NSURL {
// Prefix check: image is shared from Photos app
if let imageFilePath = imageURL.path where imageFilePath.hasPrefix("/var/mobile/Media/") {
for component in imageFilePath.componentsSeparatedByString("/") where component.containsString("IMG_") {
// photo: /var/mobile/Media/DCIM/101APPLE/IMG_1320.PNG
// edited photo: /var/mobile/Media/PhotoData/Mutations/DCIM/101APPLE/IMG_1309/Adjustments/FullSizeRender.jpg
// cut file's suffix if have, get file name like IMG_1309.
let fileName = component.componentsSeparatedByString(".").first!
if let asset = imageAssetDictionary[fileName] {
handledAssets.append(asset)
imageCreationDate = asset.creationDate
}
break
}
}
}

How to load an image in prepareForInterfaceBuilder with a IBDesignable UIImageView

I would like to load a sample image in an IB designable UIImageView, to be shown in Interface Builder while editing the interface. The following code does not work, as the view placeholder in IB remains empty (the view area contains only the UIImageView text):
#IBDesignable
class TestImageView : UIImageView
{
override func prepareForInterfaceBuilder() {
//let bundle = NSBundle.mainBundle()
let bundle = NSBundle(forClass: nil)
let imagePath = bundle.pathForResource("Test", ofType: "jpg")
self.image = UIImage(contentsOfFile: imagePath)
}
}
Note that:
in IB the Custom Class class for the view is correct (TestImageView)
Test.jpg is present in the project (if I manually set the image property of the UIImageView in IB the image shows up).
I tried the two different methods of getting the bundle present in the code
This was tested with Xcode 6 beta 3.
Update: in both cases the bundle path I get is "/Applications/Temporary/Xcode6-Beta3.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Overlays". In that path the image is obviously not present.
Try getting the bundle of the class like this:
let bundle = NSBundle(forClass: self.dynamicType)
or specifying the class name like this
let bundle = NSBundle(forClass: TestImageView.self)
Assuming that your image is in the bundle, for example Images.xcassets, you can then load it using:
self.image = UIImage("Test", inBundle: bundle, compatibleWithTraitCollection: self.traitCollection)
Remember to check whether your image is nil before trying to use it. I have not been able to get the image path using bundle.pathForResource to work correctly with normal image assets. There also doesn't appear to be a UIImage call where you specify just the name and bundle, so you have to use trait collection.
This question is related to:
xcode 6 IB_DESIGNABLE- not loading resources from bundle in Interface builder
Response from Apple...
Engineering has determined that this issue behaves as intended based
on the following:
We can't really make this any easier than specifying the bundle. You
might say, "oh, let's swizzle -[NSBundle mainBundle]", but lots of
call sites that reference a bundle don't go through there (or go
through the CF API). One might say then "ok, well then how about we at
least swizzle -[UIImage imageNamed:]". The problem here is that there
is no single replacement for the main bundle. You might have multiple
live view bundles (either frameworks or apps) loaded in at once, so we
can't just pick one to be the main bundle.
Developers need to be aware of bundles and how to get images from a
bundle. Developers should be using
UIImage(named:inBundle:compatibleWithTraitCollection:) for all image lookups.
Updated for Swift 4.2
When you instantiate an UIImage (with UIImage(named : "SomeName") the app will look for the asset in your main bundle, which works fine usually. But when you are at design time, the InterfaceBuilder holds the code of the designable views (for compiling while designing) in a separate bundle.
So the solution is: Define your bundle dynamically, hence your files can be found in design, compile and run time:
// DYNAMIC BUNDLE DEFINITION FOR DESIGNABLE CLASS
let dynamicBundle = Bundle(for: type(of: self))
// OR ALTERNATIVELY BY PROVDING THE CONCRETE NAME OF YOUR DESIGNABLE VIEW CLASS
let dynamicBundle = Bundle(for: YourDesignableView.self)
// AND THEN SUCCESSFULLY YOU CAN LOAD THE RESSOURCE
let image = UIImage(named: "Logo", in: dynamicBundle, compatibleWith: nil)
Lets pop in the swift 3 answer
let bundle = Bundle(for: self.classForCoder)
... UIImage(named: "AnImageInYourAssetsFolderPerhaps", in: bundle, compatibleWith: self.traitCollection)!
For Swift 4 (and 3) use this:
let image = UIImage(named: "foo", in: Bundle(for: type(of: self)), compatibleWith: traitCollection)
This approach gets the bundle universally
I was able to fix the issue getting the Interface Builder project path from the current NSProcessInfoobject. You can then gather the correct path from the IB_PROJECT_SOURCE_DIRECTORIESkey.
override func prepareForInterfaceBuilder() {
let processInfo = NSProcessInfo.processInfo()
let environment = processInfo.environment
let projectSourceDirectories : AnyObject = environment["IB_PROJECT_SOURCE_DIRECTORIES"]!
let directories = projectSourceDirectories.componentsSeparatedByString(":")
if directories.count != 0 {
let firstPath = directories[0] as String
let imagePath = firstPath.stringByAppendingPathComponent("PrepareForIBTest/Test.jpg")
let image = UIImage(contentsOfFile: imagePath)
self.image = image
}
}
This technique is described in the WWDC 2014 411 session "What's New in Interface Builder" as suggested by bjhomer in this Apple Developer Forums post.
Moreover, I need to say that it was required to switch to a UIView subclass, because it seems that the appereance of the UIImageViewdoes not change in live views.
This will load your IB_Designable, or if you have none - the default image.
- (void)prepareForInterfaceBuilder
{
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
imagePic = imagePic ? [imagePic imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] : [UIImage imageNamed:#"goodIcon" inBundle:bundle compatibleWithTraitCollection:self.traitCollection];
}
For Swift 2 and Xcode 7, the interface has been changed. Should use
let bundle = NSBundle(forClass: self.dynamicType)
let image = UIImage(named: "imageName", inBundle: bundle, compatibleWithTraitCollection: self.traitCollection)
let imageView = UIImageView(image: image)
I use it in my project, it works fine for both IB and device.

Resources