Changing language at runtime using SwiftGen - ios

My app is supposed to support language change at runtime. I'm using SwiftGen 5.0. ViewControllers subscribe to language change notification and I've checked that the localisation function fires correctly. My overriden tr function looks like this:
fileprivate static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
guard let bundle = LanguageManager.shared.bundle else {
fatalError("Cannot find bundle!")
}
let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "")
let locale = Locale(identifier: LanguageManager.shared.currentLanguageKey!)
return String(format: format, locale: locale, arguments: args)
}
The bundle is set like so:
if let path = Bundle.main.path(forResource: currentLanguageKey, ofType: "lproj") {
bundle = Bundle(path: path)
}
However, the tr function returns mostly previous language strings. Only one out of all labels currently in memory refreshes. Setting a breakpoint inside the function and printing bundle returns
NSBundle </var/containers/Bundle/Application/ED5A6C7D-1807-4319-8817-45E693BC45E2/MyApp.app/en_US.lproj> (not yet loaded)
which is the correct new language. After app restarts the language is set correctly. Am I doing something wrong?

Okay, I found the problem. The stencil was generating static variables:
static let label = L10n.tr("Localizable", "registration_verify.pin_code.label")
Changing stencil to generate computed properties fixed the behaviour:
static var label: String {
return L10n.tr("Localizable", "registration_verify.pin_code.label")
}

