Xcode8: Usage of image literals in frameworks - ios

I am currently updating a project to use image literals, to enjoy the benefits of non-optional images. The project is importing various frameworks, and the frameworks are containing images.
In the frameworks, we had to declare an extension on UIImage to override the initialiser, because it was looking for the image in the wrong bundle. We did something like:
extension UIImage {
convenience init?(framework_named imageName: String) {
let bundle = Bundle(for: ClassNameInFramework.self)
self.init(named: imageName, in: bundle, compatibleWith: nil)
}
}
I am wanting to use image literals in the framework too, but like before, the literals are looking for the wrong bundle for the image and the application crashes.
Does anyone know, how to specify the image literal to which bundle to look for the image?

I discovered a pretty simple workaround and was amazed to not find it anywhere on other posts. I wish it could be more elegant, but it's preferable than using the stringly-typed UIImage(named:in:compatibleWith:) initializer in my opinion.
We basically take advantage of the _ExpressibleByImageLiteral protocol, which is what Xcode uses to determine if a type is expressible by an image literal. It's part of the Swift standard library, but it's hidden from autocompletion, I guess because it's uncommon to want to initialize a custom type with an image literal. However, it's just what we want and behaves exactly like the other ExpressibleBy protocols.
struct WrappedBundleImage: _ExpressibleByImageLiteral {
let image: UIImage?
init(imageLiteralResourceName name: String) {
let bundle = Bundle(for: ClassInFramework.self)
image = UIImage(named: name, in: bundle, compatibleWith: nil)
}
}
Note that I use a wrapper instead of a UIImage subclass, which would seem like a better option. Sadly, classes like UIImage are not intended to be subclassed and you will find yourself getting a lot of headaches if you do.
And now its usage becomes:
let image = (🏞 as WrappedBundleImage).image
Not as concise as a normal image literal, but it's the best we've got for now. We just have to remember to do the as casting, otherwise our custom initializer will not get called.
You can also do something like this:
extension UIImage {
static func fromWrappedBundleImage(_ wrappedImage: WrappedBundleImage) -> UIImage? {
return wrappedImage.image
}
}
And we can now use it like this:
UIImage.fromWrappedBundleImage(🏞)
Hope this helps!

Related

How to keep a clean production version of your iOS App?

