iOS: Simple way to manage REST end points - ios

Our REST based application can be used for testing on multiple internal environments each with a different REST end point. Is there a simple way to set up environment level configuration within an iOS (Swift 3) app? I've seen a few approaches but they all seem pretty involved.

This is my approach of doing things when we have multiple end points. I used to make a ConfigurationManager class something like this
Swift 3.0 code
import Foundation
import UIKit
let kEnvironmentsPlist:NSString? = "Environments"
let kConfigurationKey:NSString? = "ActiveConfiguration"
let kAPIEndpointKey:NSString? = "APIEndPoint"
let kLoggingEnabledKey:NSString? = "LoggingEnabled"
let kAnalyticsTrackingEnabled:NSString? = "AnalyticsTrackingEnabled"
class ConfigurationManager:NSObject {
var environment : NSDictionary?
//Singleton Method
static let sharedInstance: ConfigurationManager = {
let instance = ConfigurationManager()
// setup code
return instance
}()
override init() {
super.init()
initialize()
}
// Private method
func initialize () {
var environments: NSDictionary?
if let envsPlistPath = Bundle.main.path(forResource: "Environments", ofType: "plist") {
environments = NSDictionary(contentsOfFile: envsPlistPath)
}
self.environment = environments!.object(forKey: currentConfiguration()) as? NSDictionary
if self.environment == nil {
assertionFailure(NSLocalizedString("Unable to load application configuration", comment: "Unable to load application configuration"))
}
}
// CurrentConfiguration
func currentConfiguration () -> String {
let configuration = Bundle.main.infoDictionary?[kConfigurationKey! as String] as? String
return configuration!
}
// APIEndpoint
func APIEndpoint () -> String {
let configuration = self.environment![kAPIEndpointKey!]
return (configuration)! as! String
}
// isLoggingEnabled
func isLoggingEnabled () -> Bool {
let configuration = self.environment![kLoggingEnabledKey!]
return (configuration)! as! Bool
}
// isAnalyticsTrackingEnabled
func isAnalyticsTrackingEnabled () -> String {
let configuration = self.environment![kAnalyticsTrackingEnabled!]
return (configuration)! as! String
}
func applicationName()->String{
let bundleDict = Bundle.main.infoDictionary! as NSDictionary
return bundleDict.object(forKey: "CFBundleName") as! String
}
}
In Project--> Info Add some new configurations as per your need.
I have added Staging and QA as extra endpoints.Generally I use to make Staging as Release config and QA as Debug. So it will look like:
Now go to Targets -> Build Settings and add a User Defined Setting
Give the name of the user defined like ACTIVE_CONFIGURATION.
Add a key named ActiveConfiguration in info.plist with a variable name as $(ACTIVE_CONFIGURATION) same as given in User Defined Settings with a $ in the beginning. We gave the name of key as ActiveConfiguration because we are using the same name in our ConfigurationManager.swift class for kConfigurationKey.
let kConfigurationKey:NSString? = "ActiveConfiguration"
You can define as per your naming convention.
It will look like:
Now in the ConfigurationManager class I am getting a path for Environments.plist file.
I will just make a Environments.plist file like this:
The actual description source of this file is
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Development</key>
<dict>
<key>APIEndPoint</key>
<string>https://dev</string>
<key>LoggingEnabled</key>
<true/>
<key>AnalyticsTrackingEnabled</key>
<true/>
<key>Flurry</key>
<dict>
<key>FlurryApplicationID</key>
<string></string>
<key>FlurryApplicationSecret</key>
<string></string>
</dict>
<key>Facebook</key>
<dict>
<key>FacebookAppID</key>
<string></string>
<key>FacebookAppSecret</key>
<string></string>
</dict>
</dict>
<key>QA</key>
<dict>
<key>APIEndPoint</key>
<string>https://qa</string>
<key>LoggingEnabled</key>
<true/>
<key>AnalyticsTrackingEnabled</key>
<true/>
<key>Flurry</key>
<dict>
<key>FlurryApplicationID</key>
<string></string>
<key>FlurryApplicationSecret</key>
<string></string>
</dict>
<key>Facebook</key>
<dict>
<key>FacebookAppID</key>
<string></string>
<key>FacebookAppSecret</key>
<string></string>
</dict>
</dict>
<key>Staging</key>
<dict>
<key>APIEndPoint</key>
<string>https://staging</string>
<key>LoggingEnabled</key>
<false/>
<key>AnalyticsTrackingEnabled</key>
<true/>
<key>Flurry</key>
<dict>
<key>FlurryApplicationID</key>
<string></string>
<key>FlurryApplicationSecret</key>
<string></string>
</dict>
<key>Facebook</key>
<dict>
<key>FacebookAppID</key>
<string>840474532726958</string>
<key>FacebookAppSecret</key>
<string></string>
</dict>
</dict>
<key>Production</key>
<dict>
<key>APIEndPoint</key>
<string>https://production</string>
<key>LoggingEnabled</key>
<true/>
<key>AnalyticsTrackingEnabled</key>
<true/>
<key>Flurry</key>
<dict>
<key>FlurryApplicationID</key>
<string></string>
<key>FlurryApplicationSecret</key>
<string></string>
</dict>
<key>Facebook</key>
<dict>
<key>FacebookAppID</key>
<string></string>
<key>FacebookAppSecret</key>
<string></string>
</dict>
</dict>
</dict>
</plist>
We are now good to go. Now you have to just call
ConfigurationManager.sharedInstance.APIEndpoint()
for your respective end points.
Now you just have to change the schemes from Edit Schemes and you are done and change the Build Configuration in info.
This not only manages API End Points but also other things like whether to enable analytics or tracking for the respective end point or different ids of Facebook for different end points.