Now you can config lookupFunction params in swiftgen.yml file
strings:
inputs:
- en.lproj
outputs:
- templateName: structured-swift5
params:
lookupFunction: Localize_Swift_bridge(forKey:table:fallbackValue:)
output: L10n-Constants.swift
in your project you just need implement lookupFunction,
your can use use Localize_Swift library
import Localize_Swift;
func Localize_Swift_bridge(forKey:String,table:String,fallbackValue:String)->String {
return forKey.localized(using: table);
}
generated code may like this:
internal enum Localizable {
internal static var baseConfig: String { return
L10n.tr("Localizable", "base config", fallback: #"Base Config"#) }}
extension L10n {
private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String {
let format = Localize_Swift_bridge(forKey:table:fallbackValue:)(key, table, value)
return String(format: format, locale: Locale.current, arguments: args)
}
}
https://github.com/SwiftGen/SwiftGen/blob/stable/Documentation/templates/strings/structured-swift5.md
https://github.com/marmelroy/Localize-Swift

Related

iOS user with language of "en", no region code, translation lookup fails

We have a bug reported where a user has a device with an en language and nil region code, and thus all NSLocalizedString lookups in are failing, meaning our string key is what is rendered onscreen. Thus, if we had this in our en.lproj/Localizable.strings file:
"some_key" = "Some string.";
It would render some_key instead of Some string. in our UI.
First question: how do I replicate this scenario locally? This question on Stack seems to almost describe the issue, but does not describe how one enters this state.
Second question: why would iOS not fall back to English in the event the region code was nil?
Second question: why would iOS not fall back to English in the event
the region code was nil?
The cause can be "There is no base development language that is enabled". Or it is iOS logic.
Here is my solution for Localization. I just want to share with you an alternative solution if it can help you to solve the issue.
public enum AppResourceLang: String {
case en
case vi
}
public class AppResManager {
public static let shared = AppResManager()
public var lang = "en"
var textBundle: Bundle!
public var mainBundle: Bundle!
private init() {
let mainBundleId = Bundle.main.bundleIdentifier!
// we use mainBundle's bundleIdentifier because normally your running target will contains "lproj" in Copy Bundle Resource
// If your text/image is located on different project/target or framework, you need to enter that target's bundleId.
mainBundle = Bundle(identifier: mainBundle)!
let path = mainBundle.path(forResource: lang, ofType: "lproj")
textBundle = Bundle(path: path!)
}
public func changeLang(code: String) {
if let path = mainBundle.path(forResource: code, ofType: "lproj") {
lang = code; textBundle = Bundle(path: path)
} else {
// fallback to English
lang = "en"
let path = mainBundle.path(forResource: lang, ofType: "lproj")
textBundle = Bundle(path: path!)
}
}
}
Then we can use above textBundle like below:
public extension String {
var localText: String {
guard let bundle = AppResManager.shared.textBundle else { return self }
return NSLocalizedString(self, bundle: bundle, comment: "")
}
var lTextUpcased: String {
guard let bundle = AppResManager.shared.textBundle else { return self.uppercased() }
return NSLocalizedString(self, bundle: bundle, comment: "").uppercased()
}
}
Here is my AppResource (like a framework). We can see I have Localizable.strings and it is localized for EN, VI.
Here is the real file/folder structure, you can see if you check on English in Localization for *.string, *.storyboard,... file. It will be cloned and saved in this folder (ex: en.lproj). You can base on this to point to the corresponding resource file.
Then, how to use my above codes. It is just a way to allow you to completely control the Localization.
public enum AppLanguage: String {
case en
case vi
}
// MARK: - Device info
public static func getDeviceLang() -> AppLanguage {
guard let languageCode = Locale.current.languageCode else { return .en }
let appLang = AppLanguage(rawValue: languageCode.lowercased()) ?? .en
return appLang
}
// Then this will switch the language of your resource based on device language.
AppResManager.shared.changeLang(code: YourGlobalClass.getDeviceLang().rawValue)
// Then use the above string extension to load the corresponding text
// Anywhere in your project.
let text = "your_key".localText

File Provider iOS11 startProvidingItem not invoked

I'm implementing a File Provider Extension for iOS 11.
Dispite watching the conference at https://developer.apple.com/videos/play/wwdc2017/243/ and navigating through Apple's Documentation, I still can't seem to understand how to implement some of the methods for NSFileProviderExtension and NSFileProviderEnumerator objects.
I successfully implemented NSFileProviderItem, having all of them listed in the Navite iOS 11 Files App. However, I can't trigger any document based app to open upon selecting a file.
I overrided all the methods for the NSFileProviderExtension. Some are still empty, but I placed a breakpoint to check whenever they are called.
The NSFileProviderExtension looks something like this:
class FileProviderExtension: NSFileProviderExtension {
var db : [FileProviderItem] = [] //Used "as" a database
...
override func item(for identifier: NSFileProviderItemIdentifier) throws -> NSFileProviderItem {
for i in db {
if i.itemIdentifier.rawValue == identifier.rawValue {
return i
}
}
throw NSError(domain: NSCocoaErrorDomain, code: NSNotFound, userInfo:[:])
}
override func urlForItem(withPersistentIdentifier identifier: NSFileProviderItemIdentifier) -> URL? {
guard let item = try? item(for: identifier) else {
return nil
}
// in this implementation, all paths are structured as <base storage directory>/<item identifier>/<item file name>
let manager = NSFileProviderManager.default
let perItemDirectory = manager.documentStorageURL.appendingPathComponent(identifier.rawValue, isDirectory: true)
return perItemDirectory.appendingPathComponent(item.filename, isDirectory:false)
}
// MARK: - Enumeration
func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier) throws -> NSFileProviderEnumerator {
var maybeEnumerator: NSFileProviderEnumerator? = nil
if (containerItemIdentifier == NSFileProviderItemIdentifier.rootContainer) {
maybeEnumerator = FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier)
self.db = CustomData.getData(pid: containerItemIdentifier)
} else if (containerItemIdentifier == NSFileProviderItemIdentifier.workingSet) {
// TODO: instantiate an enumerator for the working set
} else {
}
guard let enumerator = maybeEnumerator else {
throw NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])
}
return enumerator
}
My enumerateItems looks something like so:
class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
override func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) {
let itens = CustomData.getData(pid: enumeratedItemIdentifier)
observer.didEnumerate(itens)
observer.finishEnumerating(upTo: nil)
}
The static function CustomData.getData is used for testing. It returns an array of NSFileProviderItem with the desired properties. It should be replaced with a database, as explained in the conference.
class CustomData {
static func getData(pid : NSFileProviderItemIdentifier) -> [FileProviderItem] {
return [
FileProviderItem(uid: "0", pid: pid, name: "garden", remoteUrl : "https://img2.10bestmedia.com/Images/Photos/338373/GettyImages-516844708_54_990x660.jpg"),
FileProviderItem(uid: "1", pid: pid, name: "car", remoteUrl : "https://static.pexels.com/photos/170811/pexels-photo-170811.jpeg"),
FileProviderItem(uid: "2", pid: pid, name: "cat", remoteUrl : "http://www.petmd.com/sites/default/files/what-does-it-mean-when-cat-wags-tail.jpg"),
FileProviderItem(uid: "3", pid: pid, name: "computer", remoteUrl : "http://mrslamarche.com/wp-content/uploads/2016/08/dell-xps-laptop-620.jpg")
]
}
}
The problem is, when the user presses a document, urlForItem is successfully called but nothing happens upon returning the item url.
What am I doing wrong?
I can't find any examples on the internet.
Cheers
-nls
Turns out, I did not correctly implement providePlaceholder(at url:).
It is now solved.
Cheers
-nls
EDIT:
In order to list the items in your file provider, the method enumerator(for:) should be implemented.
This method will receive a containerItemIdentifier, as if telling you "what folder the user is trying to access". It returns a NSFileProviderEnumerator object, that should also be implemented by you.
Here is an example of how a simple enumerator(for:) method should look like:
class FileProviderExtension: NSFileProviderExtension {
override func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier) throws -> NSFileProviderEnumerator {
var enumerator: NSFileProviderEnumerator? = nil
if (containerItemIdentifier == NSFileProviderItemIdentifier.rootContainer) {
enumerator = FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier)
}
else {
enumerator = FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier)
}
if enumerator == nill {
throw NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])
}
return enumerator
}
(...)
}
Again, as I said, the FileProviderEnumerator should be implemented by you. The important method here is the enumerateItems(for observer:, startingAt page:)
Here it is how it should look:
class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) {
if (enumeratedItemIdentifier == NSFileProviderItemIdentifier.rootContainer) {
//Creating an example of a folder item
let folderItem = FileProviderFolder()
folderItem.parentItemIdentifier = enumeratedItemIdentifier //<-- Very important
folderItem.typeIdentifier = "public.folder"
folderItem.name = "ExampleFolder"
folderItem.id = "ExampleFolderID"
//Creating an example of a file item
let fileItem = FileProviderFile()
fileItem.parentItemIdentifier = enumeratedItemIdentifier //<-- Very important
fileItem.typeIdentifier = "public.plain-text"
fileItem.name = "ExampleFile.txt"
fileItem.id = "ExampleFileID"
self.itemList.append(contentsOf: [folderItem, fileItem])
observer.didEnumerate(self.itemList)
observer.finishEnumerating(upTo: nil)
}
else {
//1 > Find directory name using "enumeratedItemIdentifier" property
//2 > Fetch data from the desired directory
//3 > Create File or Folder Items
//4 > Send items back using didEnumerate and finishEnumerating
}
}
(...)
}
Remember that we were creating these FileProviderEnumerators, giving them the containerItemIdentifier. This property is used to determine what folder the user is trying to access.
Very important note: Each item, File or Folder, should have its parentItemIdentifier property defined. If this property is not set, the items won't appear when the user tries to open the parent folder.
Also, as the name suggests, typeIdentifier will hold the Uniform Type Identifier (UTI) for the item.
Finally, the last object we should implement is the NSFileProviderItem. Both File and Folder items are very similar, and should differ in their typeIdentifier property.
Here is a very simple example of a folder:
class FileProviderFolder: NSObject, NSFileProviderItem {
public var id: String?
public var name: String?
var parentItemIdentifier: NSFileProviderItemIdentifier
var typeIdentifier: String
init() {
}
var itemIdentifier: NSFileProviderItemIdentifier {
return NSFileProviderItemIdentifier(self.id!)
}
var filename: String {
return self.name!
}
}
The itemIdentifier is very important because, as stated before, this property will provide the directory name for the folder item when trying to enumerate its contents (refer to enumerator(for:) method).
EDIT2
If the user selects a file, the method startProvidingItem(at url:) should be called.
This method should perform 3 tasks:
1 - Find the selected item ID (usualy using the provided url, but you can use a database too)
2 - Download the file to the local device, making it available at the specified url. Alamofire does this;
3 - Call completionHandler;
Here is a simple example of this method:
class FileProviderExtension: NSFileProviderExtension {
override func urlForItem(withPersistentIdentifier identifier: NSFileProviderItemIdentifier) -> URL? {
// resolve the given identifier to a file on disk
guard let item = try? item(for: identifier) else {
return nil
}
// in this implementation, all paths are structured as <base storage directory>/<item identifier>/<item file name>
let perItemDirectory = NSFileProviderManager.default.documentStorageURL.appendingPathComponent(identifier.rawValue, isDirectory: true)
let allDir = perItemDirectory.appendingPathComponent(item.filename, isDirectory:false)
return allDir
}
override func persistentIdentifierForItem(at url: URL) -> NSFileProviderItemIdentifier? {
// exploit that the path structure has been defined as <base storage directory>/<item identifier>/<item file name>, at urlForItem
let pathComponents = url.pathComponents
assert(pathComponents.count > 2)
return NSFileProviderItemIdentifier(pathComponents[pathComponents.count - 2])
}
override func startProvidingItem(at url: URL, completionHandler: #escaping (Error?) -> Void) {
guard
let itemID = persistentIdentifierForItem(at: url),
let item = try? self.item(for: itemID) as! FileProviderFile else {
return
}
DownloadfileAsync(
file: item,
toLocalDirectory: url,
success: { (response) in
// Do necessary processing on the FileProviderFile object
// Example: setting isOffline flag to True
completionHandler(nil)
},
fail: { (response) in
completionHandler(NSFileProviderError(.serverUnreachable))
}
)
}
(...)
}
Note that, to get the ID from the URL, I'm using the recomended method: the URL it self contains the item ID.
This URL is definedin the urlForItem method.
Hope this helps.
-nls
I thought I'd provide a followup answer, the primary answer is great as a first step. In my case startProvidingItem was not called because I was not storing the files in exactly the directory the system was looking for, that is to say:
<Your container path>/File Provider Storage/<itemIdentifier>/My Awesome Image.png
That is on the slide from WWDC17 on the FileProvider extension, but I did not think it must follow that format so exactly.
I had a directory not named "File Provider Storage" into which I was putting files directly, and startProvidingItem was never called. It was only when I made a directory for the uniqueFileID into which the file was placed, AND renamed my overall storage directory to "File Provider Storage" that startProvidingItem was called.
Also note that with iOS11, you'll need to provide a providePlaceholder call as well to the FileProviderExtension, use EXACTLY the code that is in the docs for that and do not deviate unless you are sure of what you are doing.