I develop an iOS App called Swordy Quest:
https://apps.apple.com/us/app/swordy-quest-an-rpg-adventure/id1446641513
It contains Game Center integration for Leaderboards, Achievements, Player vs Player (PVP) matchmaking and Clans.
I have a local test version that I use when developing (with a test bundleID). I also have a production version of my game that I use to play the game and progress as if I was a customer. However, in order to upgrade/implement the Game Center functionality above, I need to use my production bundleID for testing. This then overwrites my 'customer game' with all my test data (ruining my 'natural' progress).
So I am wondering, is it possible to have a 'clean' production version of an app and still have a separate test version that allows me to test Game Center functionality. Or is there some way to restore a previous app state in Xcode so I could save my production clean version before polluting it with test data? I know in Mac Apps you can change the custom working directory, but I don't think you can in iOS?
I have looked into backing up my Production version of the app before working on Game Center upgrades, but it looks like this is probably not possible? Has anyone come up with a clever way around this?
Please note I have stored both CoreData and UserDefaults in the app.
Custom working directory is something only command-line tool projects. ChangeCurrentDirectoryPath option is no longer available at this place as the screenshot below in XCode 4.6.1. Sounds crazy but you can try downgrade to Xcode 4 and make it happen.
Or you will need load files using Cocoa’s NSBundle class or Core Foundation’s CFBundle functions. So make duplicate target for your Swordy Quest test. It will not affect your clean copy.
Manage schemes:
Finally click the little gear button create a clean copy to avoid touch your production code.
After you set up your keys both product and test where
Build Settings > Packaging ( write to filter Packaging )
Implement as a code below to your logic function ( for example implement in it to a function which trigger a GameHomeVC from LoginPlayerVC )
var key: String?
#if TARGET_PROD || TARGET_STORE
key = #"prodKey";
#else
key = #"testKey";
as a precursor, i'm not familiar with Game Center, so there may be concerns there that i haven't accounted for. so, with that, my instinct in solving this starts out with launch arguments. there is a great article on how to do this here: https://www.swiftbysundell.com/articles/launch-arguments-in-swift/.
Now that you're able to start changing behavior based off of launch arguments from different schemes, you can start to look at how to segment your test / prod data.
As I'm not a CoreData expert, i can't say with 100% confidence that this is possible (or easy), but i would investigate how to setup separate persistent stores based off of a launch argument. using this article as a reference, it seems like you could roughly do something like the below after creating a -testGameCenter launch argument to a new TestGameCenter scheme to create an in-memory data store when testing Game Center
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "YourDataStore")
if CommandLine.arguments.contains("-testGameCenter") {
let description = NSPersistentStoreDescription()
description.url = URL(fileURLWithPath: "/dev/null")
container.persistentStoreDescriptions = [description]
}
container.loadPersistentStores(completionHandler: { _, error in
if let error = error as NSError? {
fatalError("Failed to load stores: \(error), \(error.userInfo)")
}
})
return container
}()
if you're able to solve the CoreData problem above, it's time to start looking at how to segment your UserDefaults data. this gross but easy solution that immediately comes to mind is prefixing your UserDefault keys with test when running from your test scheme. below is an example of how could structure a wrapper around UserDefaults to manage this
struct UserDefaultsWrapper {
let userDefaults: UserDefaults
let keyPrefix: String
init(userDefaults: UserDefaults, keyPrefix: String) {
self.userDefaults = userDefaults
self.keyPrefix = keyPrefix
}
func setValue(_ value: Any?, forKey key: String) {
self.userDefaults.setValue(value, forKey: prefixedKey(forKey: key))
}
func value(forKey key: String) -> Any? {
self.userDefaults.value(forKey: prefixedKey(forKey: key))
}
func prefixedKey(forKey key: String) -> String {
return "\(keyPrefix)\(key)}"
}
}
where you could make use of the wrapper like so
let userDefaultsPrefix = CommandLine.arguments.contains("-testGameCenter") ? "testGameCenter_" : ""
let userDefaultsWrapper = UserDefaultsWrapper(userDefaults: .standard, keyPrefix: userDefaultsPrefix)
to get something more elegant, you could look a little more into UserDefaults to see if you could apply a solution similar to the one for CoreData where there are two entirely separate stores. from a quick glance at this initializer, maybe you could do something as simple as this with your wrapper instead
struct UserDefaultsWrapper {
let userDefaults: UserDefaults
init(userDefaults: UserDefaults) {
self.userDefaults = userDefaults
}
func setValue(_ value: Any?, forKey key: String) {
self.userDefaults.setValue(value, forKey: key)
}
func value(forKey key: String) -> Any? {
self.userDefaults.value(forKey: key)
}
}
where you construct it like so
let userDefaultsSuiteName: String? = CommandLine.arguments.contains("-testGameCenter") ? myTestingGameCenterSuiteName : nil
let userDefaults = UserDefaults(suiteName: userDefaultsSuiteName)
let userDefaultsWrapper = UserDefaultsWrapper(userDefaults: userDefaults)
lastly, from a comment you made on another reply, it sounds like you are also concerned with fresh install scenarios. that said, the approaches i've outlined will not help (at least i don't think) with persisting data across deletes/installs. but, what i think you should think about is if it's necessary to test those delete/install concerns from your production bundle id. could you instead either manually test those concerns from your test bundle id and/or write unit tests around the components that involve those concerns? when you are approaching your testing strategy, it's important to make sure that you're testing the right things at the right layers; testing the wrong things at the wrong layers makes each testing layer much, much harder to execute
Targets is designed to do just that. You set pre-processor macros values to get the compiler to compile specific code based on target / macros values.
In your case, you change path to the customer game / test data file based on selected the target / macro combination.
You can also set a different bundleID for each target.
Once this is all setup you simply just switch between target and compile. The whole thing should just work seamlessly.
Make a backup of your project and then follow this tutorial which covers exactly how to do this:
https://www.appcoda.com/using-xcode-targets/
If the link above is broken in future, just search "Xcode target tutorials"

How can I get the file extension from the mimetype in iOS using Swift

As the title says, I've got a file downloaded from a server and I know the mimetype of it, but how can I work out what file extension should be used for it when writing to storage?
After hunting around on here, it was clear the answer involved Apple's Uniform Type Identifiers (UTI), but I couldn't find an example of exactly what I wanted to do. I'm sure this isn't perfect Swift code (I'm new to it as a language) because of how I'm handling the managed/unmanaged variables, but this does the conversion I need:
let unmanagedFileUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, type as CFString, nil)?.takeRetainedValue()
let fileExtension = UTTypeCopyPreferredTagWithClass((unmanagedFileUTI)!, kUTTagClassFilenameExtension)?.takeRetainedValue()

Swift: Load JSON from a url and store in cache or file system

