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:
Related
I am experimenting on webrtc, My goal is to store remote audio stream as a local file without using the media server, I am aware of aecdump but I don't find proper method or blog to unzip in iOS.
I am using googleWebrtc native framework.
Thanks.
Yes First to get the IOS directory path you must have to create your own objective c file.
my_ios_wav.mm
#import <Foundation/Foundation.h>
#include <string.h>
#import "sdk/objc/helpers/NSString+StdString.h"
#include "rtc_base/checks.h"
namespace webrtc{
std::string myIOSPath() {
#autoreleasepool {
NSString* tempDir = NSTemporaryDirectory();
if (tempDir == nil)
tempDir = #"/tmp";
return [NSString stdStringForString:tempDir];
}
}
}
After that create a static function in the class you have data that you want to record.
static webrtc::WavWriter* my_funtion()
{
const std::string outfilewav = webrtc::myIOSPath() + "wavtest1.wav";
static webrtc::WavWriter *my_wav_pointer(new
webrtc::WavWriter(outfilewav, 48000,1,webrtc::WavFile::SampleFormat::kInt16));
return my_wav_pointer;
}
After that call this static function where you want to pass the data to wav file for recording like:
my_funtion()->WriteSamples(audio_frame->data(), number_of_frames);
I have created a public/private key pair (Elliptic Curve) with SecKeyGeneratePair.
How can I use the SecKey instances to generate a CSR using OpenSSL in Swift?
As far as I can tell Apple's own Security framework does not have an API currently exposed f or generating CSR's. It is technically OpenSSL wrapped; so in case you do have OpenSSL ( I personally prefer LibreSSL, libtls makes life easy ).
As an alternative you can also use Commoncrypto, which my answer won't cover but there are many many examples out there
So lets go through the steps of finding out how to do this. I found that the easiest way to work with OpenSSL is to ignore the documentation completely and go straight to reading source.
To achieve what we need, we need to 'replicate' the following 2 commands:
openssl ecparam -out server.key -name prime256v1 -genkey
openssl req -new -key server.key -out server.csr
Important Note: Be sure to pick a secure curve. Use openssl ecparam -list_curves to get a list of supported curves by your version of openssl. Read more
Step 1 Generating the key
ecparam tells us we have to look in sslsource/apps/openssl/ecparam.c first.
The source gives us 3 steps:
Set up BIO for writing, setting up group parameters and finally generating the key.
Step 2 Generating the CSR
Following the same principles but this time looking in req.c and some code in apps.c which contains some boilerplate code used in req.c
I used this method to create a crude but working proof of concept that runs in swift, you can check it out on github here: CertificateTool
The code will run on any system you can compile the swift4+ toolchain on and some version of openSSL.
Edit:
Generating the EC Secret key:
//
// ECKey.swift
// CertificateToolPackageDescription
//
// Created by Antwan van Houdt on 10/01/2018.
//
import CLibreSSL
public class ECKey {
internal let secretKey: OpaquePointer
private let group: OpaquePointer
public init() {
group = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)
EC_GROUP_set_asn1_flag(group, OPENSSL_EC_NAMED_CURVE)
EC_GROUP_set_point_conversion_form(group, POINT_CONVERSION_COMPRESSED)
secretKey = EC_KEY_new()
EC_KEY_set_group(secretKey, group)
EC_KEY_generate_key(secretKey)
}
deinit {
EC_KEY_free(secretKey)
EC_GROUP_free(group)
}
}
Creating the signing request:
//
// CertificateRequest.swift
// CertificateToolPackageDescription
//
// Created by Antwan van Houdt on 10/01/2018.
//
import CLibreSSL
public enum NIDType: String {
case email = "emailAddress"
case hostName = "CN"
case organizationalUnit = "OU"
case organization = "O"
case city = "L"
case state = "ST"
case countryCode = "C"
}
public class CertificateSigningRequest {
private let request: UnsafeMutablePointer<X509_REQ>
private let key: ECKey
private let name: UnsafeMutablePointer<X509_NAME>
public init(key: ECKey, email: String, hostName: String, organizationalUnit: String, organization: String, countryCode: String, state: String, city: String) {
request = X509_REQ_new()
self.key = key
name = X509_NAME_new()
X509_REQ_set_version(request, 2)
self.add(name: email, type: .email)
self.add(name: hostName, type: .hostName)
self.add(name: organizationalUnit, type: .organizationalUnit)
self.add(name: organization, type: .organization)
self.add(name: countryCode, type: .countryCode)
self.add(name: city, type: .city)
self.add(name: state, type: .state)
X509_REQ_set_subject_name(request, name)
self.setPublicKey()
}
deinit {
X509_REQ_free(request)
X509_NAME_free(name)
}
private func add(name: String, type: NIDType) {
var buff = Array(name.utf8)
X509_NAME_add_entry_by_NID(self.name, OBJ_txt2nid(type.rawValue), MBSTRING_UTF8, &buff, Int32(buff.count), 0, 0)
}
private func setPublicKey() {
let certKey = EVP_PKEY_new()
EVP_PKEY_set1_EC_KEY(certKey, key.secretKey)
X509_REQ_set_pubkey(request, certKey)
X509_REQ_sign(request, certKey, EVP_sha256())
EVP_PKEY_free(certKey)
}
}
Note This code does not contain any BIO related functions ( yet ) to write out the PEM data to a file or to a data buffer, but they are really easy to add.
Disclaimer: I am not a professional cryptographer nor do I know all the ins and outs of the OpenSSL API. I cannot guarantee that the code I provided is a 100% correct implementation. Always be wary of code, especially crypto code, downloaded from the web.
Though it's not a straight OpenSSL implementation, I encountered outfoxx's Shield library which makes it super easy to create CSRs via Swift.
I have a receipt in PKCS7 that I obtained from my iOS app. Apple says this is a PKCS7 structure, and within that, is information regarding past recurring purchases.
I have the raw receipt here, encoded in Base64.
I've sent this payload, with my secret key, to Apple and got this response. Based on WWDC videos, and documentation, I believe I should be able to read this receipt directly, and without sending it to apple.
I'm guessing that PEMReader in BC is the correct starting point parse it, however I'm not sure how to actually use it. I've scanned the BC source code for the strings "PKCS", and looked at unit tests, however all I ever see are casts from PEMReader into another format.
using (var stream1 = new MemoryStream(receipt.Data))
using (var stream2 = new StreamReader(stream1))
{
var pp = new PemReader(stream2);
pp.ReadObject();
}
Question
How do I use Bouncy Castle to verify a raw receipt payload generated from Apple Store?
Note to self: I intend to use this to inspect the actual binary to see if ApplicationUsername is included in the receipt, yet for some reason isn't returned in the JSON result when posting the server. (Bug on Apple's side?)
I've made this using Java 7 and BouncyCastle 1.56.
For the code below, consider that pemString is the PEM string you provided. But I had to make some modifications:
format (break lines for every 64 characters) - I've made a small program to do that
include BEGIN and END headers
So my PEM looks like:
-----BEGIN PKCS7-----
MIIv5gYJKoZIhvcNAQcCoIIv1zCCL9MCAQExCzAJBgUrDgMCGgUAMIIfhwYJKoZI
hvcNAQcBoIIfeASCH3Qxgh9wMAoCAQgCAQEEAhYAMAoCARQCAQEEAgwAMAsCAQEC
AQEEAwIBADALAgELAgEBBAMCAQAwCwIBDwIBAQQDAgEAMAsCARACAQEEAwIBADAL
....
gdTu2uzkTyT+vcBlaLHK1ZpjKozsBds7ys6Q4EFp7OLxtJTj7saEDYXCNQtXBjwl
UfSGvQkXeIbsaqSPvOVIE83K3ki5i64gccA=
-----END PKCS7-----
For the code below, I followed the definition in Apple's doc:
ReceiptAttribute ::= SEQUENCE {
type INTEGER,
version INTEGER,
value OCTET STRING
}
Payload ::= SET OF ReceiptAttribute
Code:
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.asn1.DLSet;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
String pemString = // PEM String as described above
PemReader reader = new PemReader(new StringReader(pemString));
PemObject pemObject = reader.readPemObject();
reader.close();
CMSSignedData s = new CMSSignedData(pemObject.getContent());
byte[] content = (byte[]) s.getSignedContent().getContent();
ASN1InputStream in = new ASN1InputStream(content);
// Payload: a SET of ReceiptAttribute
DLSet set = (DLSet) DLSet.fromByteArray(in.readObject().getEncoded());
int size = set.size();
for (int i = 0; i < size; i++) {
// ReceiptAttribute is a SEQUENCE
DLSequence seq = (DLSequence) set.getObjectAt(i);
// value is the third element of the sequence
DEROctetString oct = (DEROctetString) seq.getObjectAt(2);
ASN1Object obj = readObject(oct.getOctets()); // *** see comments below ***
}
in.close();
// readObject method
public ASN1Object readObject(byte[] b) throws IOException {
ASN1InputStream in = null;
try {
in = new ASN1InputStream(b);
return in.readObject();
} catch (Exception e) {
// if error occurs, just return the octet string
return new DEROctetString(b);
} finally {
in.close();
}
}
Variable obj will be the content of the ReceiptAttribute, and it can vary a lot - I've seen DERIA5String, DERUTF8String, ASN1Integer and many others. As I don't know all possible values of this field, I think it's up to you to check each value.
I have an issue with Notification Service Extension.
I have followed step by step documentation
https://developer.xamarin.com/guides/ios/platform_features/introduction-to-ios10/user-notifications/enhanced-user-notifications/#Working_with_Service_Extensions
To implement, I have done in that way.
Added Notification Service Extension with same prefix of my app (adding a suffix, ex: APP: com.testapp.main - EXT: com.testapp.main.notificationextension)
Created APPID identifier com.testapp.main.notificationextension into Member Center of Apple
Created certificate and provisioning profile to send push notification for APP ID com.testapp.main.notificationextension
Imported into Xcode and Xamarin certificate and provisioning
Build my app with reference to Notification Extension reference.
Created archive to upload to TestFlight
Signed app with its Distribution Certificate and Provisioning Profile
Signed extension with its Distribution Certificate and Provisioning Profile
Uploaded to TestFlight
Download and allowed push notification for my app
Sent rich push notification with Localytics Dashboard for messaging
- Device receive push notification but not pass for NotificationService.cs code of Notification Service Extension!
This is my NotificationService code:
using System;
using Foundation;
using UserNotifications;
namespace NotificationServiceExtension
{
[Register("NotificationService")]
public class NotificationService : UNNotificationServiceExtension
{
Action<UNNotificationContent> ContentHandler { get; set; }
UNMutableNotificationContent BestAttemptContent { get; set; }
const string ATTACHMENT_IMAGE_KEY = "ll_attachment_url";
const string ATTACHMENT_TYPE_KEY = "ll_attachment_type";
const string ATTACHMENT_FILE_NAME = "-localytics-rich-push-attachment.";
protected NotificationService(IntPtr handle) : base(handle)
{
// Note: this .ctor should not contain any initialization logic.
}
public override void DidReceiveNotificationRequest(UNNotificationRequest request, Action<UNNotificationContent> contentHandler)
{
System.Diagnostics.Debug.WriteLine("Notification Service DidReceiveNotificationRequest");
ContentHandler = contentHandler;
BestAttemptContent = (UNMutableNotificationContent)request.Content.MutableCopy();
if (BestAttemptContent != null)
{
string imageURL = null;
string imageType = null;
if (BestAttemptContent.UserInfo.ContainsKey(new NSString(ATTACHMENT_IMAGE_KEY)))
{
imageURL = BestAttemptContent.UserInfo.ValueForKey(new NSString(ATTACHMENT_IMAGE_KEY)).ToString();
}
if (BestAttemptContent.UserInfo.ContainsKey(new NSString(ATTACHMENT_TYPE_KEY)))
{
imageType = BestAttemptContent.UserInfo.ValueForKey(new NSString(ATTACHMENT_TYPE_KEY)).ToString();
}
if (imageURL == null || imageType == null)
{
ContentHandler(BestAttemptContent);
return;
}
var url = NSUrl.FromString(imageURL);
var task = NSUrlSession.SharedSession.CreateDownloadTask(url, (tempFile, response, error) =>
{
if (error != null)
{
ContentHandler(BestAttemptContent);
return;
}
if (tempFile == null)
{
ContentHandler(BestAttemptContent);
return;
}
var cache = NSSearchPath.GetDirectories(NSSearchPathDirectory.CachesDirectory, NSSearchPathDomain.User, true);
var cachesFolder = cache[0];
var guid = NSProcessInfo.ProcessInfo.GloballyUniqueString;
var fileName = guid + ATTACHMENT_FILE_NAME + imageType;
var cacheFile = cachesFolder + fileName;
var attachmentURL = NSUrl.CreateFileUrl(cacheFile, false, null);
NSError err = null;
NSFileManager.DefaultManager.Move(tempFile, attachmentURL, out err);
if (err != null)
{
ContentHandler(BestAttemptContent);
return;
}
UNNotificationAttachmentOptions options = null;
var attachment = UNNotificationAttachment.FromIdentifier("localytics-rich-push-attachment", attachmentURL, options, out err);
if (attachment != null)
{
BestAttemptContent.Attachments = new UNNotificationAttachment[] { attachment };
}
ContentHandler(BestAttemptContent);
return;
});
task.Resume();
}
else {
ContentHandler(BestAttemptContent);
}
}
public override void TimeWillExpire()
{
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
ContentHandler(BestAttemptContent);
return;
}
}
}
You are doing everything correctly, this is an issue raised by a few other xamarin developers. From what I can tell, as soon as you run the NSURLSession to download something, even if it's super super small, you go above the memory limit allowed for this type of extension. This is most probably very specific to xamarin.
Here is the link to the bugzilla.
https://bugzilla.xamarin.com/show_bug.cgi?id=43985
The work-around/hack I found is far from ideal. I rewrote this app extension in xcode in objective-c (you could use swift too I suppose). It is a fairly small (1 class) extension. Then built it in xcode using the same code signature certificate / provisioning profile and then found the .appex file in the xcode's output.
From then, you can take the "cheap way", and swap this .appex file in your .ipa folder manually just before resigning and submitting the app. If that's good enough for you, you can stop here.
Or you can automate this process, to to so, place the appex file in the csproj's extension and set the build-action as "content". Then in this csproj's file (you'll need to edit directly) you can add something like this. (In this case, the file is called Notifications.appex and is placed in a folder called NativeExtension)
<Target Name="BeforeCodeSign">
<ItemGroup>
<NativeExtensionDirectory Include="NativeExtension\Debug\**\*.*" />
</ItemGroup>
<!-- cleanup the application extension built with Xamarin (too heavy in memory)-->
<RemoveDir SessionId="$(BuildSessionId)"
Directories="bin\iPhone\Debug\Notifications.appex"/>
<!-- copy the native one, built in obj-c -->
<Copy
SessionId="$(BuildSessionId)"
SourceFiles="#(NativeExtensionDirectory)"
DestinationFolder="bin\iPhone\Debug\Notifications.appex"
SkipUnchangedFiles="true"
OverwriteReadOnlyFiles="true"
Retries="3"
RetryDelayMilliseconds="300"/>
</Target>
This gives you the general idea, but obviously if you want to support ad-hoc distribution signature, iOS app-store distribution signature you will need to add a bit more code into this (and possibly add in the csproj a native appex file for each different signature), I would suggest putting such xml code in separate ".targets" file and use conditional calltargets in the csproj. Like this:
<Target Name="BeforeCodeSign">
<CallTarget Targets="ImportExtension_Debug" Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhone' " />
<CallTarget Targets="ImportExtension" Condition=" '$(Configuration)|$(Platform)' == 'Release|iPhone' " />
</Target>
If anyone else comes here, the code by original poster works for me and the bug mention is now marked as fixed. If I have one tip, do not try to do this on Windows. You will be in for a whole world of pain and will get nowhere (actually, it did work for me, once!). Also expect Visual Studio on Mac to crash, a lot, if you try to debug!
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)