Should I set Build Configuration in Xcode

When distribution app on app store should I also set Build Configuration in Xcode from edit scheme like this
I would recommend to set up two schemas.
First one: Development -> setup with Debug build configuration.
You can use this while you are developing your app. This will give you logging, easy debugging, etc..
Second one: Distribution -> setup with Release build configuration.
Logging will not happen on this schema, also debugging will be unavailable, because the build is not optimizaed for that.
When you are preparing your submittal to the App Store, archive the Distribution schema using the Release build configuration.
You can find some more detailed description here about the difference between Debug and Release build configurations.
This will cover your question in depth. I have used build configuration environment so that if you make a build in release Configuration. your automatic values will be se according to release version. you can also download the sample code from the link below to see actually what happens when you change the scheme.
First step.
Add the variable "Configuration" in your info.plist and add value "$(CONFIGURATION)" there.
Make a Config.swift file and copy paste the below code there.
`
import Foundation
private let configManagerSharedInstance = ConfigManager()
class Config {
class var sharedInstance: ConfigManager {
return configManagerSharedInstance
}
}
// You can put as much Environment as you need but you make sure you also put these environment in the config.plist file.
enum Environment: String {
case Debug = "Debug"
case Production = "Release"
}
class ConfigManager: NSObject {
private var environment: Environment?
var config: NSDictionary?
override init() {
super.init()
// Retrieve the current evironment from the main bundle
if let currentEnvironment = Bundle.main.infoDictionary?["Configuration"] as? String {
// Store the current environment for later use
environment = Environment(rawValue: currentEnvironment)
if let projectConfigPath = Bundle.main.path(forResource: "Config", ofType: "plist") {
if let projectConfigContents = NSDictionary(contentsOfFile: projectConfigPath) as? Dictionary<String, AnyObject> {
config = projectConfigContents[currentEnvironment] as? Dictionary<String, AnyObject> as NSDictionary?
}
} else {
print("config file not found")
}
}
}
func getCurrentEnvironment() -> Environment? {
return self.environment
}
func configForKey(key: String) -> String {
return config?[key] as! String
}
//It will use to get sub dictionary and their values.
func configForCategory(category: String, andKey: String) -> String {
let configuration = config?.value(forKeyPath: category) as! NSDictionary
return configuration.value(forKeyPath: andKey) as! String
}
}
`
I have also made a file Constants.swift in which i have set the varibles using the above code.
`
//
// Constants.swift
// BuildConfiguration
//
// Created by Ourangzaib khan on 4/6/17.
// Copyright © 2017 Ourangzaib khan. All rights reserved.
//
let kBASE_URL : String = {
print(Config.sharedInstance.configForKey(key: "kBASE_URL"));
return Config.sharedInstance.configForKey(key: "kBASE_URL")
}()
let STRIPEKEY : String = {
return Config.sharedInstance.configForCategory(category: "Stripe", andKey: "Publishable Key")
}()
let PUBNUBKEYSUBSCRIBE : String = {
return Config.sharedInstance.configForCategory(category: "PubNub", andKey: "Publish Key")
}()
let PUBNUBKEYPUBLISH : String = {
return Config.sharedInstance.configForCategory(category: "PubNub", andKey: "Subscribe Key")
}()
let WOWZAKEY : String = {
return Config.sharedInstance.configForKey(key: "Wowza");
}()
`
Now you just have to select the environment using edit sceme go into the edit scheme and chose Build Configuration Now when you will run the project you will see this output WRT build configuration in below images.
https://github.com/ourangzeb/Build-Configuration-for-IOS