I am currently in the process of writing an iOS APP that downloads information from an API in JSON format then displays it in the app.
One of the key features to this app is it being able to work offline as well as online, for this reason there should be a cached version as well as an online version.
After reading through the internet to my shock I have not found any examples what so ever of this approach.
The only thing I have found that's even come close to this is HanekeSwift but the documentation seems incomplete and there is no way to clear the cache and i'm not even sure if this is a memory based cache or a filesystem based cache.
Since there is lots of ways out there to do this, core data, file system frameworks etc.. I'm not sure which one would be the best to go for, theoretically to break down my thought process all I need to do is:
Check if the JSON file exists on the system
If not download it from the network and store it for later use (Preferably as a string format)
If file exists load it into a swiftyJSON object
I feel like core data would be overkill, I feel like the file system way is dated as most of the filesystem cocoa pods/libraries don't seem to be compatible with the current swift version (2.3)
Can anyone share some light on what the generic standard way of doing this is or what option would be the most suitable for my purpose of use and why.
Kindest regards
SwiftifyJSON makes objects that support archiving.
Try this
class HSCache: NSObject {
static var defaults: NSUserDefaults = NSUserDefaults()
class func cacheThis(key: String, object : AnyObject) {
defaults.setObject(NSKeyedArchiver.archivedDataWithRootObject(object), forKey: key)
defaults.synchronize()
}
class func getFromCache(key: String, type : AnyClass) -> AnyClass? {
if defaults.objectForKey(key) != nil {
return NSKeyedUnarchiver.unarchiveObjectWithData(defaults.objectForKey(key) as! NSData) as? AnyClass
}
return nil
}
class func deleteFromCache(key: String) {
defaults.removeObjectForKey(key)
defaults.synchronize()
}
}

iOS 8 share extension doesn't work with iBooks

I try to share selected/highlighted text in iBooks with my custom extension, but it doen't have anything inside self.extensionContext
<NSExtensionContext: 0x17541d90> - UUID: <__NSConcreteUUID 0x1765e860> D69F0393-C5F1-4DEB-9A97-B479C2BC0C95 - _isHost: NO
items:
(
)
so after i choose my extension in provided list it just pops up empty SLComposeServiceViewController
Mail, iMessages, Twitter etc. works as expected. Is there any additional magic i must do to handle this?
The interesting things about this are
When sharing from iBooks, self.extensionContext.inputItems is an empty array. It's not giving you anything to share.
Sharing from iBooks only works normally with extensions that were provided by Apple. On my iPhone other extensions are available from iBooks-- Evernote, Things, and others-- but none of them work normally. They all come up with empty content.
My take: Sharing from Apple's extensions relies on some undocumented secret behavior, and there's no extra magic you can apply that would get through the app store approval process.
If you set your activation rule to TRUEPREDICATE (which means that the extension should always show up) or something very lenient, your extension will show up in iBooks. But it doesn't look like you can get any content to share right now. I'd file a bug with Apple about it.
It does seem like that the issue has been resolved with iOS 9, the following code (in Swift) correctly returns the contents of the selection in iBooks:
for item: AnyObject in self.extensionContext!.inputItems {
let inputItem = item as! NSExtensionItem
for provider: AnyObject in inputItem.attachments! {
let itemProvider = provider as! NSItemProvider
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
itemProvider.loadItemForTypeIdentifier(kUTTypeText as String, options: nil, completionHandler: { (txt, error) in
NSOperationQueue.mainQueue().addOperationWithBlock {
//Doing stuff with txt
}
})
}
}
}

Swift complains "extraneous argument label"

Trying to start some Swift work. I am using
var imageData = UIImageJPEGRepresentation(image, compressionQuality:1.0)
but I get a warning "extraneous argument label 'compressionQuality' in call. I thought that in Swift the secondary parameters were either required or 'allowed' to be labelled, but this won't let me use it at all- fails building if I leave it. Since this is a system function, I can't use the # to require it. But I WOULD like to be able to name as many parameters as possible to make code more readable for myself, I like the ObjC method names, as verbose as they sometimes are.
Is there a way to set a compiler flag to allow extra argument labels?
You can't do like that, because that function doesn't declare any external parameter name. Internal parameter names can only be used within the function that declares them.
In Swift UIImageJPEGRepresentation method is declared as:
func UIImageJPEGRepresentation(_ image: UIImage!,
_ compressionQuality: CGFloat) -> NSData!
Check both parameters, both have internal name only so you can't use:
var imageData = UIImageJPEGRepresentation(image, compressionQuality:1.0)
Change that to:
var imageData = UIImageJPEGRepresentation(image,1.0)
Update Swift 4.2:
In swift 4.2, the above mentioned methods are no longer available. Instead of that you need to use:
// For JPEG
let imageData = image.jpegData(compressionQuality: 1.0)
// For PNG
let imageData = image.pngData()
Refer the API Document for more: Images & PDF
I had a similar problem, but Xcode was complaining about it in one of my funcions.
Turned out to be an extra } in my code, making the subsequent function declarations to be outside my class.
The error message was weird as hell, so I hope it hepls somebody else.

Resources