I am using Alamofire in my IOS Application and has implemented SSL Pinning. I have kept the certificate locally in .der format. The issue is the certificate when getting expired, I have to do an App release with new certificate.
Code Snippet used:
struct Certificates {
fileprivate static let sslCertificate: SecCertificate? = Certificates.certificate(filename: (Bundle.main.infoDictionary?["SSL_CERTIFICATE"] as? String))
private static func certificate(filename: String?) -> SecCertificate? {
guard let filePath = Bundle.main.path(forResource: filename, ofType: "der"), let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)), let certificate = SecCertificateCreateWithData(nil, data as CFData) else{
return nil
}
return certificate
}
final class APIClient {
// MARK: - Host To Evaluate
private static var hostToEvaluate: String {
guard let urlString = Bundle.main.infoDictionary?["API_BASE_URL_ENDPOINT"] as? String, let hostURL = URL(string: urlString), let host = hostURL.host else{
return ""
}
return host
}
// MARK: - Evaluators
private static var evaluators: Dictionary<String,ServerTrustEvaluating> {
guard let sslCertificate = Certificates.sslCertificate else{
return [hostToEvaluate : DisabledTrustEvaluator()]
}
return [hostToEvaluate : PinnedCertificatesTrustEvaluator(certificates: [sslCertificate])]
}
// MARK: - Session
private static let session = Session(
serverTrustManager: ServerTrustManager(evaluators: evaluators)
)
}
Can I keep 2 certificates and do a check to evaluate existing certificate and if it fails pick up the new certificate.
Have checked few documentations and forums, but did not find anything relevant.
Thanks,
Abin
Yes. By default PublicKeysTrustEvaluator and PinnedCertificatesTrustEvaluator will pick up all certificates that are in the application bundle, if you don't pass anything for keys and certificates parameters in the initializer respectively. The evaluation will pass if one of the bundled certificates are matching.
Your code will look something like this:
// MARK: - Evaluators
private static var evaluators: Dictionary<String, ServerTrustEvaluating> = [
hostToEvaluate: PinnedCertificatesTrustEvaluator()
]
// MARK: - Session
private static let session = Session(
serverTrustManager: ServerTrustManager(evaluators: evaluators)
)
Just make sure that both certificates are in the application bundle. (add them to your target)
Related
I am new fairly new to swift development but am obsessed and in love with learning, I have not only dedicated myself to learning but I am starting to apply my knowledge and have built a small messaging app with the capability to select a file. I have successfully created the ability for future users of my app to send documents via the messaging system, and I am able to show the file name, type etc. However, I am struggling to understand (reading apple documentation and other community developer's posts regarding this) how to fetch a specific icon type from the iOS system based on the file's extension type (e.g. xlsx), let alone the UTIType (e.g. public.movie -> mp4, etc.).
If anyone has suggestions on how/where to facilitate this, I would like to set my current, generic file icon to a custom filetype, as the one in the attached image example below (PDF in example):
Tarun's answer was quick and very helpful in getting one step further, however, it returned this icon for pdf, png, and jpeg, all tested (regardless of the aforementioned differentiation in file type.
Here is my code:
guard let fileUrl = message.documentUrl else { return }
let fileNameUrl = (fileUrl as NSString).lastPathComponent
let fileName = fileNameUrl.components(separatedBy: "?")[0].removingPercentEncoding
let documentInteractionController = UIDocumentInteractionController()
documentInteractionController.name = fileName
documentInteractionController.url = URL(fileURLWithPath: fileNameUrl)
let fileTypeIcon = documentInteractionController.icons.first
fileIcon.image = fileTypeIcon
When you get a file url from UIDocumentPickerViewController, you need to query it's uti / mimeType etc. which you can do from following helpers.
import Foundation
import MobileCoreServices
public extension URL {
private var utiCFString: CFString? {
UTTypeCreatePreferredIdentifierForTag(
kUTTagClassFilenameExtension,
self.pathExtension as NSString,
nil
)?.takeRetainedValue()
}
var uti: String? {
self.utiCFString as String?
}
var mimeType: String? {
if let utiCFString = self.utiCFString {
return UTTypeCopyPreferredTagWithClass(
utiCFString,
kUTTagClassMIMEType
)?.takeRetainedValue() as String?
}
return nil
}
}
extension ViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let url = urls.first else { return }
guard let mimeType = url.mimeType else {
print("Invalid file type")
return
}
let uti = url.uti
}
}
Once you have all the info you need, you can try this -
let documentInteractionController = UIDocumentInteractionController()
documentInteractionController.name = filename
documentInteractionController.url = URL(fileURLWithPath: filePath)
documentInteractionController.uti = uti
let mayBeIcon = documentInteractionController.icons.last
You can use QuickLook Thumbnailing framework.Have a look at the Apple documentation.
https://developer.apple.com/documentation/quicklookthumbnailing
I would like to print the url of my Apollo iOS Client GraphQL queries to the Xcode console when the query is called.
You don't need to write swift code to extract the QueryName.
Use Proxyman, like Charles Proxy. It will display the QueryName on the column by default.
Ref: https://docs.proxyman.io/advanced-features/graphql
Per the Apollo iOS Client docs, a logging interceptor can be added in a custom Interceptor Provider.
I created a custom interceptor provider using the code from DefaultInterceptorProvider, and included the logging interceptor.
import Apollo
class InterceptorProviderWithLogging: InterceptorProvider {
private let client: URLSessionClient
private let store: ApolloStore
private let shouldInvalidateClientOnDeinit: Bool
public init(client: URLSessionClient = URLSessionClient(),
shouldInvalidateClientOnDeinit: Bool = true,
store: ApolloStore) {
self.client = client
self.shouldInvalidateClientOnDeinit = shouldInvalidateClientOnDeinit
self.store = store
}
deinit {
if self.shouldInvalidateClientOnDeinit {
self.client.invalidate()
}
}
open func interceptors<Operation: GraphQLOperation>(for operation: Operation) -> [ApolloInterceptor] {
return [
MaxRetryInterceptor(),
CacheReadInterceptor(store: self.store),
RequestLoggingInterceptor(), // added logging interceptor
NetworkFetchInterceptor(client: self.client),
ResponseCodeInterceptor(),
JSONResponseParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject),
AutomaticPersistedQueryInterceptor(),
CacheWriteInterceptor(store: self.store),
]
}
open func additionalErrorInterceptor<Operation: GraphQLOperation>(for operation: Operation) -> ApolloErrorInterceptor? {
return nil
}
}
class RequestLoggingInterceptor: ApolloInterceptor {
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: #escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
if let url = try? request.toURLRequest().url?.absoluteString.removingPercentEncoding {
if let variables = request.operation.variables {
print("\(request.operation.operationName) parameters: \(variables) \(url)")
} else {
print("\(request.operation.operationName) \(url)")
}
}
chain.proceedAsync(request: request, response: response, completion: completion)
}
}
I use the custom interceptor provider in the Request Chain Network Transport.
private(set) lazy var apolloClient: ApolloClient = {
let store = ApolloStore()
let interceptorProvider = InterceptorProviderWithLogging(store: store)
let requestChainTransport = RequestChainNetworkTransport(
interceptorProvider: interceptorProvider,
endpointURL: url,
additionalHeaders: [:],
autoPersistQueries: false,
requestBodyCreator: ApolloRequestBodyCreator(),
useGETForQueries: true,
useGETForPersistedQueryRetry: false
)
return ApolloClient(networkTransport: requestChainTransport, store: store)
}()
Extending GraphQLQuery provides access to the operation name, operation id, and variables, which can be used to build up the url. I also print out the operation name and variables for the query.
extension GraphQLQuery {
func printInfo() {
if let variables = self.variables?.JSONString {
let cleanedVariables = variables.replacingOccurrences(of: "\\", with: "")
print("GraphQL Query: \(self.operationName) \(variables))")
if let operationID = self.operationIdentifier {
let url = "\(GraphQLClient.shared.url)?extensions={\"persistedQuery\":{\"sha256Hash\":\"\(operationID)\",\"version\":1}}&id=\(operationID)&operationName=\(self.operationName)&variables=\(cleanedVariables)"
print("GraphQL URL", url)
}
} else {
print("GraphQL Query: \(self.operationName)")
if let operationID = self.operationIdentifier {
let url = "\(GraphQLClient.shared.url)?extensions={\"persistedQuery\":{\"sha256Hash\":\"\(operationID)\",\"version\":1}}&id=\(operationID)&operationName=\(self.operationName)"
print("GraphQL URL", url)
}
}
}
}
Usage:
let standingsQuery = GetStandingsForSportQuery(sportID: sportIDInt, season: season)
standingsQuery.printInfo()
Example output:
GraphQL Query: getStandingsForSport {"sportID":7,"season":"2020"})
GraphQL URL: https://api.company.com/graphql?extensions={"persistedQuery":{"sha256Hash":"932b414fdadb641f95659d6c61aa29d6d6b0ccf1fa704a0ace751187b90b8cac","version":1}}&id=932b414fdadb641f95659d6c61aa29d6d6b0ccf1fa704a0ace751187b90b8cac&operationName=getStandingsForSport&variables={"sportID":1,"season":"2020"}
The url format in this example may not be typical as we're using persisted queries. I used Charles proxy to see the actual url being sent so I'd know the format.
You could also extend GraphQLOperation instead of GraphQLQuery to get this same info, which would also support mutations and subscriptions.
I've got a private key in addition to a public key to pin certificate.
How would authenticate with the to the server?
$ file *
foo.der: data
foo.private.der: data
derived with openssl from
foo.key: PEM RSA private key
foo.pem: PEM certificate
what i need is an alamofire equivalent of this:
curl --key foo.key --cert foo.pem --location --request GET 'https://somhostofmine/v1/welcome/'
$ curl --key ./client_key.pem --cert ./client.pem --location --request GET 'https://someurl'
"Hello wold!"
works
$ curl --cert ./client.pem --location --request GET 'https://someurl/v1/welcome/'
curl: (58) unable to set private key file: './client.pem' type PEM
does not
So suggestions to use certificate itself without private key do not work.
Are we dealing with certificate pinning here or given the presence of the private key this is something else going here???
You can create a ServerTrustManager passing as parameter your evaluators per host name.
For public key pinning use PublicKeysTrustEvaluator like this:
let evaluators: [String : ServerTrustEvaluating] = [
"your.host.com": PublicKeysTrustEvaluator(performDefaultValidation: false, validateHost: false)
]
let serverTrustManager = ServerTrustManager(evaluators: evaluators)
For certificate pinning use PinnedCertificatesTrustEvaluator like this:
let evaluators: [String : ServerTrustEvaluating] = [
"your.host.com": PinnedCertificatesTrustEvaluator(
acceptSelfSignedCertificates: true,
performDefaultValidation: false,
validateHost: false
)
]
let serverTrustManager = ServerTrustManager(evaluators: evaluators)
Both methods require your certificate to be included in your bundle as .cer or .der file.
After creating your ServerTrustManager pass it to a Session instance and use this for your requests:
let session = Session(serverTrustManager: serverTrustManager)
If you want to have a more complex logic on how you verify your server or you have to use wildcards in your domain you have to subclass ServerTrustManager and override serverTrustEvaluator(forHost:) function:
class MyServerTrustManager: ServerTrustManager {
init() {
super.init(evaluators: [:])
}
override func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? {
guard host.hasSuffix(".host.com") else {
return try super.serverTrustEvaluator(forHost: host)
}
return PublicKeysTrustEvaluator(performDefaultValidation: false, validateHost: false)
}
}
packed private and public key into p12 container with openssl
this was key Alamofire without evaluation and with sending client certificate
created PKCS512() instance and created URLCredential using that
(code in the link above)
in alamofire all you need from the link above is the PKCS12 class
and URLCredential extension
public class PKCS12 {
let label:String?
let keyID:NSData?
let trust:SecTrust?
let certChain:[SecTrust]?
let identity:SecIdentity?
public init(PKCS12Data:NSData,password:String)
{
let importPasswordOption:NSDictionary = [kSecImportExportPassphrase as NSString:password]
var items : CFArray?
let secError:OSStatus = SecPKCS12Import(PKCS12Data, importPasswordOption, &items)
guard secError == errSecSuccess else {
if secError == errSecAuthFailed {
NSLog("ERROR: SecPKCS12Import returned errSecAuthFailed. Incorrect password?")
}
fatalError("SecPKCS12Import returned an error trying to import PKCS12 data")
}
guard let theItemsCFArray = items else { fatalError() }
let theItemsNSArray:NSArray = theItemsCFArray as NSArray
guard let dictArray = theItemsNSArray as? [[String:AnyObject]] else { fatalError() }
func f<T>(key:CFString) -> T? {
for d in dictArray {
if let v = d[key as String] as? T {
return v
}
}
return nil
}
self.label = f(key: kSecImportItemLabel)
self.keyID = f(key: kSecImportItemKeyID)
self.trust = f(key: kSecImportItemTrust)
self.certChain = f(key: kSecImportItemCertChain)
self.identity = f(key: kSecImportItemIdentity)
}
}
extension URLCredential {
public convenience init?(PKCS12 thePKCS12:PKCS12) {
if let identity = thePKCS12.identity {
self.init(
identity: identity,
certificates: thePKCS12.certChain,
persistence: URLCredential.Persistence.forSession)
}
else { return nil }
}
}
fed that credential to authenticate(with: urlCredential)
per Missing sessionDidReceiveChallenge in Alamofire 5 delegate
and that was it.
Hardcoding password into p12 constructor was super ugly. Eww
What's key I guess is base64 baking in p12 into code rather than having it as a file prone to ipa patcher attach.
I have one app with multiple targets (each target is for another client as separate application with different name, bundle identifier etc).
I have method:
fileprivate static func loadSessionFromKeychain() -> UserSession? {
if let sessionData = KeychainWrapper.standard.data(forKey: UserSession.sessionDefaultsKey) {
print("sessionData:")
print(sessionData.debugDescription)
if let session = NSKeyedUnarchiver.unarchiveObject(with: sessionData) as? UserSession {
_current = session
return session
} else {
print("ERROR: Could not parse UserSession from Keychain")
}
return nil
}
return nil
}
The line if let session = NSKeyedUnarchiver.unarchiveObject(with: sessionData) as? UserSession { throws error:
* Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '*
-[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (_test__MyApp.UserSession) for key (root); the class may be
defined in source code or a library that is not linked'
I've tried to catch do {} catch {} but it didn't catch and throws still the same error + xCode says
catch' block is unreachable because no errors are thrown in 'do' block`
Any ideas how to fix this?
UserSessionSwift.swift
import UIKit
import SwiftKeychainWrapper
class UserSession: NSObject, NSCoding {
// Static vars
fileprivate static var _current: UserSession?
static var current: UserSession? {
get {
// If there is already a session return it
if _current != nil {
return _current
}
// If there is no session yet but one is persistently stored return it
if let session = self.persistentLoadCurrentSession() {
_current = session
self.persistentLoadCookies()
return session
}
// Otherwise return nil
return nil
}
set(value) {
// Store the actual value
_current = value
// Perform hooks after changing the current session
if value == nil {
self.afterLogout()
} else {
self.afterLogin()
}
}
}
// Constants
fileprivate static let cookiesDefaultsKey: String = "NSUserDefaultsKeyCookieStorage"
fileprivate static let sessionDefaultsKey: String = "NSUserDefaultsKeyUserSessionStorage"
// Instance properties
let client: Client
// -------------------------------------------------------------------------------
// MARK: - Lifecycle
// -------------------------------------------------------------------------------
required init(client: Client) {
// set local properties
self.client = client
// call super init
super.init()
// Store cookies after a session was initialized
UserSession.persistentStoreCookies()
}
required init?(coder aDecoder: NSCoder) {
self.client = aDecoder.decodeObject(forKey: "client") as! Client
super.init()
}
// -------------------------------------------------------------------------------
// MARK: - Public
// -------------------------------------------------------------------------------
func encode(with aCoder: NSCoder) {
aCoder.encode(self.client, forKey: "client")
}
/**
Performs all necessary operations after user logs in: stores current cookies and user session for the case user stops and reruns the application later
*/
static func afterLogin() {
// Persistently store session data
self.persistentStoreCookies()
self.persistentStoreCurrentSession()
// Register user & device for PUSH notifications
NotificationsManager.registerForNotifications()
}
/**
Performs all necessary operations after user logs out: deletes stored cookies and user session so that the next time the user runs this application he gets the login prompt
*/
static func afterLogout() {
// Erase user session data
self.persistentEraseCookies()
self.persistentEraseCurrentSession()
// Delete all offers from local database
CoreDataHelper.deleteEntitiesInContext(CoreDataHelper.mainContext, entityName: UsedOffer.entityName)
CoreDataHelper.saveContext()
}
static func requestPopup() {
// Get popup from server
print("INFO: Checking for popups on the server...")
ClientPopupRequest.send({ (popup) -> Void in
if let popup = popup {
// If there is one, show it
popup.showAlertAndPerform(in: RootVC.sharedInstance) {
// After the popup performs its action, ask for another one
self.requestPopup()
}
} else {
// If none, exit
print("INFO: No new popups found.")
}
}) { (error) -> Void in
}
}
// -------------------------------------------------------------------------------
// MARK: - Private
// -------------------------------------------------------------------------------
/**
Saves current user session to persistent store (currently NSUserDefaults)
*/
static func persistentStoreCurrentSession() {
if let session = _current {
// Archive session
let sessionData = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWith: sessionData)
archiver.encode(session)
archiver.finishEncoding()
// Session encoded
KeychainWrapper.standard.set(session, forKey: UserSession.sessionDefaultsKey)
// UserDefaults.standard.set(sessionData, forKey: UserSession.sessionDefaultsKey)
// UserDefaults.standard.synchronize()
} else {
print("WARNING: No session to store")
}
}
/**
Tries to load an user session from persistent store (currently NSUserDefaults) and store it as current session in UserSession class. Returns the loaded instance of user session if it succeeds, otherwise returns nil
*/
fileprivate static func persistentLoadCurrentSession() -> UserSession? {
if let keychainData = loadSessionFromKeychain() {
persistentEraseUserDataSession()
return keychainData
} else if let userData = loadSessionFromStore() {
return userData
}
return nil
}
fileprivate static func loadSessionFromKeychain() -> UserSession? {
if let sessionData = KeychainWrapper.standard.data(forKey: UserSession.sessionDefaultsKey) {
print("sessionData:")
print(sessionData.debugDescription)
if let session = NSKeyedUnarchiver.unarchiveObject(with: sessionData) as? UserSession {
_current = session
return session
} else {
print("ERROR: Could not parse UserSession from Keychain")
}
return nil
}
return nil
}
fileprivate static func loadSessionFromStore() -> UserSession? {
if let sessionData = UserDefaults.standard.object(forKey: UserSession.sessionDefaultsKey) as? Data {
let unarchiver = NSKeyedUnarchiver(forReadingWith: sessionData)
if let session = unarchiver.decodeObject() as? UserSession {
unarchiver.finishDecoding()
// Session decoded
_current = session
return session
} else {
print("ERROR: Could not parse UserSession from Store")
}
return nil
}
return nil
}
fileprivate static func persistentEraseCurrentSession() {
// Remove the current session object
_current = nil
// Remove the persisted session object
UserDefaults.standard.removeObject(forKey: UserSession.sessionDefaultsKey)
KeychainWrapper.standard.removeObject(forKey: UserSession.sessionDefaultsKey)
}
fileprivate static func persistentEraseUserDataSession() {
// Remove the persisted session object
UserDefaults.standard.removeObject(forKey: UserSession.sessionDefaultsKey)
}
fileprivate static func persistentStoreCookies() {
if let cookies = HTTPCookieStorage.shared.cookies {
let cookieData = NSKeyedArchiver.archivedData(withRootObject: cookies)
UserDefaults.standard.set(cookieData, forKey: UserSession.sessionDefaultsKey)
KeychainWrapper.standard.set(cookieData, forKey: UserSession.cookiesDefaultsKey)
} else {
print("WARNING: No cookies to store")
}
}
fileprivate static func persistentLoadCookies() {
var cookieData: Data?
if let keychainData = KeychainWrapper.standard.data(forKey: UserSession.cookiesDefaultsKey) {
cookieData = keychainData
} else if let userData = UserDefaults.standard.object(forKey: UserSession.cookiesDefaultsKey) as? Data {
cookieData = userData
}
if (cookieData != nil) {
if let cookies = NSKeyedUnarchiver.unarchiveObject(with: cookieData!) as? [HTTPCookie] {
cookies.forEach { HTTPCookieStorage.shared.setCookie($0) }
} else {
print("ERROR: Could not parse [NSHTTPCookie] from unarchived data")
}
} else {
print("WARNING: No cookies to load")
}
}
fileprivate static func persistentEraseCookies() {
UserDefaults.standard.removeObject(forKey: UserSession.cookiesDefaultsKey)
KeychainWrapper.standard.removeObject(forKey: UserSession.cookiesDefaultsKey)
}
}
// EDIT: added UserSession.swift class
What you're getting here is an exception; exceptions cannot be caught or handled in Swift, and are different from errors, which is why you can't wrap the call in a do {} catch {}.
The issue here is that your archive contains the name of a class which is then not available at runtime, which can happen for several reasons:
You encoded the archive in an app that contains the class, and are attempting to decode in a different app which does not contain the class. This can happen if you forget to link the class implementation with the target you're working with, but this is much less likely in Swift because you can't import the header and forget to link the implementation
The class name has changed. This can happen for a few reasons itself, but in Swift, the most likely reason is due to your app/module name changing. Classes in Swift have runtime names which include the full path to the class. If you've got an app named "MyApp", a class called "Foo" has a qualified name of "MyApp.Foo". Similarly, a class "Bar" nested in "Foo" would have a qualified name of "MyApp.Foo.Bar". Importantly, if you change the name of your app (which is the name of your main module), the name of the class changes!
What's likely happening here is that you've either renamed the target since the archive was written (which would change the class name), or you wrote the archive with the class in one target, but are decoding in another. Even though you include the same class in both, they have different names ("MyTarget1.UserSession" vs. "MyTarget2.UserSession").
You can remedy this with a few steps:
Give the class a stable name which won't change with #objc, e.g. #objc(UserSession) class UserSession { ... }. This will give the class an Objective-C name that is constant and does not depend on the module name in any way
Use NSKeyedUnarchiver.setClass(_:forClassName:) to migrate the old archives to use the new, stable class
See NSKeyedArchiver and sharing a custom class between targets for the full details on how to migrate the archive forward.
Itai's answer is fantastic and I cannot match their explanation. In my case, the main target was looking for a class that existed only in the test target. The solution was to run all tests to completion and the build the main target again. I guess some cleanup hadn't been done the last time I ran my tests.
Hello I'm trying to publish a iOS (SWIFT) personal project in GitHub but I'm afraid of sharing my private API keys and secrets with everybody.
I'm using parse so I have in my AppDelegate something like this:
let applicationId = "mySecretApplicationId"
let clientKey = "mySecretClientKey"
Parse.setApplicationId(applicationId!, clientKey: clientKey!)
I would like to hide "mySecretApplicationId" and "mySecretClientKey", is there private place or directory in my project where I can put this variables?
Thanks!
You can use a .plist file where you store all your important keys. It is very important to put this file into your .gitignore file.
In your case, you need to set your keys.plist file like this:
And use it inside your AppDelegate as follows:
var keys: NSDictionary?
if let path = NSBundle.mainBundle().pathForResource("Keys", ofType: "plist") {
keys = NSDictionary(contentsOfFile: path)
}
if let dict = keys {
let applicationId = dict["parseApplicationId"] as? String
let clientKey = dict["parseClientKey"] as? String
// Initialize Parse.
Parse.setApplicationId(applicationId!, clientKey: clientKey!)
}
SWIFT 3 Update:
if let path = Bundle.main.path(forResource: "Keys", ofType: "plist") {
keys = NSDictionary(contentsOfFile: path)
}
Put them in a configuration file that you add to the .gitignore file. Check in a sample configuration file that every developer can use to create their own configuration.
If you want to share your project without keys then:
Add Keys( as you prefer - enum, struct, or even object/singleton)
struct Keys {
static let sandboxToken = "Tpk_hh43nneu3jwsu3u"
static let productionToken = "pk_b5h4uend8ejwnw8"
}
In your code add follow code:
extension APIManager {
enum Environment {
case sandbox, production
var apiKey: String {
switch self {
case .sandbox:
return Keys.iexSandboxToken // <- Here
case .production:
return Keys.iexProductionToken // <- Here
}
}
}
}
or if you want to deal with optionals then you can add something similar to:
struct Keys {
static let sandboxToken: String? = "Tpk_hh43nneu3jwsu3u"
static let productionToken: String?
}
and on use add assert
var apiKey: String {
switch self {
case .sandbox:
guard let token = Keys.iexSandboxToken else {
assertionFailure("Please fill the tokent in Keys.swift")
return "anything you want"
}
return token
case .production:
guard let token = Keys.iexProductionToken else {
assertionFailure("Please fill the tokent in Keys.swift")
return "anything you want"
}
return token
}
}
So, in production, it will fail.
Add it on .gitignore. So, your keys are hidden.