How do I iterate over Stickers.xcassets or get images from xcassets?

I'm working on a custom sticker app extension and I want to loop through my Sticker.xcassets folder instead of setting up a fixed for loop.
For example:
func getStickers() {
for index in 1...16 {
addSticker(location: "\(index)", description: "\(index)")
}
}
You are probably aware that you do not access the files within your project folder using string literal paths. Instead you use the functions provided by NSBundle. Happily, these include functions that will do what you are looking for. Looking at the developer reference for NSBundle at:
https://developer.apple.com/library/content/documentation/CoreFoundation/Conceptual/CFBundles/AccessingaBundlesContents/AccessingaBundlesContents.html
The listing I think you want from this document is 3-11, which describes how to pull all the assets of a given type from the bundle:
CFArrayRef birdURLs;
// Find all of the JPEG images in a given directory.
birdURLs = CFBundleCopyResourceURLsOfType( mainBundle,
CFSTR("jpg"),
CFSTR("BirdImages") );
Hope that helps!
You can't access programmatically a .xcassets file at runtime. I'm also working on a similar task, the solution I came up is very simple. Images are named using following convention: imageNameAndIdentifier (for example: heart0, heart1, heart2, etc). Using a loop where the full name of the image is constructed and an UIImage is created, I get all the needed images.
let stickerName = "heart"
var images = [UIImage]()
var lastImageIndex = 0
lastImageIndex = lastStickerImageIndex(name: stickerName)
for index in 0...lastImageIndex {
images.append(UIImage.init(named: stickerName + "\(index)"))
}
Pay attention to lastImageIndex variable. It's value is calculated based on number of files which contains in filename a given string and substracting one unit, because last index is = (length_of_array - 1).
private func lastStickerImageIndex(name: String) -> Int {
return (FileManager.default.numberOfFilesWhichNameContains(string: name) - 1)
}
I've created an extension which returns number of files based on a string which should be contained in the desired file name.
extension FileManager {
func numberOfFilesWhichNameContains(string: String) -> Int {
do {
let bundleFiles = try self.contentsOfDirectory(atPath: Bundle.main.bundlePath)
let filteredBundleFiles = bundleFiles.filter({
$0.contains(string)
})
return filteredBundleFiles.count
} catch let error {
print(error)
}
return 0
}
}

