Cross platform code sharing in Swift - ios

I have an app created from Xcode 9 / New Project / Cross Platform Game App, targets: macOS and iOS.
I wrote the following simple function to load text from a Data Asset.
func dataAssetAsString(_ name: String) -> String? {
if let asset = NSDataAsset(name: NSDataAsset.Name(name)) {
return String(data: asset.data, encoding: .utf8)
}
return nil
}
I'm puzzled by the following things:
This function only works if I import either UIKit, or AppKit.
But somehow importing MetalKit instead of UIKIt or AppKit makes it work. This really puzzles me. This should have nothing to do with MetalKit.
Most answers I found for code sharing between iOS and macOS suggest some kind of conditional import at the beginning of the file / shimming.
I went through the slides of WWDC 2014 Session 233: Sharing code between iOS and OS X, and it explicitly says we should not do shimming in Swift.
On the other hand, I don't understand what we should do instead of shimming. The mentioned very complicated process of creating and compiling shared frameworks for a 5 line function cannot be the right direction.
In 2018 (Swift 4, Xcode 9, iOS 11, macOS 10.13), what would be the recommended way to add such a trivial cross platform code to an app?
What is the magic of MetalKit which makes it work without shimming? Does it shim internally?

Related

Is Process always unresolved in iOS app or Playground?

In a iOS app, I want to use a file which have an following function using Process:
public func system(_ body: String) throws {
if #available(macOS 10.0, *) {
let process = Process()
...
} else {
fatalError()
}
}
Then, I got a fallowing error even though I applied Availability Condition and I don't evoke this function:
Use of unresolved identifier 'Process'.
I tried a similar code in Playground, and I got the same error.
I learned we cannot use Process in iOS Apps with a regular way by this question: How to execute terminal commands in Swift 4? , and I have a solution that I separate these codes with files by each using platforms. But I want to use this single file if I can.
Please give me your another solution for my ideal.
if #available() does a runtime check for OS versions.
if #available(macOS 10.0, *)
evaluates to true if the code is running on macOS 10.0 or later,
or on iOS/tvOS/watchOS with an OS which is at least the minimum deployment target.
What you want is a conditional compilation, depending on the platform:
#if os(macOS)
let process = Process()
#else
// ...
#endif
Even though you already solved this problem, just for you to know, I want to tell you that actually, Process() (or CommandLine() in Swift 3.0 or newer) is available for iOS, but you'll need to use a custom Objective-C header file which creates the object Process()/CommandLine(), or rather NSTask(), and everything it needs.
Then, in order to use this code with Swift, you'll need to create a Bridging-Header, in which you'll need to import the NSTask.h file for it to be exposed to Swift and being able to use it in your Swift code.
Once done this, use NSTask() instead of Process():
let process = NSTask() /* or NSTask.init() */
Or just use the following function in your code whenever you want to run a task:
func task(launchPath: String, arguments: String...) -> NSString {
let task = NSTask.init()
task?.setLaunchPath(launchPath)
task?.arguments = arguments
// Create a Pipe and make the task
// put all the output there
let pipe = Pipe()
task?.standardOutput = pipe
// Launch the task
task?.launch()
task?.waitUntilExit()
// Get the data
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
return output!
}
As you can see, NSTask() would be the equivalent to Process() in this case.
And call it like this:
task(launchPath: "/usr/bin/echo", arguments: "Hello World")
This will also return the value, so you can even display it by doing:
print(task(launchPath: "/usr/bin/echo", arguments: "Hello, World!"))
Which will print:
~> Hello, World!
For this to work and not throwing an NSInternalInconsistencyException, you'll need to set the launchPath to the executable's full path instead to just the directory containing it.
You'll also need to set all the command arguments separated by commas.
Tested on both iPad Mini 2 (iOS 12.1 ~> Jailbroken) and iPhone Xr (iOS 12.2 ~> not jailbroken).
NOTE: Even though this works both on non-jailbroken and jailbroken devices, your App will be rejected on the AppStore, as #ClausJørgensen said:
You're using private APIs, so it'll be rejected on the App Store. Also, Xcode 11 has some new functionality that will trigger a build failure when using certain private APIs.
If your app is targeting jailbroken iOS devices and will be uploaded to a third-party store like Cydia, Zebra, Thunderbolt or Sileo, then this would work correctly.
Hope this helps you.

dismissGrantingAccessToURL: failing with "didPickDocumentURLs called with nil or 0 URLS"

