Using fonts from xcfamework - ios

So I have xcframework which have fonts file (from react-native-vector-icons).
In test project (testApp), I am importing xcframework and I was expecting it to copy resources from my framework but unfortunately it doesn't copy these fonts which are there inside the xcframework and I have to manually add it in Copy Bundle Resources.
For end user, I don't want them to go inside framework and add manually.
In my framework, I have Cops Pods resources in Build Phase but haven't done anything such which would tell the project consuming this framework to copy these fonts (or resources).
What do I need to do so that in project it copies fonts (or resources) by itself? instead of manually doing it?

If you distribute your xcframework using Cocoapods then you can have any resource file automatically copied in the destination .app bundle by Cocoapods.
In your podspec you define the resources you need to copy and the xcframework (and NO spec.sources):
spec.resources = ['Fonts/EncodeSans-ExtraBold.ttf']
spec.vendored_frameworks = 'Example.xcframework'
Or from within the xcframework:
spec.resources = ['Example.xcframework/<a path>/EncodeSans-ExtraBold.ttf']
spec.vendored_frameworks = 'Example.xcframework'
When you execute pod install Cocoapods will create a "[CP] Copy Pods Resources" phase that will copy in the destination app whatever you have defined in the resources:
To avoid collisions(the user or other pod might copy the same file), better use resource_bundles instead of resources.

You need to register the fonts with the font manager. I did this extending UIFont
extension UIFont {
private class ModuleClass {}
static func fontsURLs() -> [URL] {
let bundle = Bundle(for: ModuleClass.self)
let fileNames = ["Feather"]
return fileNames.compactMap { fontName in
bundle.url(forResource: fontName, withExtension: "ttf")
}
}
/// Registers the specified graphics font with the font manager.
/// - Parameter url: Bundle URL of the font to register
/// - Throws: If font cannot be registered
static func register(from url: URL) throws {
guard let fontDataProvider = CGDataProvider(url: url as CFURL) else {
throw FontError.dataProviderNotCreated(message: "Could not create font data provider for \(url).")
}
guard let font = CGFont(fontDataProvider) else {
throw FontError.fontNotCreated(message: "Could not create font data provider for \(url).")
}
var error: Unmanaged<CFError>?
guard CTFontManagerRegisterGraphicsFont(font, &error) else {
throw error?.takeUnretainedValue() ?? FontError.fontNotRegistered
}
}
}
Then, you need use this.
public extension UIFont {
/// Registers the specified graphics font with the font manager.
/// Registered fonts participate in font descriptor matching.
static func registerFonts() {
let urls = fontsURLs()
for url in urls {
do {
try register(from: url)
} catch {
debugPrint(error)
}
}
}
}
and in your main project (app) register the fonts
// In AppDelegate.swift
import YourModule
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UIFont.registerFonts()
return true
}

Related

What's the definition of Font Asset in the Asset Catalog?

Starting from iOS 13, CTFontManager has the following function:
#discussion Font assets are extracted from the asset catalog and registered. This call must be made after the completion handler of either NSBundleResourceRequest beginAccessingResourcesWithCompletionHandler: or conditionallyBeginAccessingResourcesWithCompletionHandler: is called successfully.
Name the assets using Postscript names for individual faces, or family names for variable/collection fonts. The same names can be used to unregister the fonts with CTFontManagerUnregisterFontDescriptors. In iOS, fonts registered with the persistent scope are not automatically available to other processes. Other process may call CTFontManagerRequestFonts to get access to these fonts.
#param fontAssetNames
Array of font name assets in asset catalog.
...
CTFontManagerRegisterFontsWithAssetNames(_ fontAssetNames: CFArray, _ bundle: CFBundle?, _ scope: CTFontManagerScope, _ enabled: Bool, _ registrationHandler: ((CFArray, Bool) -> Bool)?)
However, Asset Catalog does not have any way to add "Font Assets".
What I've tried:
took font KanitRegular.ttf (PostScript name is Kanit-Regular) here.
created Data Asset named Kanit-Regular in the asset catalog.
Renamed font file to Kanit-Regular.ttf and put it into the data asset.
Data Asset's Contents.json now looks like this:
{
"data" : [
{
"filename" : "Kanit-Regular.ttf",
"idiom": "universal",
"universal-type-identifier" : "public.truetype-ttf-font"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Tried to load this font via CTFontManager
like this:
func registerFont() {
var cfBundle: CFBundle?
if let bundle = Bundle(for: type(of: self)) {
cfBundle = CFBundleCreate(kCFAllocatorDefault, bundle.bundleURL as CFURL)
}
CTFontManagerRegisterFontsWithAssetNames(["Kanit-Regular"] as CFArray, cfBundle, .persistent, true) { (errors, done) -> Bool in
print(errors)
return done
}
}
After this, getting errors printed:
▿ 1 element
- 0 : Error Domain=NSPOSIXErrorDomain Code=22 "Invalid argument" UserInfo={CTFontManagerErrorFontAssetNameKey=(
"Kanit-Regular"
)}
Is there any way to make it work?
Got it working by making the following:
Tag the Kanit-Regular data asset with fonts On-Demand Resource Tag (tag name can be the name of your preference, fonts is just the example).
Put fonts tag into Initial Install Tags Prefetched Resource Tags section
Add Fonts Capability in Signing & Capabilities and tick all boxes in it
Implement bundle resource access request before registering fonts
like this:
func registerFont() {
var cfBundle: CFBundle?
var resourceRequest: NSBundleResourceRequest?
if let bundle = Bundle(for: type(of: self)) {
resourceRequest = NSBundleResourceRequest(tags: Set(arrayLiteral: "fonts"), bundle: bundle)
cfBundle = CFBundleCreate(kCFAllocatorDefault, bundle.bundleURL as CFURL)
}
resourceRequest?.beginAccessingResources() { error in
if let error = error {
print(error)
} else {
CTFontManagerRegisterFontsWithAssetNames(["Kanit-Regular"] as CFArray, cfBundle, .persistent, true) { (errors, done) -> Bool in
print(errors)
return done
}
}
}
}
Outcome
When scope is passed as .persistent or .user, on initial call of CTFontManagerRegisterFontsWithAssetNames function the user will be asked to install fonts into the system, which is not what I really need. In case the scope is .process or .none, the same errors output is returned as provided at the end of the question.
Although this function does not fit my needs, I at least validated that it is working. Maybe someone finds it useful.

Flutter iOS File Sharing - Cannot Open Shared File

I am building a flutter app for iOS. I have created a file type that my app and another app can share back and forth. I am using the flutter receive_sharing_intent library to achieve this. To test it, I made all the necessary changes to my info.plist file to handle my custom file type, I placed an example file of that file type in the downloads folder of my device (testing on ipad), and I click on it from there to open it up in my app. The OS knows that my app can handle it. So, my app opens, it is receives the path of the shared file, but my app can't open the file. Here is the code in my main.dart that is handling reception of the file:
StreamSubscription _intentDataStreamSubscription =
ReceiveSharingIntent.getTextStream().listen((String value) {
setState(() {
try{
//This was added to test and make sure the directory actually exists.
//The value for the directory path was added after I ran it once to capture where the shared file was coming from.
Directory dir = Directory("/private/var/mobile/Containers/Shared/AppGroup/7DD4B316-3D73-4339-9B11-7516DE52F6FC/File Provider Storage/Downloads/");
bool dirExists = dir.existsSync();
var files = dir.listSync();
File drawingFile = File.fromUri(Uri.parse(value));
bool fileExists = drawingFile.existsSync();
var contents = drawingFile.readAsStringSync();
DrawingCanvas canvas = DrawingCanvas.fromFileContents(contents);
DrawingCanvasBloc canvasBloc = DrawingCanvasBloc(canvas);
Navigator.of(context).push(MaterialPageRoute(builder: (context) => CanvasScreen(canvasBloc)));
}
catch(e){
log(e.toString());
}
});
}, onError: (err) {
print("getLinkStream error: $err");
});
Scenario: I run the application. I go into my downloads folder in files on the ipad. I select my example.fbg file located there (my custom file type). My app opens up.
In the above code it blows up when I try to list the contents of the directory. I put this in here (after previously catching the directory path and hard coding it) to test to make sure I was even getting the right location. dirExists is true but I can't list the files in it. The error I get is:
FileSystemException: Directory listing failed, path = '/private/var/mobile/Containers/Shared/AppGroup/7DD4B316-3D73-4339-9B11-7516DE52F6FC/File Provider Storage/Downloads/' (OS Error: Operation not permitted, errno = 1)
If I take that line out and continue down to the opening of the file (readAsStringSync) I get:
FileSystemException: Cannot open file, path = '/private/var/mobile/Containers/Shared/AppGroup/7DD4B316-3D73-4339-9B11-7516DE52F6FC/File Provider Storage/Downloads/example.fbg' (OS Error: Operation not permitted, errno = 1)
I'm not sure why it won't let me access this file. Is there a permissions thing I'm missing? Let me know if I need to include more information in the question and I will update.
Finally found a work around and got it working using this answer. What I ended up doing was opening the file in the app delegate, saving the file to the apps documents directory, then passing that url to the Flutter application. I opened the iOS module in xcode and changed the AppDelegate.swift to the following:
import UIKit
import Flutter
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
var drawingFilename = ""
do {
let isAcccessing = url.startAccessingSecurityScopedResource()
var error: Error? = nil
let path = url.path
let string = try String(contentsOf: url)
drawingFilename = (url.path as NSString).lastPathComponent
print(drawingFilename)
let filename = getDocumentsDirectory().appendingPathComponent(drawingFilename)
do {
try string.write(to: filename, atomically: true, encoding: String.Encoding.utf8)
} catch {
// failed to write file – bad permissions, bad filename, missing permissions, or more likely it can't be converted to the encoding
}
if isAcccessing {
url.stopAccessingSecurityScopedResource()
}
if #available(iOS 9.0, *) {
return super.application(app, open: filename, options: options)
} else {
return false
}
} catch {
print("Unable to load data: \(error)")
return false
}
}
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
}
Hope this helps someone in the future.