Import Swift class created dynamically

I am experimenting with a piece of code and I need some assistance. I am creating a file that is a swift file that contains a class and a variable. I am able to successfully create and read the file. Now, is it possible for my to use this swift file and access its variable (v, in this case)?
func writeF() {
let file = "Sample.swift"
let text = "import Foundation \n" +
"public class Sample { \n" +
" let v: Int = 0 \n" +
"}"
if let dir = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.AllDomainsMask, true).first {
let path = NSURL(fileURLWithPath: dir).URLByAppendingPathComponent(file)
//writing
do {
try text.writeToURL(path, atomically: false, encoding: NSUTF8StringEncoding)
}
catch {print("error writing file")}
//reading
do {
let text2 = try NSString(contentsOfURL: path, encoding: NSUTF8StringEncoding)
print(text2)
}
catch {
print("error reading file")
}
}
}
You can't add code on runtime. When your code is compiled, there are no *.swift files left, that can be read by humans. After compiling, your code is basically 0 and 1 only.
As FelixSFD said in their answer, you cannot dynamically build a Swift file and compile it at runtime on the device, at least not in the normal Sandboxed environment. If you have a Jailbroken device, you can build and install the Swift open source runtime and compile programs on-the-fly that way.
As an alternative, you could look into the JavaScriptCore framework to build and run dynamic JavaScript code, and bridge it into your app.
Here's a quick example of passing an object to JavaScript and returning the same object with a class mapping:
import JavaScriptCore
let js = "function test(input) { return input }"
class TestClass: NSObject {
var name: String
init(name: String) {
self.name = name
}
}
let context = JSContext()
context.evaluateScript(js)
let testFunc = context.objectForKeyedSubscript("test")
let result = testFunc.callWithArguments([TestClass(name: "test")])
result.toDictionary()
let testObj = result.toObjectOfClass(TestClass.self) as? TestClass
testObj?.name // "test"

Resources