Survival Swift: File IO

Posted by Grego on November 24, 2021

Here’s a quick file I/O example using Swift.

To load a file first we need a URL for the file to load. This will usually point to a local resource, either in your app’s bundle or from your documents folder.

Loading from (app) bundle

Include it in your bundle

First, include the file in your bundle by adding it in the Build Phases for your app’s target, under Copy Bundle Resources.

Add file to bundle in xcode

Then load the file

To load from your App’s bundle (or any other bundle), use Bundle.url(forResource:withExtension:), then initialize a Data object using the Data(contentsOf:) initializer in a do ... catch block:

enum File {
  static func appData(from file: String, in bundle: Bundle = .main) -> Data? {
    guard let path = bundle.url(forResource: file, withExtension: nil) else {
      return nil
    }

    var data: Data?
    do {
      data = try Data(contentsOf: path)
    }
    catch {
      print("Error reading data: \(error)")
    }

    return data
  }
}

Loading from a local file

To load from a local file we can use FileManager:

/// find user's home folder
private static var homeFolder: URL? {
  FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
}

static func data(from file: String) -> Data? {
  guard let path = homeFolder?.appendingPathComponent(file) else {
    return nil
  }

  var data: Data?
  do {
    data = try Data(contentsOf: path)
  }
  catch {
    print("Error reading data: \(error)")
  }

  return data
}

Converting data

Now we can convert the data.

To string

Use String(data:encoding:):

func loadTextFile() {
  let data = File.appData(from: File.Sample.text)!
  let string = String(data: data, encoding: .utf8)

  print(string)
}

Decoding JSON, or other Decodable types:

Take the data from the file load and decode it using the appropriate decoder:

private static func decode<T: Decodable>(data: Data) -> T? {
  do {
    return try JSONDecoder().decode(T.self, from: data)
  }
  catch {
    print("Error decoding file: \(error)")
  }
  return nil
}

Here I swallow exceptions and return an optional to make a simple example, cater to your own needs.

Writing a file

You can write files using Data.write(to:). You can Example generic function to write to a json file:

func write<T: Encodable>(_ object: T, toJson file: String) {
  guard let path = File.homeFolder?.appendingPathComponent(file) else {
    return
  }

  do {
    let data = try JSONEncoder().encode(object)
    try data.write(to: path)
  }
  catch {
    print("Could not write file \(path): \(error)")
  }
}

Another approach could be to extend Encodable and implement a new write(toJson:) method:

extension Encodable {
  /// writes object to json file in home folder
  func write(toJson file: String) {
    guard let path = File.homeFolder?.appendingPathComponent(file) else {
      return
    }

    do {
      let data = try JSONEncoder().encode(self)
      try data.write(to: path)
    }
    catch {
      print("Could not write file \(path): \(error)")
    }
  }
}