How to reuse fonts in Swift? - ios

Imagine I have this custom font
font.ttf
And I have two projects. Project A in /projects/A and project B in /projects/B
I don't want to copy paste this font among projects.
I created a cocoa touch framework and added the font.ttf to that. Then reused that framework in projects A and B. But it doesn't work. Is it possible?

According to this thread, you can try
let bundle = Bundle(identifier: "frameworkIdentifier")!
let url = bundle.url(forResource: "fontName", withExtension: "ttf")!
if let dataProvider = CGDataProvider(url: url as CFURL) {
let font = CGFont(dataProvider)
print(font)
}
//
//

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)

How to load an html file into a webView for multiple targets

Inside my app I have a html credits file which I want to load into a webView. The code I'm using works perfectly fine for my main target. However later on I created a second target: the pro-version of the app but for this one the code doesn't work because url is nil.
Both targets are selected on Target Membership for credits.html
I have tried changing the Location of the file in the identity inspector to all possibilities with no success.
I guess the file is somehow saved with a path only the main target can access.
if let urlPath = Bundle.main.path(forResource: "credits", ofType: "html") {
if let url = URL(string: urlPath) {
webView.loadRequest(URLRequest(url: url))
}
}
You need to use Bundle(identifier:) to get a bundle for a specific target. You can learn the Bundle identifier for each specific target by going to the Build settings of your target and looking at the Product Bundle Identifier variable.
if let targetBundle = Bundle(identifier: "yourBundleId"), let filePath = targetBundle.url(forResource: "credits", withExtension: "html") {
webView.loadRequest(URLRequest(url: url))
}

How to localize iMessage stickers?

I am creating an iMessage Sticker Pack Extension that have stickers that contain texts. I don't see any option in Xcode to localize the stickers? Is it possible to localize stickers?
Maybe it's obvious, but you can manage it inside of an extension. If you create not a pack, but extension, you can fetch exact stickers for your localization
If you want to show different sticker-assets dependant on e.g. your device's language, you need to create a iMessage Application instead of a Sticker Pack Application.
And you need to write some code as it's not possible to have this behaviour within a simple Sticker Pack Application.
However this is pretty simple. For a start, follow this tutorial:
http://blog.qbits.eu/ios-10-stickers-application/
Some code-syntax there is outdated, but XCode will help you to easily fix that.
You can place your localized resources inside different folders and drag'n'drop them into you XCode project (make sure to check "Create folder references"):
Screenshot of project structure
You can then do something like this in your viewDidLoad():
//-- get current language set
var imageSetPath = "/EN"; //-- default to english
let languageCode = NSLocale.preferredLanguages[0]
if languageCode.hasPrefix("zh-Hans") { imageSetPath = "/CNS" }
else if languageCode.hasPrefix("zh-Hans") { imageSetPath = "/CNT" }
else if languageCode.hasPrefix("ko") { imageSetPath = "/KR" }
else if languageCode.hasPrefix("ja") { imageSetPath = "/JP" }
//-- load localized stickers
imageUrls = recursivePathsForResources(path: Bundle.main.bundlePath + imageSetPath, type: "png")
loadStickers(urls: imageUrls)
where
func recursivePathsForResources(path: String, type: String) -> [URL] {
// Enumerators are recursive
let enumerator = FileManager.default.enumerator(atPath: path)
var filePaths = [URL]()
while let filePath = enumerator?.nextObject() as? String {
if NSURL(fileURLWithPath: filePath).pathExtension == type {
let url = URL(fileURLWithPath: path).appendingPathComponent(filePath)
filePaths.append(url)
}
}
return filePaths
}
(altered from https://stackoverflow.com/a/5860015/6649403)

Overriding images in a storyboard in a framework

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:").

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