Localization and CocoaPods - ios

I have a project that uses CocoaPods. As a result, I have a workspace which contains two projects: mine and Pods.
Pods contains code which I'd like to localize, and I've created .strings files in Pod. However, NSLocalizedString fails to load these strings. I suspect this happens because the .strings file is not in the main bundle, but there's no Pod bundle, because it is compiled into a static library.
Is there a better way to localize code in a CocoaPods project than in my main project?

NSLocalizedString just invokes NSBundle's localizedStringForKey:value:table: so I wrote a NSBundle category to enable looking into several bundles (which in iOS are just folders):
NSString * const kLocalizedStringNotFound = #"kLocalizedStringNotFound";
+ (NSString *)localizedStringForKey:(NSString *)key
value:(NSString *)value
table:(NSString *)tableName
backupBundle:(NSBundle *)bundle
{
// First try main bundle
NSString * string = [[NSBundle mainBundle] localizedStringForKey:key
value:kLocalizedStringNotFound
table:tableName];
// Then try the backup bundle
if ([string isEqualToString:kLocalizedStringNotFound])
{
string = [bundle localizedStringForKey:key
value:kLocalizedStringNotFound
table:tableName];
}
// Still not found?
if ([string isEqualToString:kLocalizedStringNotFound])
{
NSLog(#"No localized string for '%#' in '%#'", key, tableName);
string = value.length > 0 ? value : key;
}
return string;
}
Then redefined NSLocalizedString macro in my prefix file:
#undef NSLocalizedString
#define NSLocalizedString(key, comment) \
[NSBundle localizedStringForKey:key value:nil table:#"MyStringsFile" backupBundle:AlternativeBundleInsideMain]
The same for other macros if needed (i.e. NSLocalizedStringWithDefaultValue)

#Rivera Swift 2.0 version:
static let kLocalizedStringNotFound = "kLocalizedStringNotFound"
static func localizedStringForKey(key:String,
value:String?,
table:String?,
bundle:NSBundle?) -> String {
// First try main bundle
var string:String = NSBundle.mainBundle().localizedStringForKey(key, value: kLocalizedStringNotFound, table: table)
// Then try the backup bundle
if string == kLocalizedStringNotFound {
string = bundle!.localizedStringForKey(key, value: kLocalizedStringNotFound, table: table)
}
// Still not found?
if string == kLocalizedStringNotFound {
print("No localized string for '\(key)' in '\(table)'")
if let value = value {
string = value.characters.count > 0 ? value : key
} else {
string = key
}
}
return string;
}

You should not put any file in the Pods project, because the pod command will recreate the project again and again.
So put the string files in your own project.
If you want to ship localized string files in your own Pod, you should include it in a bundle and make sure, the bundle will be installed in your Podspec file.
For example:
def s.post_install(target)
puts "\nGenerating YOURPOD resources bundle\n".yellow if config.verbose?
Dir.chdir File.join(config.project_pods_root, 'YOURPOD') do
command = "xcodebuild -project YOURPOD.xcodeproj -target YOURPODResources CONFIGURATION_BUILD_DIR=../Resources"
command << " 2>&1 > /dev/null" unless config.verbose?
unless system(command)
raise ::Pod::Informative, "Failed to generate YOURPOD resources bundle"
end
File.open(File.join(config.project_pods_root, target.target_definition.copy_resources_script_name), 'a') do |file|
file.puts "install_resource 'Resources/YOURPODResources.bundle'"
end
end
end

The best approach for localizing resources in a pod (public or private is indifferent) is to add the .strings localization files to the pod bundle.
The main app is very picky when comes to pick up the localization files from the pod frameworks, here the steps you must follow:
1 - Create the Localizable.strings and all the other *.strings files you need for your localization and put them in a folder, something like this:
Some constraints to respect:
The folders name must be XX.lproj, where XX is your EXACT language name, note: en != en-GB != en-US
All the lproj folder need to be at the same level
Every lproj folder need to have the same number of .strings files and with the same names
2 - Configure your .podspec file so the pod can pick up the localization correctly and copy them in the .framework.
Example:
s.resource = 'your_path/Localizations/**/*', '... possibly other resources'
after doing pod install the result in your Development Pods folder should be something like this:
One important thing to check is that created framework is that all the .lproj folder need to be on the root folder, otherwise are not picked up correctly by the main app.
3 - In your pod's code instead than the usual NSLocalizedString you must use NSLocalizedStringFromTableInBundle:
NSLocalizedStringFromTableInBundle(#"My string", nil, [NSBundle bundleForClass:[MCLoginContext class]], #"String context")
The entire process is very delicate and annoying, but that's should be all.

for swift you could use a Loc enum:
enum Loc : String{
case ok = "OK"
case cancel = "Cancel"
var localized: String {
let s = self.rawValue
let bundle = Bundle(for: <classname inside the bundle>.self)
let result = NSLocalizedString(s, tableName: nil, bundle: bundle, value: "", comment: "")
return result;
}
}
And use it so:
let s = Loc.ok.localized
print(s)

Related

Unity iOS development: How to add frameworks from pods via post build script?

We are using the Facebook Unity SDK 15.1.0 in Unity 2019.4.40f1. This SDK has some serious bugs. One of them is that it won't add the required frameworks to the Unity-iPhone target. The project will build, but immediately crash on startup.
The frameworks are there, but in a Pod:
I can add them manually in the General => Frameworks, Libraries, and Embedded Content section of the target:
Everything works fine then.
However, doing this after every build is quite tedious, so I would like to automate this task via a post build script. I am scratching my head about this, since I cannot find good samples online that actually work.
So my question is: How do you add a .xcframework buried in Facebook SDK's pods so it correctly shows up in the target?
Here is how i would do it. This may not be 100% correct but good enough to modify it to make it work for you. Basically you need to use PBXProject.AddFrameworkToProject to add frameworks.
#if UNITY_IOS
[PostProcessBuild(1)]
public static void ChangeXcodePlist(BuildTarget buildTarget, string pathToBuiltProject) {
if (buildTarget == BuildTarget.iOS) {
// get pbx project path
var projPath = PBXProject.GetPBXProjectPath(pathToBuiltProject);
if (File.Exists(projPath))
{
var proj = new PBXProject();
proj.ReadFromString(File.ReadAllText(projPath));
string mainTargetGuid = null, testTargetGuid = null, frameworkTargetGuid = null;
#if UNITY_2019_4_OR_NEWER // APIs are different for getting main unity targets changes based on versions
mainTargetGuid = proj.GetUnityMainTargetGuid();
frameworkTargetGuid = proj.GetUnityFrameworkTargetGuid();
#else
mainTargetGuid =
proj.TargetGuidByName(PBXProject.GetUnityTargetName());
testTargetGuid =
proj.TargetGuidByName(PBXProject.GetUnityTestTargetName());
frameworkTargetGuid = proj.TargetGuidByName("UnityFramework");
#endif
// add your frameworks here
if (!String.IsNullOrEmpty(mainTargetGuid))
{
Debug.Log("Adding targets to mainTargetGuid")
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKCoreKit.xcframework", false);
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKGamingServicesKit.xcframework", false);
}
// add to test target aswell if exists
if (!String.IsNullOrEmpty(testTargetGuid))
{
Debug.Log("Adding targets to testTargetGuid")
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKCoreKit.xcframework", false);
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKGamingServicesKit.xcframework", false);
}
if (!String.IsNullOrEmpty(frameworkTargetGuid))
{
Debug.Log("Adding targets to frameworkTargetGuid")
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKCoreKit.xcframework", false);
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKGamingServicesKit.xcframework", false);
}
proj.WriteToFile(projPath);
}
}
}
#endif
You can also use PBXProject.ContainsFramework before you include the frameworks.
The code in the previous answer is not working. Freimworks are not added like that.
Correct code for adding facebook's frameworks (working):
[PostProcessBuild(1000)]
public static void OnPostprocessBuild(BuildTarget buildTarget, string pathToBuiltProject) {
if (buildTarget != BuildTarget.iOS) return;
string projectPath = PBXProject.GetPBXProjectPath(pathToBuiltProject);
PBXProject project = new PBXProject();
project.ReadFromFile(projectPath);
string mainTargetGuid = project.GetUnityMainTargetGuid();
List<string> frameworks = new List<string> {
"FBSDKCoreKit",
"FBSDKGamingServicesKit"
};
foreach (string framework in frameworks) {
string frameworkName = framework + ".xcframework";
var src = Path.Combine("Pods", framework, "XCFrameworks", frameworkName);
var frameworkPath = project.AddFile(src, src);
project.AddFileToBuild(mainTargetGuid, frameworkPath);
project.AddFileToEmbedFrameworks(mainTargetGuid, frameworkPath);
}
// Write.
project.WriteToFile(projectPath);
}

failing to write output image in specific folder using opencv imwrite c/c++ function [duplicate]

I have recently started working in C++ and came across this situation when I have to create a directory while executing my code. The code is working fine when I have to create a single folder but it fails when I have to create another folder withing this newly created folder.
Suppose, I am in C: and want to store my file in C:/A/B/ .The following piece of code using mkdir() works fine if I have to store my file in C:/A/ but fails when I am adding another folder B.
Following is my code snippet:
#include <sys/stat.h>
#include <string>
using namespace std;
int main()
{
string stringpath = "C:/A/B/";
int status = mkdir(stringpath.c_str(),0777);
if(status!=0)
{
//.....
}
else
{
//....
}
}
Can someone help me in creating this directory where I can have any number of folders inside the parent directory? (P.S:I have added the header files sys/stat.h,iostream and string)
This is how you do it in C++17:
#include <filesystem>
namespace fs = std::filesystem;
fs::create_directories("./a/b/c")
mkdir() creates only the last component of the specified path. In your example, it will create only B. If any of the parent directories do not exist (ie, if A does not exist), the function fails with ENOENT. You need to split up the path and call mkdir() for every intermediate directory in the path, ignoring EEXIST errors as you go.
status = mkdir("C:/A/", 0777);
if ((status < 0) && (errno != EEXIST)) ...
status = mkdir("C:/A/B/", 0777);
if ((status < 0) && (errno != EEXIST)) ...
If you don't want to handle this manually, use a wrapper that handles it for you, such as Boost's create_directories() function:
bool create_directories(const path& p);
bool create_directories(const path& p, system::error_code& ec);
Effects: Establishes the postcondition by calling create_directory() for any element of p that does not exist.
Postcondition: is_directory(p)
Returns: true if a new directory was created, otherwise false.
Throws: As specified in Error reporting.
Complexity: O(n+1)where n is the number of elements of p that do not exist.
You can call the following:
string stringpath = "C:/A/B/";
int status = mkdir(stringpath.c_str(),0777);
If
C:/A/ directory exists. If its not exists, then do the following:
string stringpath = "C:/A/";
int status = mkdir(stringpath.c_str(),0777);
stringpath = "C:/A/B/";
int status = mkdir(stringpath.c_str(),0777);
In C++11 you can use the experimental functios:
#include <experimental/filesystem>
...
std::stringstream bufH;
bufH << dirName << fName;
if (!std::experimental::filesystem::exists(bufH.str()))
{
std::experimental::filesystem::create_directories(bufH.str());
}
Try the octal flag 7777 like this to have all the rights necessary to create this folder.
int status = mkdir(stringpath.c_str(), 7777);
Or do a chmod in the A folder like that :
chmod -r 7777 *

iOS - swift - Generating key pair for secp224k1 curve (ECDH)

I have been trying to generate public and private keys for the secp224k1 curve in iOS. We are using ECDH method to do api handshake between mobile and backend. In Java it is done using the below code.
public static KeyPair getECKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException {
ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp224k1");
KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDH", "SC");
kpg.initialize(ecSpec);
return kpg.generateKeyPair();
}
Is there a way to generate keys with the specific curve(secp224k1) type in swift? I tried using the apple provided EC algorithm to do the handshake by using the below code.
//Generates public and private key with EC algorithm
public static func getKey() -> [String: SecKey]? {
let attributes: [String: Any] =
[kSecAttrKeySizeInBits as String: 256,
kSecAttrKeyType as String: kSecAttrKeyTypeEC,
kSecPrivateKeyAttrs as String:
[kSecAttrIsPermanent as String: false]
]
var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
let err = error!.takeRetainedValue() as Error
print(err.localizedDescription)
return nil
}
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
print("Error occured while creating public key")
return nil
}
return ["publicKey": publicKey, "privateKey": privateKey]
}
When I send the public key generated via above method I get an error from server with a message:
"error":"java.security.InvalidKeyException: ECDH key agreement requires ECPublicKey for doPhase","exception":"InvalidAuthException"
I tried VirgilCrypto for swift which came close to solving the problem. But it doesn't have the specific curve type in the library which I need. It has support for secp256r1 only. Also, answers from the below posts I tried and didn't work out.
Elliptic Curve Diffie Hellman in ios/swift
Any suggestions or help would be great, Thanks.
The Koblitz 224-bit curve is not supported by iOS. One solution might be to use a different curve type or a third-party library with secp224k1 support.
From your comments it can be concluded that the secp224k1 curve type is a requirement.
A third-party library that might be used is Virgil Crypto, which is available through github https://github.com/VirgilSecurity/virgil-crypto. It is a C++ library. (Virgil Security is also offering a Swift wrapper lib called virgil-crypto-x, but that one no longer supports secp224k1 in the current version).
C++ libs can be used in Swift indirectly by creating a Objective-C++ wrapper with a defined interface.
Build VSCCrypto.framework
On the command line enter:
git clone https://github.com/VirgilSecurity/virgil-crypto
cd virgil-crypto
utils/build.sh --target=ios
This builds the framework for iOS.
Add VSCCrypto.framework to Xcode Project
select root node in project navigator
in Xcode create 'New Group'
name it 'Frameworks'
drag & drop in Finder the VSCCrypto.framework in the folder virgil-crypto/build/ios/lib to the 'Frameworks' group
in XCode on the right under 'Embedded Binaries' tap plus
select the VSCCrypto.framework
Objective-C++ Wrapper
create a ECDHCrypto Objective-C file
when asked for 'Would you like to configure an Objective-C bridging header' tap on 'Create Bridging Header'
in the project navigator change the file suffix from .m to .mm (to allow C++ code there)
add #import "ECDHCrypto.h" to the bridging header
ECDHCrypto.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#interface ECDHCrypto : NSObject
#property(nonatomic, strong) NSString *ownPrivateKey;
#property(nonatomic, strong) NSString *ownPublicKey;
- (void)generateKeyPair;
- (NSString *)shared:(NSString *)otherPublicKey;
#end
NS_ASSUME_NONNULL_END
ECDHCrypto.mm
#import "ECDHCrypto.h"
#import <VSCCrypto/VirgilCrypto.h>
using virgil::crypto::VirgilKeyPair;
using virgil::crypto::VirgilByteArray;
using virgil::crypto::VirgilCipherBase;
using virgil::crypto::str2bytes;
using virgil::crypto::bytes2str;
using virgil::crypto::bytes2hex;
#implementation ECDHCrypto
- (void)generateKeyPair {
VirgilKeyPair keyPair = VirgilKeyPair::generate(VirgilKeyPair::Type::EC_SECP224K1);
VirgilByteArray ownPublicKeyBates = keyPair.publicKey();
self.ownPublicKey = [NSString stringWithCString:bytes2str(ownPublicKeyBates).c_str()
encoding:[NSString defaultCStringEncoding]];
VirgilByteArray ownPrivateKeyBytes = keyPair.privateKey();
self.ownPrivateKey = [NSString stringWithCString:bytes2str(ownPrivateKeyBytes).c_str()
encoding:[NSString defaultCStringEncoding]];
}
- (NSString *)shared:(NSString *)otherPublicKey {
NSAssert(self.ownPrivateKey, #"private key must be set, e.g. use generateKeyPair");
std::string otherPKString([otherPublicKey cStringUsingEncoding:NSASCIIStringEncoding]);
VirgilByteArray pubKey = str2bytes(otherPKString);
std::string ownPrivateKeyString([self.ownPrivateKey cStringUsingEncoding:NSASCIIStringEncoding]);
VirgilByteArray ownPrivateKeyBytes = str2bytes(ownPrivateKeyString);
VirgilByteArray shared_ba = VirgilCipherBase::computeShared(pubKey, ownPrivateKeyBytes);
std::string hex = bytes2hex(shared_ba);
NSString *shared = [NSString stringWithCString:hex.c_str()
encoding:[NSString defaultCStringEncoding]];
return shared;
}
#end
Usage in Swift
let otherPK = """
-----BEGIN PUBLIC KEY-----
ME4wEAYHKoZIzj0CAQYFK4EEACADOgAEgeW/foqxCDOd1y6lnXONkRThS6xhjLHP
SEXs7jHSpoaPQH4vArcGmIb1cAZcepEh7WDQxCyfQXg=
-----END PUBLIC KEY-----
"""
let ecdhCrypto = ECDHCrypto()
ecdhCrypto.generateKeyPair();
print("ecdhCrypto.ownPublicKey: \n" + ecdhCrypto.ownPublicKey);
print("shared secret: " + ecdhCrypto.shared(otherPK));
Test With Java Counterpart
To test whether a key exchange is successful, one can perform the following test:
In Java, a secp224k1 key pair is generated and the public key is output to the console.
The public key is copied into the Swift code of an iOS application using Copy/Paste. The app then generates a key pair and writes its own public key to the console, as well as the calculated shared secret. The iOS public key is then inserted into the Java program as an input (displayed in green).
Finally, one can compare the shared secret of the iOS app and the Java program. Here it is the same, so the key exchange was successful.
In the upper area you see Xcode with the iOS source code and in the lower area the output of the Java program:

iOS Dropbox API : how to tell folders from file packages?

I'm using Dropbox iOS API v2. When trying to sort out folders from files, files that are packages (i.e. a preferences file, or xcodeproject file, .framework, etc.) show up as folder types. Is there a way to distinguish between folders and file packages?
DBFILESMetadata * metaData = ...;
if ([metaData isKindOfClass:[DBFILESFileMetadata class]]) {
// is a file
} else if ([metaData isKindOfClass:[DBFILESFolderMetadata class]]) {
// is a folder or file package
} else if ([metaData isKindOfClass:[DBFILESDeletedMetadata class]]) {
// has been deleted
}
If the 'bundle' (B) bit is set, then it should be treated as a bundle, rather than a single file. However, not all 'packages' are 'bundles'.
See: How do I flag a folder as being a package?
If the package is used by an app that is installed on the device, then its extension should be registered in the system by that application to identify it as being a package folder rather than a plain folder. However, I'm not sure if you can identify these as packages from an app that does not have that particular type registered.
See: https://developer.apple.com/library/content/documentation/CoreFoundation/Conceptual/CFBundles/DocumentPackages/DocumentPackages.html
If the package is not a bundle and has an extension that is not registered by an app installed on the current device, then I don't think there is any method that can be certain to be accurate, and your work around of has-a-filename-extension is probably the only thing you can do.
This is what I ended up with.
DBFILESMetadata * metaData = ...;
if ([metaData isKindOfClass:[DBFILESFileMetadata class]]) {
// is a file
} else if ([metaData isKindOfClass:[DBFILESFolderMetadata class]]) {
if ([[metaData.name pathExtension] isEqualToString:#""] == NO) {
// is a file package / bundle
} else {
// is a folder
}
} else if ([metaData isKindOfClass:[DBFILESDeletedMetadata class]]) {
// has been deleted
}

How to tell at runtime whether an iOS app is running through a TestFlight Beta install

Is it possible to detect at runtime that an application has been installed through TestFlight Beta (submitted through iTunes Connect) vs the App Store? You can submit a single app bundle and have it available through both. Is there an API that can detect which way it was installed? Or does the receipt contain information that allows this to be determined?
For an application installed through TestFlight Beta the receipt file is named StoreKit/sandboxReceipt vs the usual StoreKit/receipt. Using [NSBundle appStoreReceiptURL] you can look for sandboxReceipt at the end of the URL.
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSString *receiptURLString = [receiptURL path];
BOOL isRunningTestFlightBeta = ([receiptURLString rangeOfString:#"sandboxReceipt"].location != NSNotFound);
Note that sandboxReceipt is also the name of the receipt file when running builds locally and for builds run in the simulator.
Swift Version:
let isTestFlight = Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
Based on combinatorial's answer I created the following SWIFT helper class. With this class you can determine if it's a debug, testflight or appstore build.
enum AppConfiguration {
case Debug
case TestFlight
case AppStore
}
struct Config {
// This is private because the use of 'appConfiguration' is preferred.
private static let isTestFlight = Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
// This can be used to add debug statements.
static var isDebug: Bool {
#if DEBUG
return true
#else
return false
#endif
}
static var appConfiguration: AppConfiguration {
if isDebug {
return .Debug
} else if isTestFlight {
return .TestFlight
} else {
return .AppStore
}
}
}
We use these methods in our project to supply different tracking id's or connection string per environment:
func getURL(path: String) -> String {
switch (Config.appConfiguration) {
case .Debug:
return host + "://" + debugBaseUrl + path
default:
return host + "://" + baseUrl + path
}
}
OR:
static var trackingKey: String {
switch (Config.appConfiguration) {
case .Debug:
return debugKey
case .TestFlight:
return testflightKey
default:
return appstoreKey
}
}
UPDATE 05-02-2016:
A prerequisite to use a preprocessor macro like #if DEBUG is to set some Swift Compiler Custom Flags. More information in this answer: https://stackoverflow.com/a/24112024/639227
Modern Swift version, which accounts for Simulators (based on accepted answer):
private func isSimulatorOrTestFlight() -> Bool {
guard let path = Bundle.main.appStoreReceiptURL?.path else {
return false
}
return path.contains("CoreSimulator") || path.contains("sandboxReceipt")
}
I use extension Bundle+isProduction on Swift 5.2:
import Foundation
extension Bundle {
var isProduction: Bool {
#if DEBUG
return false
#else
guard let path = self.appStoreReceiptURL?.path else {
return true
}
return !path.contains("sandboxReceipt")
#endif
}
}
Then:
if Bundle.main.isProduction {
// do something
}
There is one way that I use it for my projects. Here are the steps.
In Xcode, go to the the project settings (project, not target) and add "beta" configuration to the list:
Then you need to create new scheme that will run project in "beta" configuration. To create scheme go here:
Name this scheme whatever you want. The you should edit settings for this scheme. To do this, tap here:
Select Archive tab where you can select Build configuration
Then you need to add a key Config with value $(CONFIGURATION) the projects info property list like this:
Then its just the matter what you need in code to do something specific to beta build:
let config = Bundle.main.object(forInfoDictionaryKey: "Config") as! String
if config == "Debug" {
// app running in debug configuration
}
else if config == "Release" {
// app running in release configuration
}
else if config == "Beta" {
// app running in beta configuration
}

Resources