As Zac Kwan suggested, you can use different schemes to accomplish this, but you don't necessarily have to create a different configuration as well. Each scheme can specify unique environment variables. Then, access them from Swift:
let prodURL = "http://api.com"
let baseURL = ProcessInfo.processInfo.environment["BASE_URL"] ?? prodURL

I found that creating different Scheme and Configuration for your project works best. My setup is as follow:
I usually have 3 different scheme, MyApp-dev, MyApp-staging and MyApp.
Each of the scheme i created User-Defined-Attribute to have different string appending to my Bundle Display Name. So it can concurrently appear on my iOS device as MyApp-d, MyApp-s and MyApp. Each also have its own Bundle ID Then I create custom flags for each of them.
So in my Routes.swift files i have something like this at the top:
#if PRODUCTION
static let hostName = "http://production.com/api/v1/"
#elseif STAGING
static let hostName = "http://staging.com/api/v1/"
#else
static let hostName = "http://development.com/api/v1/"
#endif
There is quite a few ways in how to update different hostname. But ultimately creating different Scheme and Configuration is always the first step.
Here is a few links that might help you get started:
https://medium.com/#danielgalasko/change-your-api-endpoint-environment-using-xcode-configurations-in-swift-c1ad2722200e#.o6nhic3pf
http://limlab.io/swift/2016/02/22/xcode-working-with-multiple-environments.html

I ended up using https://github.com/theappbusiness/ConfigGenerator:
A command line tool to auto-generate configuration file code, for use
in Xcode projects. The configen tool is used to auto-generate
configuration code from a property list. It is intended to create the
kind of configuration needed for external URLs or API keys used by
your app. Currently supports both Swift and Objective-C code
generation.

Related

Apple Local Push Connectivity with error nil?

I'm trying to configure Local Push Connectivity. I already have Local Push Entitlement, and have install a provisioning profile with local push entitlement. It's build fine but when app start, PushProvider didn't active and start and Push Manager show error nil. I have done every instructions that sample code have provided.
This is my project.
In my application target, I have a bundle id com.my_team_name.my_app_name
and in the app group name group.com.my_team_name.my_app_name
In the .entitlement, I've set the required configuration:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>app-push-provider</string>
</array>
<key>com.apple.developer.networking.wifi-info</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.my_team_name.my_app_name</string>
</array>
</dict>
</plist>
Info.plist has noting to change
And I have a PushManager Class with this code
class AppPushManager: NSObject, NEAppPushDelegate{
func appPushManager(_ manager: NEAppPushManager, didReceiveIncomingCallWithUserInfo userInfo: [AnyHashable : Any] = [:]) {
}
static let shared = AppPushManager()
private var pushManager: NEAppPushManager = NEAppPushManager()
private let pushManagerDescription = "PushDefaultConfiguration"
private let pushProviderBundleIdentifier = "com.my_team_name.my_app_name.PushProvider"
func initialize() {
if pushManager.delegate == nil {
pushManager.delegate = self
}
pushManager.localizedDescription = pushManagerDescription
pushManager.providerBundleIdentifier = pushProviderBundleIdentifier
pushManager.isEnabled = true
pushManager.providerConfiguration = [
"host": "my_server.local"
]
pushManager.matchSSIDs = ["my_wifi_ssid"]
pushManager.saveToPreferences(completionHandler: { error in
print("error? \(String(describing: error))")
print("is active: \(pushManager.isActive)")
})
}
}
In my extension, A PushProvider Target. I have a bundle id com.my_team_name.my_app_name.PushProvider
and in the app group name group.com.my_team_name.my_app_name
In the Info.plist of my extension, I've added the required configuration:
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.networkextension.app-push</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).PushProvider</string>
</dict>
the .entitlement file have the same contents as the application.
and I have created the extension class "PushProvider.swift" as follow:
class PushProvider: NEAppPushProvider {
...
}
When I run the application, I got this printed out
error? nil
is active: false
I think it might be something with providerBundleIdentifier in Push Manager. Have anyone know what I've missing?

