Skip to content

Instantly share code, notes, and snippets.

@shaps80
Last active January 9, 2021 16:48
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shaps80/beb79331132024f701f664c4b3be46ca to your computer and use it in GitHub Desktop.
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)
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)
"""
}
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
}
}
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)
}
}
@shaps80
Copy link
Author

shaps80 commented Dec 4, 2020

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 method formatted(_:) which allows you to specify compact. This essentially removes trailing 0’s from the version.

E.g. 2.1.0 becomes 2.1

import UIKit

var encoder = JSONEncoder()
let decoder = JSONDecoder()

let agent = UserAgent.shared
print(agent)

let agentData = try encoder.encode(agent)
let decodedAgent = try decoder.decode(UserAgent.self, from: agentData)
print(decodedAgent)

print("--")

let version = Version("123")
print(version.formatted(.compact))

let versionData = try encoder.encode(version)
let decodedVersion = try decoder.decode(Version.self, from: versionData)
print(decodedVersion.formatted(.compact))

print("--")

var v1 = Version(1, 0, 0)
var v2 = Version("2.0.0")
print(v1 < v2)

var v11: Version = "1.1.0"
var v12: Version = "1.2.0"
print(v11 < v12)

var v111: Version = "1.1.1"
var v112: Version = "1.1.2"
print(v111 < v112)

print(v1 < v11)
print(v1 < v112)

var b1: Version = "1.2.3-1"
var b2: Version = "1.2.3-2"

print(b1 < b2)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment