Last active
January 9, 2021 16:48
-
-
Save shaps80/beb79331132024f701f664c4b3be46ca to your computer and use it in GitHub Desktop.
Swift type for representing a UserAgent (includes an implementation similar of Apple’s Version from SPM)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
extension UIDevice { | |
/* | |
List can be updated here: | |
https://gist.github.com/adamawolf/3048717 | |
*/ | |
internal static var models: String = """ | |
i386 : iPhone Simulator | |
x86_64 : iPhone Simulator | |
iPhone1,1 : iPhone | |
iPhone1,2 : iPhone 3G | |
iPhone2,1 : iPhone 3GS | |
iPhone3,1 : iPhone 4 | |
iPhone3,2 : iPhone 4 GSM Rev A | |
iPhone3,3 : iPhone 4 CDMA | |
iPhone4,1 : iPhone 4S | |
iPhone5,1 : iPhone 5 (GSM) | |
iPhone5,2 : iPhone 5 (GSM+CDMA) | |
iPhone5,3 : iPhone 5C (GSM) | |
iPhone5,4 : iPhone 5C (Global) | |
iPhone6,1 : iPhone 5S (GSM) | |
iPhone6,2 : iPhone 5S (Global) | |
iPhone7,1 : iPhone 6 Plus | |
iPhone7,2 : iPhone 6 | |
iPhone8,1 : iPhone 6s | |
iPhone8,2 : iPhone 6s Plus | |
iPhone8,4 : iPhone SE (GSM) | |
iPhone9,1 : iPhone 7 | |
iPhone9,2 : iPhone 7 Plus | |
iPhone9,3 : iPhone 7 | |
iPhone9,4 : iPhone 7 Plus | |
iPhone10,1 : iPhone 8 | |
iPhone10,2 : iPhone 8 Plus | |
iPhone10,3 : iPhone X Global | |
iPhone10,4 : iPhone 8 | |
iPhone10,5 : iPhone 8 Plus | |
iPhone10,6 : iPhone X GSM | |
iPhone11,2 : iPhone XS | |
iPhone11,4 : iPhone XS Max | |
iPhone11,6 : iPhone XS Max Global | |
iPhone11,8 : iPhone XR | |
iPhone12,1 : iPhone 11 | |
iPhone12,3 : iPhone 11 Pro | |
iPhone12,5 : iPhone 11 Pro Max | |
iPhone12,8 : iPhone SE 2nd Gen | |
iPhone13,1 : iPhone 12 Mini | |
iPhone13,2 : iPhone 12 | |
iPhone13,3 : iPhone 12 Pro | |
iPhone13,4 : iPhone 12 Pro Max | |
iPod1,1 : 1st Gen iPod | |
iPod2,1 : 2nd Gen iPod | |
iPod3,1 : 3rd Gen iPod | |
iPod4,1 : 4th Gen iPod | |
iPod5,1 : 5th Gen iPod | |
iPod7,1 : 6th Gen iPod | |
iPod9,1 : 7th Gen iPod | |
iPad1,1 : iPad | |
iPad1,2 : iPad 3G | |
iPad2,1 : 2nd Gen iPad | |
iPad2,2 : 2nd Gen iPad GSM | |
iPad2,3 : 2nd Gen iPad CDMA | |
iPad2,4 : 2nd Gen iPad New Revision | |
iPad3,1 : 3rd Gen iPad | |
iPad3,2 : 3rd Gen iPad CDMA | |
iPad3,3 : 3rd Gen iPad GSM | |
iPad2,5 : iPad mini | |
iPad2,6 : iPad mini GSM+LTE | |
iPad2,7 : iPad mini CDMA+LTE | |
iPad3,4 : 4th Gen iPad | |
iPad3,5 : 4th Gen iPad GSM+LTE | |
iPad3,6 : 4th Gen iPad CDMA+LTE | |
iPad4,1 : iPad Air (WiFi) | |
iPad4,2 : iPad Air (GSM+CDMA) | |
iPad4,3 : 1st Gen iPad Air (China) | |
iPad4,4 : iPad mini Retina (WiFi) | |
iPad4,5 : iPad mini Retina (GSM+CDMA) | |
iPad4,6 : iPad mini Retina (China) | |
iPad4,7 : iPad mini 3 (WiFi) | |
iPad4,8 : iPad mini 3 (GSM+CDMA) | |
iPad4,9 : iPad Mini 3 (China) | |
iPad5,1 : iPad mini 4 (WiFi) | |
iPad5,2 : 4th Gen iPad mini (WiFi+Cellular) | |
iPad5,3 : iPad Air 2 (WiFi) | |
iPad5,4 : iPad Air 2 (Cellular) | |
iPad6,3 : iPad Pro (9.7 inch, WiFi) | |
iPad6,4 : iPad Pro (9.7 inch, WiFi+LTE) | |
iPad6,7 : iPad Pro (12.9 inch, WiFi) | |
iPad6,8 : iPad Pro (12.9 inch, WiFi+LTE) | |
iPad6,11 : iPad (2017) | |
iPad6,12 : iPad (2017) | |
iPad7,1 : iPad Pro 2nd Gen (WiFi) | |
iPad7,2 : iPad Pro 2nd Gen (WiFi+Cellular) | |
iPad7,3 : iPad Pro 10.5-inch | |
iPad7,4 : iPad Pro 10.5-inch | |
iPad7,5 : iPad 6th Gen (WiFi) | |
iPad7,6 : iPad 6th Gen (WiFi+Cellular) | |
iPad7,11 : iPad 7th Gen 10.2-inch (WiFi) | |
iPad7,12 : iPad 7th Gen 10.2-inch (WiFi+Cellular) | |
iPad8,1 : iPad Pro 11 inch 3rd Gen (WiFi) | |
iPad8,2 : iPad Pro 11 inch 3rd Gen (1TB, WiFi) | |
iPad8,3 : iPad Pro 11 inch 3rd Gen (WiFi+Cellular) | |
iPad8,4 : iPad Pro 11 inch 3rd Gen (1TB, WiFi+Cellular) | |
iPad8,5 : iPad Pro 12.9 inch 3rd Gen (WiFi) | |
iPad8,6 : iPad Pro 12.9 inch 3rd Gen (1TB, WiFi) | |
iPad8,7 : iPad Pro 12.9 inch 3rd Gen (WiFi+Cellular) | |
iPad8,8 : iPad Pro 12.9 inch 3rd Gen (1TB, WiFi+Cellular) | |
iPad8,9 : iPad Pro 11 inch 4th Gen (WiFi) | |
iPad8,10 : iPad Pro 11 inch 4th Gen (WiFi+Cellular) | |
iPad8,11 : iPad Pro 12.9 inch 4th Gen (WiFi) | |
iPad8,12 : iPad Pro 12.9 inch 4th Gen (WiFi+Cellular) | |
iPad11,1 : iPad mini 5th Gen (WiFi) | |
iPad11,2 : iPad mini 5th Gen | |
iPad11,3 : iPad Air 3rd Gen (WiFi) | |
iPad11,4 : iPad Air 3rd Gen | |
iPad11,6 : iPad 8th Gen (WiFi) | |
iPad11,7 : iPad 8th Gen (WiFi+Cellular) | |
iPad13,1 : iPad air 4th Gen (WiFi) | |
iPad13,2 : iPad air 4th Gen (WiFi+Celular) | |
Watch1,1 : Apple Watch 38mm case | |
Watch1,2 : Apple Watch 42mm case | |
Watch2,6 : Apple Watch Series 1 38mm case | |
Watch2,7 : Apple Watch Series 1 42mm case | |
Watch2,3 : Apple Watch Series 2 38mm case | |
Watch2,4 : Apple Watch Series 2 42mm case | |
Watch3,1 : Apple Watch Series 3 38mm case (GPS+Cellular) | |
Watch3,2 : Apple Watch Series 3 42mm case (GPS+Cellular) | |
Watch3,3 : Apple Watch Series 3 38mm case (GPS) | |
Watch3,4 : Apple Watch Series 3 42mm case (GPS) | |
Watch4,1 : Apple Watch Series 4 40mm case (GPS) | |
Watch4,2 : Apple Watch Series 4 44mm case (GPS) | |
Watch4,3 : Apple Watch Series 4 40mm case (GPS+Cellular) | |
Watch4,4 : Apple Watch Series 4 44mm case (GPS+Cellular) | |
Watch5,1 : Apple Watch Series 5 40mm case (GPS) | |
Watch5,2 : Apple Watch Series 5 44mm case (GPS) | |
Watch5,3 : Apple Watch Series 5 40mm case (GPS+Cellular) | |
Watch5,4 : Apple Watch Series 5 44mm case (GPS+Cellular) | |
""" | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
/// Defines a structure for representing a user-agent. You can | |
public struct UserAgent: Codable, Equatable { | |
public static let shared: UserAgent = { | |
UserAgent( | |
productName: Bundle.main.productName, | |
model: UIDevice.current.model, | |
appVersion: Bundle.main.version, | |
osVersion: Version(UIDevice.current.systemVersion) ?? Version(0, 0, 0), | |
networkVersion: Bundle.main.networkVersion, | |
kernelVersion: UIDevice.current.kernel | |
) | |
}() | |
/// The name of the product. E.g. Notes | |
private var productName: String | |
/// The model name for this device. E.g. iPhone 8 Plus | |
private var model: String | |
/// The app versions and build. E.g. 2.1.13-3 | |
private var appVersion: Version | |
/// The OS version. E.g. 14.2 | |
private var osVersion: Version | |
/// The CFNetwork version. E.g. 1206 | |
private var networkVersion: Version | |
/// The Darwin kernel version. E.g. 20.1 | |
private var kernelVersion: Version | |
} | |
extension UserAgent: CustomStringConvertible { | |
public var description: String { | |
// Assuming app version: 2.1.13-1 | |
// "App name/2.1.13-1 (iPhone) iOS/14.2 CFNetwork/1206 Darwin/20.1.0" | |
"\(productName)/\(appVersion.formatted(.compact)) (\(model)) os/\(osVersion.formatted(.compact)) CFNetwork/\(networkVersion.formatted(.compact)) Darwin/\(kernelVersion.formatted(.compact))" | |
} | |
} | |
private extension Bundle { | |
var productName: String { | |
return infoDictionary?["CFBundleName"] as! String | |
} | |
var build: String { | |
return infoDictionary?["CFBundleVersion"] as! String | |
} | |
var version: Version { | |
let string = infoDictionary?["CFBundleShortVersionString"] as! String | |
return Version(string) ?? Version(0, 0, 0) | |
} | |
var networkVersion: Version { | |
let version = Bundle(identifier: "com.apple.CFNetwork")? | |
.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" | |
return Version(stringLiteral: version) | |
} | |
} | |
private extension UIDevice { | |
var kernel: Version { | |
var sysinfo = utsname() | |
uname(&sysinfo) | |
if let darwinVersion = String( | |
bytes: Data( | |
bytes: &sysinfo.release, | |
count: Int(_SYS_NAMELEN) | |
), | |
encoding: .ascii | |
)?.trimmingCharacters(in: .controlCharacters) { | |
return Version(darwinVersion) ?? Version(0, 0, 0) | |
} else { | |
return Version(0, 0, 0) | |
} | |
} | |
var model: String { | |
var sysinfo = utsname() | |
uname(&sysinfo) | |
guard let modelId = String( | |
bytes: Data( | |
bytes: &sysinfo.machine, | |
count: Int(_SYS_NAMELEN) | |
), | |
encoding: .ascii | |
)?.trimmingCharacters(in: .controlCharacters) else { | |
return "Unknown Device" | |
} | |
return name(for: modelId) | |
} | |
func name(for model: String) -> String { | |
var name = model | |
UIDevice.models.enumerateLines { line, stop in | |
let components = line.components(separatedBy: " : ") | |
guard components[0] == model else { return } | |
name = components[1] | |
stop = true | |
} | |
return name | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
/// A version according to the semantic versioning specification. | |
/// | |
/// A package version is a three period-separated integer, for example `1.0.0`. It must conform to the semantic versioning standard in order to ensure | |
/// that your package behaves in a predictable manner once developers update their | |
/// package dependency to a newer version. To achieve predictability, the semantic versioning specification proposes a set of rules and | |
/// requirements that dictate how version numbers are assigned and incremented. To learn more about the semantic versioning specification, visit | |
/// [semver.org](www.semver.org). | |
/// | |
/// **The Major Version** | |
/// | |
/// The first digit of a version, or *major version*, signifies breaking changes to the API that require | |
/// updates to existing clients. For example, the semantic versioning specification | |
/// considers renaming an existing type, removing a method, or changing a method's signature | |
/// breaking changes. This also includes any backward-incompatible bug fixes or | |
/// behavioral changes of the existing API. | |
/// | |
/// **The Minor Version** | |
/// | |
/// Update the second digit of a version, or *minor version*, if you add functionality in a backward-compatible manner. | |
/// For example, the semantic versioning specification considers adding a new method | |
/// or type without changing any other API to be backward-compatible. | |
/// | |
/// **The Patch Version** | |
/// | |
/// Increase the third digit of a version, or *patch version*, if you are making a backward-compatible bug fix. | |
/// This allows clients to benefit from bugfixes to your package without incurring | |
/// any maintenance burden. | |
public struct Version: Comparable { | |
/// The major version according to the semantic versioning standard. | |
public let major: Int | |
/// The minor version according to the semantic versioning standard. | |
public let minor: Int | |
/// The patch version according to the semantic versioning standard. | |
public let patch: Int | |
/// The pre-release identifier according to the semantic versioning standard, such as `-beta.1`. | |
public let prereleaseIdentifiers: [String] | |
/// The build metadata of this version according to the semantic versioning standard, such as a commit hash. | |
public let buildMetadataIdentifiers: [String] | |
private var isPrerelease: Bool { | |
!self.prereleaseIdentifiers.isEmpty | |
} | |
/// Initializes and returns a newly allocated version struct | |
/// for the provided components of a semantic version. | |
/// | |
/// - Parameters: | |
/// - major: The major version numner. | |
/// - minor: The minor version number. | |
/// - patch: The patch version number. | |
/// - prereleaseIdentifiers: The pre-release identifier. | |
/// - buildMetaDataIdentifiers: Build metadata that identifies a build. | |
public init(_ major: Int, _ minor: Int, _ patch: Int, prereleaseIdentifiers: [String] = [], buildMetadataIdentifiers: [String] = []) { | |
precondition(major >= 0 && minor >= 0 && patch >= 0, "Negative versioning is invalid.") | |
self.major = major | |
self.minor = minor | |
self.patch = patch | |
self.prereleaseIdentifiers = prereleaseIdentifiers | |
self.buildMetadataIdentifiers = buildMetadataIdentifiers | |
} | |
public static func < (lhs: Self, rhs: Self) -> Bool { | |
let lhsComparators = [lhs.major, lhs.minor, lhs.patch] | |
let rhsComparators = [rhs.major, rhs.minor, rhs.patch] | |
if lhsComparators != rhsComparators { | |
return lhsComparators.lexicographicallyPrecedes(rhsComparators) | |
} | |
guard lhs.prereleaseIdentifiers.count > 0 else { | |
return false // Non-prerelease lhs >= potentially prerelease rhs | |
} | |
guard rhs.prereleaseIdentifiers.count > 0 else { | |
return true // Prerelease lhs < non-prerelease rhs | |
} | |
let zippedIdentifiers = zip(lhs.prereleaseIdentifiers, rhs.prereleaseIdentifiers) | |
for (lhsPrereleaseIdentifier, rhsPrereleaseIdentifier) in zippedIdentifiers { | |
if lhsPrereleaseIdentifier == rhsPrereleaseIdentifier { | |
continue | |
} | |
let typedLhsIdentifier: Any = Int(lhsPrereleaseIdentifier) ?? lhsPrereleaseIdentifier | |
let typedRhsIdentifier: Any = Int(rhsPrereleaseIdentifier) ?? rhsPrereleaseIdentifier | |
switch (typedLhsIdentifier, typedRhsIdentifier) { | |
case let (int1 as Int, int2 as Int): return int1 < int2 | |
case let (string1 as String, string2 as String): return string1 < string2 | |
case (is Int, is String): return true // Int prereleases < String prereleases | |
case (is String, is Int): return false | |
default: | |
return false | |
} | |
} | |
return lhs.prereleaseIdentifiers.count < rhs.prereleaseIdentifiers.count | |
} | |
} | |
extension Version: ExpressibleByStringLiteral { | |
/// Initializes and returns a newly allocated version struct for the provided string literal. | |
/// | |
/// - Parameters: | |
/// - version: A string literal to use for creating a new version object. | |
public init(stringLiteral value: String) { | |
guard let version = Version(value) else { | |
self.init(0, 0, 0) | |
return | |
} | |
self.init(version) | |
} | |
/// Initializes a version struct with the provided version. | |
/// | |
/// - Parameters: | |
/// - version: A version object to use for creating a new version struct. | |
public init(_ version: Version) { | |
major = version.major | |
minor = version.minor | |
patch = version.patch | |
prereleaseIdentifiers = version.prereleaseIdentifiers | |
buildMetadataIdentifiers = version.buildMetadataIdentifiers | |
} | |
/// Initializes and returns a newly allocated version struct for the provided version string. | |
/// | |
/// - Parameters: | |
/// - version: A version string to use for creating a new version object. | |
public init?(_ versionString: String) { | |
let prereleaseStartIndex = versionString.firstIndex(of: "-") | |
let metadataStartIndex = versionString.firstIndex(of: "+") | |
let requiredEndIndex = prereleaseStartIndex ?? metadataStartIndex ?? versionString.endIndex | |
let requiredCharacters = versionString.prefix(upTo: requiredEndIndex) | |
var requiredComponents = requiredCharacters | |
.split(separator: ".", maxSplits: 2, omittingEmptySubsequences: false) | |
.compactMap(String.init) | |
.compactMap { Int($0) } | |
.filter { $0 >= 0 } | |
guard requiredComponents.count > 0 else { return nil } | |
requiredComponents.reverse() | |
self.major = requiredComponents.popLast() ?? 0 | |
self.minor = requiredComponents.popLast() ?? 0 | |
self.patch = requiredComponents.popLast() ?? 0 | |
func identifiers(start: String.Index?, end: String.Index) -> [String] { | |
guard let start = start else { return [] } | |
let identifiers = versionString[versionString.index(after: start)..<end] | |
return identifiers.split(separator: ".").map(String.init) | |
} | |
self.prereleaseIdentifiers = identifiers( | |
start: prereleaseStartIndex, | |
end: metadataStartIndex ?? versionString.endIndex) | |
self.buildMetadataIdentifiers = identifiers(start: metadataStartIndex, end: versionString.endIndex) | |
} | |
} | |
extension Version: CustomStringConvertible { | |
public enum Format { | |
case full | |
case compact | |
} | |
public func formatted(_ format: Format) -> String { | |
var base: String | |
switch format { | |
case .full: | |
base = [major, minor, patch].lazy | |
.map { "\($0)" } | |
.joined(separator: ".") | |
case .compact: | |
base = [major, minor, patch].lazy | |
.filter { $0 != 0 } | |
.map { "\($0)" } | |
.joined(separator: ".") | |
} | |
if !prereleaseIdentifiers.isEmpty { | |
base += "-" + prereleaseIdentifiers.joined(separator: ".") | |
} | |
if !buildMetadataIdentifiers.isEmpty { | |
base += "+" + buildMetadataIdentifiers.joined(separator: ".") | |
} | |
return base.isEmpty ? "0" : base | |
} | |
public var description: String { | |
formatted(.full) | |
} | |
} | |
extension Version: Codable { | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
let string = try container.decode(String.self) | |
if let version = Version(string) { | |
self.init(version) | |
} else { | |
self.init(0, 0, 0) | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
try container.encode(description) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The following is some simple test code I wrote in a Swift Playground.
Note the
Version
implementation is almost identification to Apple’s own implementation. However it also adds a methodformatted(_:)
which allows you to specifycompact
. This essentially removes trailing0
’s from the version.E.g.
2.1.0
becomes2.1