使用Swift强类型访问Info.plist文件

922 阅读2分钟
原文链接: danielemargutti.com

As Apple’s developers we started facing the type-safe constraint in our code with the advent of Swift.

From my side I’ve always tried to fully embrace this approach even if often this means to come to a deal with several parts of the UIKit which are obviously created with a different – more dynamic – paradigm in mind.
Sometimes is easy, some other less, but they still a good excercise to think outside the box to keep our code safer and cleaner.

Recently I’ve re-faced an apparent trivial task; I would to read the configuration of an application saved inside the Info.plist file of the app.

My

Info.plist
  contains an additional node called
configuration
  with several data inside:
server_url
 ,
environment_name
  an a bunch of other keys.

Values are dynamic and assigned based upon the current schema you have set to launch the application (ie.

$(SERVER_URL)
  is the url server which has a value or another depending from the configuration environment like
testing/production
 ).

The goal is to get these values by keeping the type of the data.
The most straightforward approach is to use the magic behind the

Codable
  protocol (this article is not about
Codable
 , you can found tons of articles around like here, here or here).

// A class to read and decode strongly typed values in `plist` files. 
public class PListFile<Value: Codable> {
 
	/// Errors.
	///
	/// - fileNotFound: plist file not exists.
	public enum Errors: Error {
		case fileNotFound
	}
 
	/// Plist file source.
	///
	/// - infoPlist: main bundel's Info.plist file
	/// - plist: other plist file with custom name
	public enum Source {
		case infoPlist(_: Bundle)
		case plist(_: String, _: Bundle)
 
		/// Get the raw data inside given plist file.
		///
		/// - Returns: read data
		/// - Throws: throw an exception if it fails
		internal func data() throws -> Data {
			switch self {
			case .infoPlist(let bundle):
				guard let infoDict = bundle.infoDictionary else {
					throw Errors.fileNotFound
				}
				return try JSONSerialization.data(withJSONObject: infoDict)
			case .plist(let filename, let bundle):
				guard let path = bundle.path(forResource: filename, ofType: "plist") else {
					throw Errors.fileNotFound
				}
				return try Data(contentsOf: URL(fileURLWithPath: path))
			}
		}
	}
 
	/// Data read for file
	public let data: Value
 
	/// Initialize a new Plist parser with given codable structure.
	///
	/// - Parameter file: source of the plist
	/// - Throws: throw an exception if read fails
	public init(_ file: PListFile.Source = .infoPlist(Bundle.main)) throws {
		let rawData = try file.data()
		let decoder = JSONDecoder()
		self.data = try decoder.decode(Value.self, from: rawData)
	}
 
}

The code is pretty simple, you need just specify a

Codable
  structure which can handle your interesting data, then allocate the
PListFile
  class with the generic type.

In our example data we can provide the following structure:

public struct InfoPList: Codable {
 
  public struct Configuration: Codable {
    public let url: URL?
    public let environment: String
  }
  
  public let configuration: Configuration
 
}

Then:

do {
  let appPList = try PListFile<InfoPList>()
 
  // then read values
  let url = appPList.data.configuration.url // it’s an URL
} catch let err {
  print(“Failed to parse data: \(err)”)
}

You can use the class itself to read every other file other than

Info.plist
 ; just pass a
.plist("otherPListFile")
  as init parameter and provide your own
Codable
  structure.