Use AWSMobileClient without `awsconfiguration.json`in iOS

I'd like to authenticate an iOS device to use AppSync/S3 services via Cognito user pools. The AWSMobileClient provides some nice conveniences but the initialization requires that you're bundle have an awsconfiguration.json file -- which our application will define dynamically. Is there a way to configure that manually?
The current solution is to use the multi-environment workflow from the CLI.
https://aws-amplify.github.io/docs/cli/multienv?sdk=ios
Edit
If the multi-environment workflow from the Amplify team doesn't work for you, what you can do is create debug and prod versions of your config, and then create a build phase that copies the correct one based on your build settings (debug vs release, etc). This is working extremely well for one of my projects.
#export; #Prints list of all xcode variables with values
printf "$CONFIGURATION\n";
if [ "$CONFIGURATION" = "Debug" ]; then
printf "creating debug configuration";
cp -r "$PROJECT_DIR/awsconfiguration-debug.json" "$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/awsconfiguration.json"
else
printf "creating production configuration";
cp -r "$PROJECT_DIR/awsconfiguration-prod.json" "$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/awsconfiguration.json"
fi
As of AWS iOS SDK 2.11.0 (9th September 2019) it is now possible to configure without an awsconfiguration.json file.
It's even documented in the amplify documentation here
See also my answer to a related question
Here's a specific solution:
extension AWSMobileClient {
convenience init?(configuration url: URL) {
guard let data = try? Data(contentsOf: url) else { return nil }
guard let dict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any] else { return nil }
self.init(configuration: dict)
}
convenience init?(configuration name: String) {
guard let url = Bundle.main.url(forResource: name, withExtension: "json") else {
return nil
}
print("INITIALIZING AWSMobileClient [\(name)]")
self.init(configuration: url)
}
}
To use it, you can have as many different awsconfiguration-XXX.json files as you need, and at runtime you initialize with the one you want by:
let mobileClient = AWSMobileClient(configuration: "awsconfiguration-XXX")
mobileClient.initialize { (userState, error) in ... }

Use resource bundles in CocoaPods

