2

Developing an iOS application that connects to an api server, I'd like the app to connect to the local development api server at http://localhost:3000 if the local server is running. Otherwise, the app should connect to the production server, for example at https://example.com.

How can I check if the local api server is running?

This is only necessary in the iOS simulator, not in the real app.

fiedl
  • 5,667
  • 4
  • 44
  • 57
  • send a request and check for an error code? – john elemans Aug 18 '16 at 23:12
  • Are you trying to achieve a development/production configuration? You should have two independent (or even 3) builds for each case and you can achieve this by creating different targets and loading your backend configuration based on the build. – Daniel Ormeño Aug 18 '16 at 23:25
  • Thanks, @DanielOrmeño! I'll have a look into that. I'm brand new to swift/xcode/iOS :) — So, is there something like an environment variable I can set for the different targets and then read out in code? – fiedl Aug 18 '16 at 23:28
  • No problem, I'll provide you with some info in an answer. – Daniel Ormeño Aug 18 '16 at 23:28

2 Answers2

1

If you are developing an app and you are organizing your code for production, you should aim to having at least two different builds. Ideally you would have up to 3, one for development, one for staging and one for production.

Form within your XCode project you can configure multiple build targets which can then be associated to different apps in iTunes Connect, that way you can always be sure that your changes are targeting the development build.

You really don't want your production application to be checking for your dev server API as you don't want your users to be hitting that serer while you work on updates.

This is a good starting point to getting your targets set up, you can get that up and running in no time. With these different targets, you can then configure a new info.plist for each build and its always a good idea to also have a different app icon that lets you know you are testing or working with the different targets.

You can then have as many configuration files as you need for each target, a config file could look like this:

Config.plist (in xml)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "...">
<plist version="1.0">
    <dict>
        <key>ClientId</key>
        <string>{SomeIdValue}</string>
        <key>ClientSecret</key>
        <string>{ClientSecret}</string>
        <key>ServerUrl</key>
        <string>{my.target.apiEndpoint}</string>
    </dict>
</plist>

You can then have a provider that serves your application with the right API end point configuration, as so (Using config.plist for production and configDebub.plist for debug):

struct ConfigProvider {

#if DEVELOPMENT
let name: String = "configDebug"
#else
let name: String = "config"
#endif

enum ConfigError: ErrorType {
    case FileNotWritten
    case FileDoesNotExist
}

var sourcePath:String? {
    guard let path = NSBundle.mainBundle().pathForResource(name, ofType: "plist") else { return .None }
    return path
}

var destPath:String? {
    guard sourcePath != .None else { return .None }
    let dir = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
    return (dir as NSString).stringByAppendingPathComponent("\(name).plist")
}

init?() {
    let fileManager = NSFileManager.defaultManager()

    guard let source = sourcePath else { return nil }
    guard let destination = destPath else { return nil }
    guard fileManager.fileExistsAtPath(source) else { return nil }

    if !fileManager.fileExistsAtPath(destination) {
        do {
            try fileManager.copyItemAtPath(source, toPath: destination)
        } catch let error as NSError {
            print("Unable to copy file. ERROR: \(error.localizedDescription)")
            return nil
        }
    }
}

func getConfig() -> NSDictionary?{
    let fileManager = NSFileManager.defaultManager()
    if fileManager.fileExistsAtPath(destPath!) {
        guard let dict = NSDictionary(contentsOfFile: destPath!) else { return .None }
        return dict
    } else {
        return .None
    }
}

func getServerUrl() -> String {
    if let c = getConfig()?["ServerUrl"] {
        return c as! String
    }
    // Handle error
}

func getClientSecret() -> String {
    if let c = getConfig()?["ClientSecret"] {
        return c as! String
    }
    // Handle error
}

func getClientId() -> String {
    if let c = getConfig()?["ClientId"] {
        return c as! String
    }
    // Handle error
}

func getMutablePlistFile() -> NSMutableDictionary?{
    let fileManager = NSFileManager.defaultManager()
    if fileManager.fileExistsAtPath(destPath!) {
        guard let dict = NSMutableDictionary(contentsOfFile: destPath!)     else { return .None }
            return dict
        } else {
        return .None
        }
    }
}

Then you can configuration details as:

private static let config = ConfigProvider()!
private let clientId = config.getClientId()
private let clientSecret = config.getClientSecret()
private let serverUrl = config.getServerUrl()

Hope this helps!

Daniel Ormeño
  • 2,743
  • 2
  • 25
  • 30
0

Suppose, at some point in the startup process, visitEntryPointUrl() is called.

  func visitEntryPointUrl() {
    if Device.isSimulator {
      visitLocalServerIfRunningAndProductionServerOtherwiese()
    } else {
      visit(self.productionEntryPointUrl)
    }
  }

Check if it's the simulator

// Device.swift
import Foundation
struct Device {
  // https://stackoverflow.com/a/30284266/2066546
  static var isSimulator: Bool {
    return TARGET_OS_SIMULATOR != 0
  }
}

I've taken this trick from https://stackoverflow.com/a/30284266/2066546.

Check if the local server is running

  func visitLocalServerIfRunningAndProductionServerOtherwiese() {
    let testSessionConfiguration = 
        NSURLSessionConfiguration.defaultSessionConfiguration()
    testSessionConfiguration.timeoutIntervalForRequest =
        NSTimeInterval(1) // seconds
    testSessionConfiguration.timeoutIntervalForResource =
        NSTimeInterval(1) // seconds
    let testSession = NSURLSession(configuration: testSessionConfiguration)
    let task = testSession.dataTaskWithURL(self.developmentEntryPointUrl) { 
        (data, response, error) -> Void in

      // https://stackoverflow.com/a/28321213/2066546,   
      // https://stackoverflow.com/a/33715865/2066546
      dispatch_async(dispatch_get_main_queue()) { 
        if data != nil {
          print("local server running. connecting to \(self.developmentEntryPointUrl)")
          self.visit(self.developmentEntryPointUrl)
        } else {
          print("server not running. connecting to \(self.productionEntryPointUrl)")
          self.visit(self.productionEntryPointUrl)
        }
      }
    }
    task.resume()
  }

References

Community
  • 1
  • 1
fiedl
  • 5,667
  • 4
  • 44
  • 57