iOS 13 Custom Fonts download and installation - swift

THIS IS NOT A DUPLICATE QUESTION
I have searched for everything that I can, but I didn't find something meaningful.
Here is the question:
I want to download a font using CoreText API but it works in iOS 12, NOT available on iOS 13.
There is a demo project from Apple in 2013. But The API it used seems to be private since iOS 13.
/*
#param progressBlock
Callback block to indicate the progress.
Return true to continue, and return false to cancel the process.
This block is called on a private serial queue on OS X 10.15, iOS 13, and later.
*/
#available(iOS 6.0, *)
public func CTFontDescriptorMatchFontDescriptorsWithProgressHandler(_ descriptors: CFArray, _ mandatoryAttributes: CFSet?, _ progressBlock: #escaping CTFontDescriptorProgressHandler) -> Bool
As the documentation declared:
This block is called on a private serial queue on OS X 10.15, iOS 13, and later.
Here is my code for font download:
let fontName = "STXingkai-SC-Light"
let attributes = [kCTFontNameAttribute : fontName] as CFDictionary
let fontDescription = CTFontDescriptorCreateWithAttributes(attributes)
// create font font descriptor and add it in an array
let descs = [fontDescription] as CFArray
CTFontDescriptorMatchFontDescriptorsWithProgressHandler(descs, nil) { (state, progressParamater) -> Bool in
let progressValue = (progressParamater as Dictionary)[kCTFontDescriptorMatchingPercentage]?.doubleValue
switch state {
case .didBegin: print("didBegin")
case .didFinish: print("didFinish")
case .willBeginDownloading: print("willBeginDownloading")
case .didFinishDownloading:
print("--> download finish")
DispatchQueue.main.async {
self.fontLabel.font = UIFont(name: self.fontName, size: 20)
}
case .downloading:
print("downloading#####\(progressValue ?? 0.0)")
DispatchQueue.main.async {
self.progressView.progress = Float(progressValue ?? 0.0)
}
case .didFailWithError:
if let error = (progressParamater as Dictionary)[kCTFontDescriptorMatchingError] as? NSError {
print(error.description)
} else {
print("ERROR MESSAGE IS NOT AVAILABLE")
}
default: print(String(reflecting: state))
}
return true
}
I tried this API on iOS 12 and Xcode 10, everything works fine and I can download a font with provided fontName.
But, when I use this one on Xcode 11 Version 11.0 beta 6 (11M392q) and macOS Catalina 10.15 Beta (19A526h), something went wrong and that method is no longer effective. And I got some output:
didBegin
__C.CTFontDescriptorMatchingState
didFinish
done
"Error Domain=com.apple.CoreText.CTFontManagerErrorDomain Code=303 \"0
font descriptors do not have information to specify a font file.\"
UserInfo={NSLocalizedDescription=0
font descriptors do not have information to specify a font file.
0 font descriptors do not have information to specify a font file
The state will only have didBegin and didFinish, not calling downloading.
WWDC19 seeesion 227 has announced that CTFontManagerRegisterFontDescriptors(fontDescriptors:scope:enabled:registrationHandler:) is available and something about font management. But the source code from the session pdf is not clearly and I've tried, got an other error:
done
[Error Domain=com.apple.CoreText.CTFontManagerErrorDomain Code=306 "The file is not in an allowed location. It must be either in the application’s bundle or an on-demand resource." UserInfo={NSLocalizedDescription=The file is not in an allowed location. It must be either in the application’s bundle or an on-demand resource., CTFontManagerErrorFontURLs=(
"http://iweslie.com/fonts/HanziPenSC-W3.ttf"
), NSLocalizedFailureReason=Font registration was unsuccessful.}]
done
Here is my code referencing from WWDC19 session 227:
let urlString = "http://iweslie.com/fonts/HanziPenSC-W3.ttf"
let url = URL(string: urlString)! as CFURL
let fontURLArray = [url] as CFArray
CTFontManagerRegisterFontURLs(fontURLArray, .persistent, true) { (errors, done) -> Bool in
if done {
print("done")
}
print(errors as Array)
return true
}
And got the error:
Error Domain=com.apple.CoreText.CTFontManagerErrorDomain Code=306
"The file is not in an allowed location. It must be either in the application’s bundle or an on-demand resource."

For other people looking for a solution for iOS 13
The easiest way to do it is probably using Apple Bundle and the 'new' CTFontManagerRegisterFontURLs api.
Just gather all your urls in a list by utilizing Bundle.main.url:
let fontUrl = Bundle.main.url(forResource: fileName, withExtension: "ttf")
Then just register your fonts:
CTFontManagerRegisterFontURLs([fontUrl] as CFArray, .persistent, true) { (errors, done) -> Bool in
if(done) {
print("Done")
}
print(errors as Array)
return true
}
If you're using another solution and having problems with the font name it might be xcode which is at fault. A tip I saw in another thread for figuring out what xcode is naming your fonts are:
for family in UIFont.familyNames.sorted() {
let names = UIFont.fontNames(forFamilyName: family)
print("Family: \(family) Font names: \(names)")
}
Also remember to enable Install Fonts capability under the Signing & Capabilities in your project settings.

I have facing the same problem. and I finaly found this.
https://juejin.im/post/5aebd428f265da0ba266d897
the problem is you are using the wrong font postscriptname. the correct font name is "STXingkaiSC-Light".
I have test this on xcode 12, iOS 13 simulator.
you can find the correct font postscript name using "Font BooK.app".

Related

Set `selected text` through the macOS Accessibility APIs in Catalyst apps

Not sure if it's a bug or what. I made an app that add Vim bindings all over macOS, but it fails in Catalyst apps. The text is readable mostly—although line numbers are wrong and line ranges too—but I can't set the selected text. It does nothing expect selecting the text, but not replacing it with the new one. It works well in all native apps that support the Accessibility.
Am I doing things wrong or is this a confirmed bug? (I've been reporting bugs in the Accessibility API for two years, with no change from Apple's part.)
The code is fairly simple. As an example:
func ....() {
var selectedTextRange = CFRange()
selectedTextRange.location = 2
selectedTextRange.length = 3
let newValue = AXValueCreate(.cfRange, &selectedTextRange)
guard AXUIElementSetAttributeValue(axFocusedElement, kAXSelectedTextRangeAttribute as CFString, newValue!) == .success else { return false }
guard AXUIElementSetAttributeValue(axFocusedElement, kAXSelectedTextAttribute as CFString, "hehe" as CFTypeRef) == .success else { return false }
return true
}
The func returns true. But in Catalyst apps there's a selection from location 2 to 5, but the text is not replaced by "hehe".
I've got an Xcode project also that shows the issue: https://github.com/godbout/AXBugsWithCatalystApps
Thanks.

How to remove sandbox without xcode

Earlier I asked a question regarding generateCGImagesAsynchronously. Thankfully it got answered and works great.
The issue is that it only works as a Cocoa app on xcode. I am now trying to move this logic to an executable Swift package but AVFoundation code, such as generateCGImagesAsynchronously, won't work. That is, no error is raised but those functions seem to be mocked. I presume this might have to do with my package being sandboxed? I was able to remove the sandbox from the Cocoa app I previously wrote the code for, but I can't figure out how to do that for this executable.
I am new to Swift and trying to understand it and it's kind of frustrating to think that what I want my code to do is dependent on the IDE I am using.
If anyone can point me in the direction of where to read in the docs, or some other sources, on how to make programs without using xcode, that would be great. Thanks!
Here is my code:
import Darwin
import Foundation
import AppKit
import AVFoundation
import Cocoa
#discardableResult func writeCGImage(
_ image: CGImage,
to destinationURL: URL
) -> Bool {
guard let destination = CGImageDestinationCreateWithURL(
destinationURL as CFURL,
kUTTypePNG,
1,
nil
) else { return false }
CGImageDestinationAddImage(destination, image, nil)
return CGImageDestinationFinalize(destination)
}
func imageGenCompletionHandler(
requestedTime: CMTime,
image: CGImage?,
actualTime: CMTime,
result: AVAssetImageGenerator.Result,
error: Error?
) {
guard let image = image else { return }
let path = saveToPath.appendingPathComponent(
"img\(actualTime).png"
)
writeCGImage(image, to: path)
}
let arguments: [String] = Array(CommandLine.arguments.dropFirst())
// For now, we assume the second arg, which is the
// path that the user wants us to save to, always exists.
let saveToPath = URL(fileURLWithPath: arguments[1], isDirectory: true)
let vidURL = URL(fileURLWithPath: arguments[0])
let vidAsset = AVAsset(url: vidURL)
let vidDuration = vidAsset.duration
let imageGen = AVAssetImageGenerator(asset: vidAsset)
var frameForTimes = [NSValue]()
let sampleCounts = 20
let totalTimeLength = Int(truncatingIfNeeded: vidDuration.value as Int64)
let steps = totalTimeLength / sampleCounts
for sampleCount in 0 ..< sampleCounts {
let cmTime = CMTimeMake(
value: Int64(sampleCount * steps),
timescale: Int32(vidDuration.timescale)
)
frameForTimes.append(NSValue(time: cmTime))
}
imageGen.generateCGImagesAsynchronously(
forTimes: frameForTimes,
completionHandler: imageGenCompletionHandler
)
As I said in a comment on your previous question, this has nothing to do with Xcode per se. Xcode just generates a lot of code and build commands for you.
macOS is a complex operating system and programs that want to use its more advanced features must follow certain patterns. One of this patterns is called the run loop. If you create a Cocoa app, you get most of these things for free.
Since you are trying to perform some asynchronous actions, you need a run loop. Appending this should work:
RunLoop.current.run()
Otherwise, your program will simply terminate when the main thread (your code) finishes. The run loop, however, causes the program to run a loop and wait for asynchronous events (this also includes UI interactions, for example) to occur.
Note that inserting this same line also fixes your issues from the other question.

How can I get IOBluetoothDevice's battery level, using Swift and AppKit (Xcode for MacOS)

I am using Xcode to develop a MacOS app, based on Cocoa & AppKit, written in Swift.
I am using IOBluetoothDevice objects throughout my app, and I want to be able to display the devices' battery levels, if accessible.
I expect that devices which battery levels are visible on the OS's Bluetooth settings (see image below), to be also accessible programmatically (e.g., AirPods, Magic Keyboard, etc.). However, I could not find this anywhere.
I have also thought about executing a terminal command and found this thread, but it did also not work.
Thanks
You can get the battery level of Bluetooth devices from the IORegistry with IOKit.
This is a simple example to get the battery level for the Magic Trackpad 2
import IOKit
var serialPortIterator = io_iterator_t()
var object : io_object_t
let port: mach_port_t
if #available(macOS 12.0, *) {
port = kIOMainPortDefault // New name in macOS 12 and higher
} else {
port = kIOMasterPortDefault // Old name in macOS 11 and lower
}
let matchingDict : CFDictionary = IOServiceMatching("AppleDeviceManagementHIDEventService")
let kernResult = IOServiceGetMatchingServices(port, matchingDict, &serialPortIterator)
if KERN_SUCCESS == kernResult {
repeat {
object = IOIteratorNext(serialPortIterator)
if object != 0, let percent = IORegistryEntryCreateCFProperty(object, "BatteryPercent" as CFString, kCFAllocatorDefault, 0).takeRetainedValue() as? Int {
print(percent)
break
}
} while object != 0
IOObjectRelease(object)
}
IOObjectRelease(serialPortIterator)
For other devices you have to replace AppleDeviceManagementHIDEventService and Trackpad2 with the appropriate values. You can display the entire IORegistry in Terminal.app with ioreg -l.