I am making a pod (MySDK) and would like to load the assets from the separate resource bundles CocoaPods generates.
However, I can not get it to work.
Here is how I tried to load the storyboard:
let storyBoard = UIStoryboard(name: "SDK", bundle: Bundle(identifier:"org.cocoapods.SchedJoulesSDK"))
This gives the error:
'Could not find a storyboard named 'SDK' in bundle
The bundle is added in Xcode:
And my podspec looks like this:
s.resource_bundles = {
'MySDK' => ['SDK/*/*.{xib,storyboard,xcassets}']
}
Any ideas?
If you use resource or resources in a CocoaPods PodSpec file, you tell Cocoapods that these are the resource files your library will load during runtime.
If your library is built as a dynamic framework, these files are just copied to the resource folder path of that framework and everything will be fine. Yet if your library is built as a static library, these are copied to the resource folder of the main application bundle (.app) and this can be a problem as this main application may already have a resource of that name or another Pod may have a resource of that name, in that case these files will overwrite each other. And whether a Pod is built as dynamic framework or as a static library is not specified by the PodSpec but in the Podfile by the application integrating your Pod.
Thus for Pods with resources, it is highly recommended to use resource_bundles instead!
In your case, the lines
s.resource_bundles = {
'MySDK' => ['SDK/*/*.{xib,storyboard,xcassets}'] }
tell CocoaPods to create a resource bundle named MySDK (MySDK.bundle) and place all files matching the pattern into that resource bundle. If your Pod is built as a framework, this bundle is located in the resources folder of your framework bundle; if it is built as a static library, the bundle is copied to the resources folder of the main application bundle, which should be safe if you name your bundle the same way as your Pod (you should not name it "MySDK", rather "SchedJoulesSDK").
This bundle will have the same identifier as your Pod, however when dynamic frameworks are built, your framework bundle will have that identifier as well and then it's undefined behavior which bundle is being loaded when you load it by identifier (and currently the outer bundle always wins in my tests).
Correct code would look like this (not tested, though):
// Get the bundle containing the binary with the current class.
// If frameworks are used, this is the frameworks bundle (.framework),
// if static libraries are used, this is the main app bundle (.app).
let myBundle = Bundle(for: Self.self)
// Get the URL to the resource bundle within the bundle
// of the current class.
guard let resourceBundleURL = myBundle.url(
forResource: "MySDK", withExtension: "bundle")
else { fatalError("MySDK.bundle not found!") }
// Create a bundle object for the bundle found at that URL.
guard let resourceBundle = Bundle(url: resourceBundleURL)
else { fatalError("Cannot access MySDK.bundle!") }
// Load your resources from this bundle.
let storyBoard = UIStoryboard(name: "SDK", bundle: resourceBundle)
As resourceBundle cannot change at runtime, it is safe to create it only once (e.g. on app start or when your framework is initialized) and store it into a global variable (or global class property), so you have it always around when needed (a bundle object also hardly uses any RAM memory, as it only encapsulates meta data):
final class SchedJoulesSDK {
static let resourceBundle: Bundle = {
let myBundle = Bundle(for: SchedJoulesSDK.self)
guard let resourceBundleURL = myBundle.url(
forResource: "MySDK", withExtension: "bundle")
else { fatalError("MySDK.bundle not found!") }
guard let resourceBundle = Bundle(url: resourceBundleURL)
else { fatalError("Cannot access MySDK.bundle!") }
return resourceBundle
}()
}
The property is initialized lazy (that's default for static let properties, no need for the lazy keyword) and the system ensures that this happen only once, as a let property must not changed once initialized. Note that you cannot use Self.self in that context, you need to use the actual class name.
In your code you can now just use that bundle wherever needed:
let storyBoard = UIStoryboard(name: "SDK",
bundle: SchedJoulesSDK.resourceBundle)
You can use like...
s.resource = "icon.png" //for single file
or
s.resources = "Resources/*.png" //for png file
or
s.resources = "Resources/**/*.{png,storyboard}" //for storyboard and png files
or
s.resource_bundles = {
"<ResourceBundleName>" => ["path/to/resources/*/**"]
}
This is what I ended up with...
import Foundation
public extension Bundle {
public static func resourceBundle(for frameworkClass: AnyClass) -> Bundle {
guard let moduleName = String(reflecting: frameworkClass).components(separatedBy: ".").first else {
fatalError("Couldn't determine module name from class \(frameworkClass)")
}
let frameworkBundle = Bundle(for: frameworkClass)
guard let resourceBundleURL = frameworkBundle.url(forResource: moduleName, withExtension: "bundle"),
let resourceBundle = Bundle(url: resourceBundleURL) else {
fatalError("\(moduleName).bundle not found in \(frameworkBundle)")
}
return resourceBundle
}
}
Example usage...
class MyViewController: UIViewController {
init() {
super.init(nibName: nil, bundle: Bundle.resourceBundle(for: Self.self))
}
}
or
tableView.register(UINib(nibName: Self.loginStatusCellID, bundle: Bundle.resourceBundle(for: Self.self)), forCellReuseIdentifier: Self.loginStatusCellID)
I have the same issue, if I only use the bundle line:
s.resource_bundles = {
'MySDK' => ['SDK/*/*.{xib,storyboard,xcassets}']
}
I get a runtime crash when I reference my storyboard. However, if I explicitly add each storyboard as a resource like so:
s.resources = ["Resources/MyStoryboard.storyboard", "Resources/MyStoryboard2.storyboard"]
Everything works fine. I don't think we should have to explicitly add each storyboard as a resource, but I haven't been able to make it work any other way.
One thing you could try is changing you resource bundle reference to recursively search in your folders with the ** nomenclature like this:
s.resource_bundles = {
'MySDK' => ['SDK/**/*.{xib,storyboard,xcassets}']
}

Get all URLs for resources in sub-directory in Swift

I am attempting to create an array of URLs for all of the resources in a sub-directory in my iOS app. I can not seem to get to the correct path, I want to be able to retrieve the URLs even if I do not know the names (i.e. I don't want to hard code the file names into the code).
Below is a screen shot of the hierarchy, I am attempting to get all the files in the 'test' folder:
Any help is greatly appreciated, I have attempted to use file manager and bundle main path but to no joy so far.
This is the only code I have currently:
let fileManager = FileManager.default
let path = Bundle.main.urls(forResourcesWithExtension: "pdf", subdirectory: "Files/test")
print(path)
I have also tried this code but this prints all resources, I can't seem to specify a sub-directory:
let fm = FileManager.default
let path = Bundle.main.resourcePath!
do {
let items = try fm.contentsOfDirectory(atPath: path)
for item in items {
print("Found \(item)")
}
} catch {
// failed to read directory – bad permissions, perhaps?
}
Based on an answer from #vadian , The folders were changed from virtual groups to real folders. Using the following code I was able to get a list of resources:
let fileManager = FileManager.default
let path = Bundle.main.resourcePath
let enumerator:FileManager.DirectoryEnumerator = fileManager.enumerator(atPath: "\(path!)/Files/test")!
while let element = enumerator.nextObject() as? String {
if element.hasSuffix("pdf") || element.hasSuffix("jpg") { // checks the extension
print(element)
}
}
Consider that the yellow folders are virtual groups, not real folders (although Xcode creates real folders in the project directory). All files in the yellow folders are moved into the Resources directory in the bundle when the app is built.
Real folders in the bundle are in the project navigator.
You can follow the following steps to get them:
Create a new folder inside your project folder with the extension is .bundle (for example: Images.bundle).
Copy resource files into that new folder.
Drag that new folder into the project that opening in Xcode.
Retrieve the URLs by using the following code snippet:
let urls = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: "Images.bundle")
You can also view the guide video here: https://youtu.be/SpMaZp0ReEo
I came across a similar issue today. I needed to retrieve the URL of a resource file in a bundle ignoring its path.
I wrote the following:
public extension Bundle {
/// Returns the file URL for the resource identified by the specified name, searching all bundle resources.
/// - Parameter resource: The name of the resource file, including the extension.
/// - Returns: The file URL for the resource file or nil if the file could not be located.
func recurseUrl(forResource resource: String) -> URL? {
let contents = FileManager.default.allContentsOfDirectory(atPath: self.bundlePath)
for content in contents {
let fileNameWithPath = NSString(string: content)
if let fileName = fileNameWithPath.pathComponents.last {
if resource == fileName {
return URL(fileURLWithPath: content)
}
}
}
return nil
}
Based on this:
public extension FileManager {
/// Performs a deep search of the specified directory and returns the paths of any contained items.
/// - Parameter path: The path to the directory whose contents you want to enumerate.
/// - Returns: An array of String objects, each of which identifies a file or symbolic link contained in path. Returns an empty array if the directory does not exists or has no contents.
func allContentsOfDirectory(atPath path: String) -> [String] {
var paths = [String]()
do {
let url = URL(fileURLWithPath: path)
let contents = try FileManager.default.contentsOfDirectory(atPath: path)
for content in contents {
let contentUrl = url.appendingPathComponent(content)
if contentUrl.hasDirectoryPath {
paths.append(contentsOf: allContentsOfDirectory(atPath: contentUrl.path))
}
else {
paths.append(contentUrl.path)
}
}
}
catch {}
return paths
}
}
Which achieves the goal of retrieving the URL for the first match of a given resource filename in a bundle's resources, all directories wide.
I tend to think that Swift's func url(forResource name: String?, withExtension ext: String?) -> URL? should behave this way in the first place.

Resources