TIC SSL Trust Error while trying to read a txt file

I'm having this problem with reading from a txt file from a website without a certificate even though I have allowed it in Info.plist.
My Swift code looks like this:
let url = URL(string: "http://newskit.matsworld.io/domaci/text.txt")!
let task = URLSession.shared.downloadTask(with: url) { localURL, urlResponse, error in
if let localURL = localURL {
if let string = try? String(contentsOf: localURL) {
print(string)
}
}
}
task.resume()
And the Info.plist looks like this:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>http://newskit.matsworld.io</key>
<dict>
<key>ExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>IncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
I'm stuck with this and can't figure it out. Thanks for help!
The domain key is supposed to be just the domain without the scheme (http) and without subdomains if IncludesSubdomains is specified
<key>matsworld.io</key>

swift 3 awslambda invoker error [duplicate]

I am facing a strange issue with AWSS3.
Setup:
AWS Mobile HUB
Cognito
DynamoDB
S3
--> Cognito, Dynamo & even S3 (through cognito user data) work.
However I now tried to connect directly to AWS3 with the following code:"
let transferManager = AWSS3TransferManager.default()
let uploadRequest = AWSS3TransferManagerUploadRequest()
uploadRequest?.bucket = "XXXXXXXXXXXX"
uploadRequest?.key = "user-data/" + awsId! + "/primary_profile_picture.png"
uploadRequest?.body = imgUrl as URL
transferManager.upload(uploadRequest!).continueWith(executor: AWSExecutor.mainThread(), block: { (task:AWSTask<AnyObject>) -> Any? in
if let error = task.error as? NSError {
if error.domain == AWSS3TransferManagerErrorDomain, let code = AWSS3TransferManagerErrorType(rawValue: error.code) {
switch code {
case .cancelled, .paused:
break
default:
print("Error uploading: \(uploadRequest?.key) Error: \(error)")
}
} else {
print("Error uploading: \(uploadRequest?.key) Error: \(error)")
}
return nil
}
let uploadOutput = task.result
print("Upload complete for: \(uploadRequest?.key)")
return nil
})
and am getting the error:
AWSiOSSDK v2.5.1 [Debug] AWSInfo.m line:122 | -[AWSServiceInfo initWithInfoDictionary:checkRegion:] | Couldn't read the region configuration from Info.plist for the client. Please check your `Info.plist` if you are providing the SDK configuration values through `Info.plist`.
2017-02-20 19:29:21.748997 [2210:1152801] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'The service configuration is `nil`. You need to configure `Info.plist` or set `defaultServiceConfiguration` before using this method.'
I am using the downloaded plist configuration from AWS Mobiel HUB and am therfore a bit surprised that it does not work (as all other components do).
Any ideas what the issue might be? The plist actually contains the bucket ID & region.
For me, I had to configure the credentials with the following code, before uploading:
let credentialsProvider = AWSCognitoCredentialsProvider(regionType:.USEast1,identityPoolId:PoolID)
let configuration = AWSServiceConfiguration(region:.USEast1, credentialsProvider:credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = configuration
Where PoolID is my Cognito identity. I hope this helps others.
Your info.plist needs to have S3TransferManager in it.
So, **AWS -> S3TransferManager -> Default -> Region -> ...**
You can find an example of one here
Swift 3 - Xcode 8.3.3
For people still having this issue, I just spent 3h fighting against this annoying setup issue.
I added both these chunks in my Info.plist (replace the variables between ** ** in the second bloc) and now it's working again.
Amazon's documentation isn't updated properly I think. I hope this can save some people some time.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>amazonaws.com</key>
<dict>
<key>NSThirdPartyExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>amazonaws.com.cn</key>
<dict>
<key>NSThirdPartyExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
and:
<key>AWS</key>
<dict>
<key>CredentialsProvider</key>
<dict>
<key>CognitoIdentity</key>
<dict>
<key>Default</key>
<dict>
<key>PoolId</key>
<string>**YourCognitoIdentityPoolId**</string>
<key>Region</key>
<string>**AWSRegionUnknown**</string>
</dict>
</dict>
</dict>
<key>S3TransferManager</key>
<dict>
<key>Default</key>
<dict>
<key>Region</key>
<string>**AWSRegionUnknown**</string>
</dict>
</dict>
</dict>
I had the same issue instead of S3TransferManager you should put DynamoDBObjectMapper
e.g..
<key>DynamoDBObjectMapper</key>
<dict>
<key>Default</key>
<dict>
<key>Region</key>
<string>us-east-1</string>
</dict>
</dict>
The problem was, region should be:
us-east-1
Instead of; US_EAST_1
Add Service configuration file with the data
If the above marked solution is not working for some cases. Try the below solution.
As the error says, the service configuration file is nil. So, we need to add the awsconfiguration.json file with the below data.
{
"Version": "1.0",
"IdentityManager": {
"Default": {}
},
"CredentialsProvider": {
"CognitoIdentity": {
"Default": {
"PoolId": "REPLACE_ME",
"Region": "REPLACE_ME"
}
}
},
"S3TransferUtility": {
"Default": {
"Bucket": "REPLACE_ME",
"Region": "REPLACE_ME"
}
}
}
Replace the bucket, region, poolId of your project.

AWSS3 Region / plist configuration issue 'The service configuration is `nil`

I am facing a strange issue with AWSS3.
Setup:
AWS Mobile HUB
Cognito
DynamoDB
S3
--> Cognito, Dynamo & even S3 (through cognito user data) work.
However I now tried to connect directly to AWS3 with the following code:"
let transferManager = AWSS3TransferManager.default()
let uploadRequest = AWSS3TransferManagerUploadRequest()
uploadRequest?.bucket = "XXXXXXXXXXXX"
uploadRequest?.key = "user-data/" + awsId! + "/primary_profile_picture.png"
uploadRequest?.body = imgUrl as URL
transferManager.upload(uploadRequest!).continueWith(executor: AWSExecutor.mainThread(), block: { (task:AWSTask<AnyObject>) -> Any? in
if let error = task.error as? NSError {
if error.domain == AWSS3TransferManagerErrorDomain, let code = AWSS3TransferManagerErrorType(rawValue: error.code) {
switch code {
case .cancelled, .paused:
break
default:
print("Error uploading: \(uploadRequest?.key) Error: \(error)")
}
} else {
print("Error uploading: \(uploadRequest?.key) Error: \(error)")
}
return nil
}
let uploadOutput = task.result
print("Upload complete for: \(uploadRequest?.key)")
return nil
})
and am getting the error:
AWSiOSSDK v2.5.1 [Debug] AWSInfo.m line:122 | -[AWSServiceInfo initWithInfoDictionary:checkRegion:] | Couldn't read the region configuration from Info.plist for the client. Please check your `Info.plist` if you are providing the SDK configuration values through `Info.plist`.
2017-02-20 19:29:21.748997 [2210:1152801] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'The service configuration is `nil`. You need to configure `Info.plist` or set `defaultServiceConfiguration` before using this method.'
I am using the downloaded plist configuration from AWS Mobiel HUB and am therfore a bit surprised that it does not work (as all other components do).
Any ideas what the issue might be? The plist actually contains the bucket ID & region.
For me, I had to configure the credentials with the following code, before uploading:
let credentialsProvider = AWSCognitoCredentialsProvider(regionType:.USEast1,identityPoolId:PoolID)
let configuration = AWSServiceConfiguration(region:.USEast1, credentialsProvider:credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = configuration
Where PoolID is my Cognito identity. I hope this helps others.
Your info.plist needs to have S3TransferManager in it.
So, **AWS -> S3TransferManager -> Default -> Region -> ...**
You can find an example of one here
Swift 3 - Xcode 8.3.3
For people still having this issue, I just spent 3h fighting against this annoying setup issue.
I added both these chunks in my Info.plist (replace the variables between ** ** in the second bloc) and now it's working again.
Amazon's documentation isn't updated properly I think. I hope this can save some people some time.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>amazonaws.com</key>
<dict>
<key>NSThirdPartyExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>amazonaws.com.cn</key>
<dict>
<key>NSThirdPartyExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
and:
<key>AWS</key>
<dict>
<key>CredentialsProvider</key>
<dict>
<key>CognitoIdentity</key>
<dict>
<key>Default</key>
<dict>
<key>PoolId</key>
<string>**YourCognitoIdentityPoolId**</string>
<key>Region</key>
<string>**AWSRegionUnknown**</string>
</dict>
</dict>
</dict>
<key>S3TransferManager</key>
<dict>
<key>Default</key>
<dict>
<key>Region</key>
<string>**AWSRegionUnknown**</string>
</dict>
</dict>
</dict>
I had the same issue instead of S3TransferManager you should put DynamoDBObjectMapper
e.g..
<key>DynamoDBObjectMapper</key>
<dict>
<key>Default</key>
<dict>
<key>Region</key>
<string>us-east-1</string>
</dict>
</dict>
The problem was, region should be:
us-east-1
Instead of; US_EAST_1
Add Service configuration file with the data
If the above marked solution is not working for some cases. Try the below solution.
As the error says, the service configuration file is nil. So, we need to add the awsconfiguration.json file with the below data.
{
"Version": "1.0",
"IdentityManager": {
"Default": {}
},
"CredentialsProvider": {
"CognitoIdentity": {
"Default": {
"PoolId": "REPLACE_ME",
"Region": "REPLACE_ME"
}
}
},
"S3TransferUtility": {
"Default": {
"Bucket": "REPLACE_ME",
"Region": "REPLACE_ME"
}
}
}
Replace the bucket, region, poolId of your project.

Apple Watch Device crashing with SandboxViolation deny(1) network-outbound

Im having an issue on the Apple Watch when making network calls.
It works fine on the simulator but when deployed to the device I see this in the device logs:
MyAppleWatch kernel(Sandbox)[0] : SandboxViolation: MyWatchApp(203) deny(1) network-outbound /private/var/run/mDNSResponder
The code for making the call is done using refit and works on the simulator so I would imagine it is not the cause but I will post it if necessary.
I have set these values in the Info.plist of the WatchExtensionApp (and not set them in the WatchApp as these keys )
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>my.domain.com</key>
<dict>
<key>NSExceptionRequiresForwardSecrecy</key>
<true/>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
I set the WatchApp and WatchExtensionApp to:
Just at a bit of loss as to what to try next. Any help would be greatly appreciated.
Ok I got it working, Firstly I changed my Info.plist to:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>my.domain.com</key>
<dict>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
I had to change all my network calls to us NSUrlSession rather than using Refit + HttpClient. So I assume there is something in the HttpClient that watchOS doesn't like or watchos only works with NSUrlSession. Anyway here is a sample of one of my calls:
public async Task<HttpResponse> MakeCall(NSMutableUrlRequest request)
{
var result = new HttpResponse();
var config = NSUrlSessionConfiguration.DefaultSessionConfiguration;
var session = NSUrlSession.FromConfiguration(config);
var resultUser = await session.CreateDataTaskAsync(request);
var response = resultUser.Response as NSHttpUrlResponse;
if (response != null)
{
result.StatusCode = (int)response.StatusCode;
result.Content = NSString.FromData(resultUser.Data, NSStringEncoding.UTF8).ToString();
}
return result;
}
public async Task<HttpResponse> GetStuffWithParameter(string parameter, string authorization)
{
var url = new NSUrl($"https://www.domain.com/api/stuff/{parameter}");
var header = new NSMutableDictionary();
header.SetValueForKey(new NSString(authorization), new NSString("Authorization"));
header.SetValueForKey(new NSString("application/json"), new NSString("Content-Type"));
var request = new NSMutableUrlRequest(url)
{
Headers = header,
HttpMethod = "GET"
};
return await MakeCall(request);
}
public class HttpResponse
{
public int StatusCode { get; set; }
public object Content { get; set; }
}
Hope this helps.

Resources