I have an iOS application that is providing Document Picker feature working perfectly on iOS 10 but that on iOS 11 always calls the documentPickerWasCancelled: with this message in logs:
[UIDocumentLog] UIDocumentPickerViewController : didPickDocumentURLs
called with nil or 0 URLS
I'm correctly calling dismissGrantingAccessToURL: with a valid NSURL on the provider extension but it never calls the documentPicker:didPickDocumentsAtURLs: on the other side.
I think I'm missing something, can you give me an explanation for this bad behaviour?
I'm having the same problems. Unfortunately I think the explanation is a bug or backwards-incompatibility in iOS 11. According to the documents it should be enough with a Document Picker extension:
"The Document Picker View Controller extension can perform import and export operations on its own. If you want to support open and move operations, you must pair it with a File Provider extension."
https://developer.apple.com/documentation/uikit/uidocumentpickerextensionviewcontroller?language=objc
And indeed this worked fine in iOS 10 and earlier. iOS 11 was probably meant to be backwards compatible with the existing FileProvider-less DocumentPickers, but seems it's not. Or perhaps they forgot to update the documents.
Instead, one can implement the new updated File Provider that gives access to your files via the standard document browser UI:
https://developer.apple.com/documentation/fileprovider
This does work with an iOS 11 FileProvider backing the iOS10 picker. You probably want to create a new FileProvider using the new Xcode template, then use :
#available(iOSApplicationExtension 11.0, *)
on the FileProviderItem and FileProviderEnumerator classes, then :
if #available(iOSApplicationExtension 11.0, *) {
in the methods on your FileProviderExtension
I find that my iOS 10 picker does correctly call this method, but note the completionHandler?(nil) was required to make it work. By default, the template for iOS11 inserts a completion that reports a failure. This code works for me:
override func startProvidingItem(at url: URL, completionHandler: ((_ error: Error?) -> Void)?) {
completionHandler?(nil)
// completionHandler?(NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:]))
}
However, that isn't the end to this iOS10/11 incompatibility. If you make an iOS10/11 compatible file provider, it won't run on some iOS10 devices as far as I can see. I can run or debug mine on a 32-bit iOS device, but the FileProvider crashes on a 64-bit iOS 10 device with this error:
dyld: Library not loaded: /System/Library/Frameworks/FileProvider.framework/FileProvider
Referenced from: /private/var/containers/Bundle/Application/61BBD1A7-EA1E-4C10-A208-CA1DFA433C8D/test.app/PlugIns/testFileProvider.appex/testFileProvider
Reason: image not found

NSAttributedStringKey.attachment versus NSAttachmentAttributeName

I'm having problems with NSAttributedStringKey.attachment versus NSAttachmentAttributeName. Here's the relevant code:
var key: Any?
if #available(iOS 11, *) {
key = NSAttributedStringKey.attachment
}
else {
key = NSAttachmentAttributeName
}
One of two things are happening. In the actual place where I'm trying to use this code (a Cococapod of my own design, with a deployment target of iOS 8 and now building with Xcode 9), I get an error:
Type 'NSAttributedStringKey' (aka 'NSString') has no member 'attachment'
Or, if I just make a new example project and set the deployment target at iOS 8, I get:
'NSAttachmentAttributeName' has been renamed to 'NSAttributedStringKey.attachment'
This is not the behavior I'd expect with #available. Thoughts?
This String vs struct difference is between Swift 3 (uses Strings such as NSAttachmentAttributeName) and Swift 4 (uses struct static attributes such as NSAttributedStringKey.attachment), not between iOS <11 and iOS >=11. For instance, you can use NSAttributedStringKey.attachment and similar in any supporting version of iOS (e.g. .attachment is available since iOS 7) within a Swift 4 project. #available doesn't apply because it's a Swift language version difference rather than an OS version difference.
Ensure your pod is set to the correct Swift version and it should then work as expected. You can tell CocoaPods that by adding a .swift-version file at the top of your project:
$ echo 4.0 >.swift-version
This magical version file is mentioned in passing in a CocoaPods blog post from last year: http://blog.cocoapods.org/CocoaPods-1.1.0/

CGDataProviderCreateWithCFData function not recognized in Xcode IOS

I am looking at a camera tutorial for IOS and the CGDataProviderCreateWithCFData function was used. However Xcode doesn't seem to recognize it. Was this function removed from the latest Xcode Swift update? I am using Xcode version 8.1 and Swift 3. Also I have the following imports:
import UIKit
import AVFoundation
Have you tried searching "CGDataProviderCreateWithCFData" in the Apple's API reference page?
When I tried, I was guided to the Objective-C page, and its Swift link shows:
Initializer
init(data:)
Creates a data provider that reads from a CFData object.
In Swift 3, CGDataProviderCreateWithCFData is imported as an initializer of CGDataProvider and use it as:
let dataProvider = CGDataProvider(data: imageData as CFData)
(Assuming imageData as a Data.)
You can use it with import UIKit.

Swift App Extension: instance method count is unavailable

I just create my first app extension using XCode 7.1. One code file containing the code below is shared with both targets:
var str = "";
var l = str.count; //Compile error for extension target App: count is unavailable: There is no ...
The reason for this compile error seams to be that App extension compiles with swift 1.2 while the container target compiles with swift 2.0.
One solution would be importing the content App into the extension App doesn't appear to be a good solution from what i read about it. Sharing the code between targets can be difficult if both are not compiled using the same compiler.
I just run through all target settings and didn't find nothing that could be changed.
Can't find any post about this problem, witch is not so uncommon, so it is must likely i am interpreting something in a wrong way.
The only solution i can think of is using NSString instead of String but that is just an workaround for one class type. More problems of this kind will emerge in the future.
In Swift 2 it's
str.characters.count
Use str.characters.count to get String length in Swift 2

Resources