The "prefs" URL Scheme is not working in iOS 10 (Beta 1 & 2)

I can't get the "prefs" URL Scheme to work in iOS 10 (Beta 1).
It's set up correctly since the same App works fine on iOS 9.
Is this a bug or did it get renamed / removed?
Code:
let settingsUrl = NSURL(string: "prefs:root=SOMETHING")
if let url = settingsUrl {
UIApplication.sharedApplication().openURL(url)
}
Update: (Beta 2)
Still not working in Beta 2.
It seams to be an bug. For example if you want do invite someone using GameCenter in iOS 10 and you're not logged into iMessage, you'll get a popup asking you to log in. But the "Settings" button does absolutely nothing.
Just replace prefs to App-Prefs for iOS 10
Below code works for iOS 8,9,10
Swift 3.0 and Xcode >= 8.1
if #available(iOS 10.0, *)
{
UIApplication.shared.openURL(URL(string: "App-Prefs:root=SOMETHING")!)
}
else
{
UIApplication.shared.openURL(URL(string: "prefs:root=SOMETHING")!)
}
Swift 2.2
if #available(iOS 10.0, *)
{
UIApplication.sharedApplication().openURL(NSURL(string:"App-Prefs:root=SOMETHING")!)
}
else
{
UIApplication.sharedApplication().openURL(NSURL(string:"prefs:root=SOMETHING")!)
}
Works for me.
Happy Coding 😊
You can use UIApplicationOpenSettingsURLString to open your own app's settings (this has been available since iOS 8) but any other prefs: URL is now considered a private API and use will result in app rejection.
You can use Prefs:root=SOMETHING
iOS 10 updated URL Scheme for Settings, you need to upcase the "p".
Ref: https://github.com/cyanzhong/app-tutorials/blob/master/schemes.md
NOTICE: It only works on Widgets, not works in Apps. (iOS 10.0.2)
#Saumil Shah's solution works in App, is more useful.
For the record, for Location services App-Prefs:root=Privacy&path=LOCATION worked for me. When I tested on a device and not a simulator.
I won't list the things I tried that did not work, it's a long list.
Usage example that assumes either location services are disabled or permission is denied or not determined:
if !CLLocationManager.locationServicesEnabled() {
if let url = URL(string: "App-Prefs:root=Privacy&path=LOCATION") {
// If general location settings are disabled then open general location settings
UIApplication.shared.openURL(url)
}
} else {
if let url = URL(string: UIApplicationOpenSettingsURLString) {
// If general location settings are enabled then open location settings for the app
UIApplication.shared.openURL(url)
}
}
This is not available on iOS 11, we can just open Settings like:
if let url = URL(string:UIApplicationOpenSettingsURLString) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
If anyone is interested in a "gray area" API, you can use:
//url = "prefs:root=SOMETHING"
[[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:url withOptions:nil];
This will give you what you want. Hide it well, and it works in iOS 10.

Open another Mac app

In my app I would like to open another app that is installed on the User's Mac (such as iPhoto). I am not sure what I should be looking for in the documentation. What is this called and how should I do it?
Thank you
Swift 5 or later
import Cocoa
func openPhotos() -> Bool {
if let photosApp = FileManager.default.urls(
for: .applicationDirectory,
in: .systemDomainMask
).first?.appendingPathComponent("Photos.app") {
return NSWorkspace.shared.open(photosApp)
}
return false
}
Usage:
if openPhotos() {
print(true)
}
Or using launchApplication with the app name parameter in the method:
import Cocoa
func openApp(_ named: String) -> Bool {
NSWorkspace.shared.launchApplication(named)
}
Usage:
if openApp("Photos") {
print(true)
}
XCode 11 • MacOS Catalina 10.15 • Swift 5
NSWorkspace.shared.launchApplication is deprecated and starting from the MacOS 10.15 the new function NSWorkspace.shared.openApplication shall be used.
Example - open terminal application by its bundle id
guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.Terminal") else { return }
let path = "/bin"
let configuration = NSWorkspace.OpenConfiguration()
configuration.arguments = [path]
NSWorkspace.shared.openApplication(at: url,
configuration: configuration,
completionHandler: nil)
Example - open terminal application by its path
let url = NSURL(fileURLWithPath: "/System/Applications/Utilities/Terminal.app", isDirectory: true) as URL
let path = "/bin"
let configuration = NSWorkspace.OpenConfiguration()
configuration.arguments = [path]
NSWorkspace.shared.openApplication(at: url,
configuration: configuration,
completionHandler: nil)
You can use NSWorkspace class written by Swift/Cocoa.
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSWorkspace_Class/index.html#//apple_ref/occ/instm/NSWorkspace/launchApplication:
let task = NSTask.launchedTaskWithLaunchPath(<#path: String#>, arguments: <#[AnyObject]#>) will probably do what you want
I second the answer by vookimedlo - unfortunately, I cannot yet comment (silly reputation limit) so I post this as an extra answer.
This is just one caveat, which might not affect too many: while launchApplication() accepted a path to an executable (e.g. "MyApp.app/Contents/MacOS/MyApp"), this will result in an error (lacking privileges) with openApplication(::). You have to supply the path to the app bundle ("MyApp.app") instead.
Of particular interest when you try to make a helper ("launcher") to add as a login item. See the following and keep my comment in mind:
https://theswiftdev.com/2017/10/27/how-to-launch-a-macos-app-at-login/
(GREAT article by Tibor Bödecs)
BTW, as for vookimedlo's code: in my experience, you don't need to specify the OpenContext.arguments with [path], you can simply pass a default NSWorkspace.OpenContext()...
There are different ways to do that. The most efficient is to use fvork and execve - see man vfork and man execve.
Less efficient but more flexible is to use the system library call. What that actually does is runs a shell - like bash - then passes the string you provide, to bash. So you can set up pipelines, redirection and such.
Or you can send an Apple Event to the Finder: "Tell Finder Open iPhoto".
In the first two cases you want to launch the executable inside the bundle, that is, /Applications/iPhoto.app/Contents/MacOS/iPhoto.
Try the above from the command line, in the Terminal:
$ /Applications/iPhoto.app/Contents/MacOS/iPhoto
You'll see the iPhoto App launch.