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.