mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Add Xcode Releases data source
This commit is contained in:
parent
b342baff02
commit
293aef80e3
9 changed files with 237 additions and 25 deletions
|
|
@ -18,6 +18,9 @@
|
|||
CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF84D2595079F00E47BAF /* ScrollingTextView.swift */; };
|
||||
CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8512595080100E47BAF /* AcknowledgementsView.swift */; };
|
||||
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8652595130600E47BAF /* View+IsHidden.swift */; };
|
||||
CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */ = {isa = PBXBuildFile; productRef = CA9FF86C25951C6E00E47BAF /* XCModel */; };
|
||||
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF876259528CC00E47BAF /* Version+XcodeReleases.swift */; };
|
||||
CA9FF87B2595293E00E47BAF /* DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF87A2595293E00E47BAF /* DataSource.swift */; };
|
||||
CAA1CB2D255A5262003FD669 /* AppleAPI in Frameworks */ = {isa = PBXBuildFile; productRef = CAA1CB2C255A5262003FD669 /* AppleAPI */; };
|
||||
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */; };
|
||||
CAA1CB45255A5B60003FD669 /* SignIn2FAView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */; };
|
||||
|
|
@ -82,6 +85,8 @@
|
|||
CA9FF84D2595079F00E47BAF /* ScrollingTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingTextView.swift; sourceTree = "<group>"; };
|
||||
CA9FF8512595080100E47BAF /* AcknowledgementsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementsView.swift; sourceTree = "<group>"; };
|
||||
CA9FF8652595130600E47BAF /* View+IsHidden.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+IsHidden.swift"; sourceTree = "<group>"; };
|
||||
CA9FF876259528CC00E47BAF /* Version+XcodeReleases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Version+XcodeReleases.swift"; sourceTree = "<group>"; };
|
||||
CA9FF87A2595293E00E47BAF /* DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSource.swift; sourceTree = "<group>"; };
|
||||
CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInCredentialsView.swift; sourceTree = "<group>"; };
|
||||
CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignIn2FAView.swift; sourceTree = "<group>"; };
|
||||
CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSMSView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -128,6 +133,7 @@
|
|||
CABFA9E42592F08E00380FEE /* Version in Frameworks */,
|
||||
CABFA9E92592F0B400380FEE /* PromiseKit in Frameworks */,
|
||||
CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */,
|
||||
CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */,
|
||||
CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */,
|
||||
CABFA9F32592F0E400380FEE /* PMKFoundation in Frameworks */,
|
||||
CAA1CB2D255A5262003FD669 /* AppleAPI in Frameworks */,
|
||||
|
|
@ -201,6 +207,7 @@
|
|||
CA378F982466567600A58CE0 /* AppState.swift */,
|
||||
CABFAA2B2592FBFC00380FEE /* Configure.swift */,
|
||||
CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */,
|
||||
CA9FF87A2595293E00E47BAF /* DataSource.swift */,
|
||||
CABFA9BA2592EEEA00380FEE /* DateFormatter+.swift */,
|
||||
CABFA9B22592EEEA00380FEE /* Entry+.swift */,
|
||||
CABFA9A92592EEE900380FEE /* Environment.swift */,
|
||||
|
|
@ -213,6 +220,7 @@
|
|||
CABFA9AB2592EEE900380FEE /* URLRequest+Apple.swift */,
|
||||
CABFA9B32592EEEA00380FEE /* URLSession+Promise.swift */,
|
||||
CABFA9A82592EEE900380FEE /* Version+.swift */,
|
||||
CA9FF876259528CC00E47BAF /* Version+XcodeReleases.swift */,
|
||||
CABFA9A62592EEE900380FEE /* Version+Xcode.swift */,
|
||||
CABFA9A72592EEE900380FEE /* XcodeList.swift */,
|
||||
);
|
||||
|
|
@ -326,6 +334,7 @@
|
|||
CABFA9F22592F0E400380FEE /* PMKFoundation */,
|
||||
CABFA9F72592F0F900380FEE /* KeychainAccess */,
|
||||
CABFA9FC2592F13300380FEE /* LegibleError */,
|
||||
CA9FF86C25951C6E00E47BAF /* XCModel */,
|
||||
);
|
||||
productName = XcodesMac;
|
||||
productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */;
|
||||
|
|
@ -385,6 +394,7 @@
|
|||
CABFA9F12592F0E400380FEE /* XCRemoteSwiftPackageReference "Foundation" */,
|
||||
CABFA9F62592F0F900380FEE /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
||||
CABFA9FB2592F13300380FEE /* XCRemoteSwiftPackageReference "LegibleError" */,
|
||||
CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */,
|
||||
);
|
||||
productRefGroup = CAD2E79F2449574E00113D76 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
|
@ -462,6 +472,7 @@
|
|||
CABFA9CE2592EEEA00380FEE /* Version+Xcode.swift in Sources */,
|
||||
CAA1CB49255A5C97003FD669 /* SignInSMSView.swift in Sources */,
|
||||
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */,
|
||||
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */,
|
||||
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */,
|
||||
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */,
|
||||
CABFA9C22592EEEA00380FEE /* Promise+.swift in Sources */,
|
||||
|
|
@ -469,6 +480,7 @@
|
|||
CABFA9CF2592EEEA00380FEE /* Process.swift in Sources */,
|
||||
CABFA9C72592EEEA00380FEE /* Entry+.swift in Sources */,
|
||||
CABFAA2C2592FBFC00380FEE /* SettingsView.swift in Sources */,
|
||||
CA9FF87B2595293E00E47BAF /* DataSource.swift in Sources */,
|
||||
CABFA9C92592EEEA00380FEE /* URLRequest+Apple.swift in Sources */,
|
||||
CABFAA432593104F00380FEE /* AboutView.swift in Sources */,
|
||||
CABFA9CC2592EEEA00380FEE /* Path+.swift in Sources */,
|
||||
|
|
@ -840,6 +852,14 @@
|
|||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/xcodereleases/data";
|
||||
requirement = {
|
||||
kind = revision;
|
||||
revision = b47228c688b608e34b3b84079ab6052a24c7a981;
|
||||
};
|
||||
};
|
||||
CABFA9DD2592F07A00380FEE /* XCRemoteSwiftPackageReference "Path" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/mxcl/Path.swift";
|
||||
|
|
@ -899,6 +919,11 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
CA9FF86C25951C6E00E47BAF /* XCModel */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */;
|
||||
productName = XCModel;
|
||||
};
|
||||
CAA1CB2C255A5262003FD669 /* AppleAPI */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = AppleAPI;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "XcodeReleases",
|
||||
"repositoryURL": "https://github.com/xcodereleases/data",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b47228c688b608e34b3b84079ab6052a24c7a981",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "PMKFoundation",
|
||||
"repositoryURL": "https://github.com/PromiseKit/Foundation",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import Combine
|
|||
import Path
|
||||
import LegibleError
|
||||
import KeychainAccess
|
||||
import SwiftUI
|
||||
|
||||
class AppState: ObservableObject {
|
||||
private let list = XcodeList()
|
||||
|
|
@ -20,6 +21,10 @@ class AppState: ObservableObject {
|
|||
@Published var isProcessingAuthRequest = false
|
||||
@Published var secondFactorData: SecondFactorData?
|
||||
|
||||
private var dataSource: DataSource {
|
||||
Current.defaults.string(forKey: "dataSource").flatMap(DataSource.init(rawValue:)) ?? .default
|
||||
}
|
||||
|
||||
// MARK: - Authentication
|
||||
|
||||
func validateSession() -> AnyPublisher<Void, Error> {
|
||||
|
|
@ -185,8 +190,8 @@ class AppState: ObservableObject {
|
|||
|
||||
private func update() -> AnyPublisher<[Xcode], Never> {
|
||||
signInIfNeeded()
|
||||
.flatMap {
|
||||
self.list.update()
|
||||
.flatMap { [unowned self] in
|
||||
self.list.update(dataSource: self.dataSource)
|
||||
}
|
||||
.handleEvents(
|
||||
receiveCompletion: { completion in
|
||||
|
|
|
|||
17
Xcodes/Backend/DataSource.swift
Normal file
17
Xcodes/Backend/DataSource.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import Foundation
|
||||
|
||||
public enum DataSource: String, CaseIterable, Identifiable, CustomStringConvertible {
|
||||
case apple
|
||||
case xcodeReleases
|
||||
|
||||
public var id: Self { self }
|
||||
|
||||
public static var `default` = DataSource.xcodeReleases
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .apple: return "Apple"
|
||||
case .xcodeReleases: return "Xcode Releases"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,13 +19,13 @@ public extension Version {
|
|||
return major == installed.major &&
|
||||
minor == installed.minor &&
|
||||
patch == installed.patch &&
|
||||
prereleaseIdentifiers == installed.prereleaseIdentifiers
|
||||
prereleaseIdentifiers.map { $0.lowercased() } == installed.prereleaseIdentifiers.map { $0.lowercased() }
|
||||
}
|
||||
else {
|
||||
return major == installed.major &&
|
||||
minor == installed.minor &&
|
||||
patch == installed.patch &&
|
||||
prereleaseIdentifiers == installed.prereleaseIdentifiers &&
|
||||
prereleaseIdentifiers.map { $0.lowercased() } == installed.prereleaseIdentifiers.map { $0.lowercased() } &&
|
||||
buildMetadataIdentifiers.map { $0.lowercased() } == installed.buildMetadataIdentifiers.map { $0.lowercased() }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
51
Xcodes/Backend/Version+XcodeReleases.swift
Normal file
51
Xcodes/Backend/Version+XcodeReleases.swift
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import Version
|
||||
import struct XCModel.Xcode
|
||||
|
||||
extension Version {
|
||||
/// Initialize a Version from an XcodeReleases' XCModel.Xcode
|
||||
///
|
||||
/// This is kinda quick-and-dirty, and it would probably be better for us to adopt something closer to XCModel.Xcode under the hood and map the scraped data to it instead.
|
||||
init?(xcReleasesXcode: XCModel.Xcode) {
|
||||
var versionString = xcReleasesXcode.version.number ?? ""
|
||||
|
||||
// Append trailing ".0" in order to get a fully-specified version string
|
||||
let components = versionString.components(separatedBy: ".")
|
||||
versionString += Array(repeating: ".0", count: 3 - components.count).joined()
|
||||
|
||||
// Append prerelease identifier
|
||||
switch xcReleasesXcode.version.release {
|
||||
case let .beta(beta):
|
||||
versionString += "-Beta"
|
||||
if beta > 1 {
|
||||
versionString += ".\(beta)"
|
||||
}
|
||||
case let .dp(dp):
|
||||
versionString += "-DP"
|
||||
if dp > 1 {
|
||||
versionString += ".\(dp)"
|
||||
}
|
||||
case .gm:
|
||||
versionString += "-GM"
|
||||
case let .gmSeed(gmSeed):
|
||||
versionString += "-GM.Seed"
|
||||
if gmSeed > 1 {
|
||||
versionString += ".\(gmSeed)"
|
||||
}
|
||||
case let .rc(rc):
|
||||
versionString += "-Release.Candidate"
|
||||
if rc > 1 {
|
||||
versionString += ".\(rc)"
|
||||
}
|
||||
case .release:
|
||||
break
|
||||
}
|
||||
|
||||
// Append build identifier
|
||||
if let buildNumber = xcReleasesXcode.version.build {
|
||||
versionString += "+\(buildNumber)"
|
||||
}
|
||||
|
||||
self.init(versionString)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3,6 +3,7 @@ import Foundation
|
|||
import Path
|
||||
import Version
|
||||
import SwiftSoup
|
||||
import struct XCModel.Xcode
|
||||
|
||||
/// Provides lists of available Xcodes
|
||||
public final class XcodeList {
|
||||
|
|
@ -12,26 +13,34 @@ public final class XcodeList {
|
|||
|
||||
public private(set) var availableXcodes: [Xcode] = []
|
||||
|
||||
public var shouldUpdate: Bool {
|
||||
return availableXcodes.isEmpty
|
||||
}
|
||||
|
||||
public func update() -> AnyPublisher<[Xcode], Error> {
|
||||
releasedXcodes().combineLatest(prereleaseXcodes())
|
||||
.receive(on: DispatchQueue.main)
|
||||
.map { releasedXcodes, prereleaseXcodes in
|
||||
// Starting with Xcode 11 beta 6, developer.apple.com/download and developer.apple.com/download/more both list some pre-release versions of Xcode.
|
||||
// Previously pre-release versions only appeared on developer.apple.com/download.
|
||||
// /download/more doesn't include build numbers, so we trust that if the version number and prerelease identifiers are the same that they're the same build.
|
||||
// If an Xcode version is listed on both sites then prefer the one on /download because the build metadata is used to compare against installed Xcodes.
|
||||
let xcodes = releasedXcodes.filter { releasedXcode in
|
||||
prereleaseXcodes.contains { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: releasedXcode.version) } == false
|
||||
} + prereleaseXcodes
|
||||
self.availableXcodes = xcodes
|
||||
try? self.cacheAvailableXcodes(xcodes)
|
||||
return xcodes
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
public func update(dataSource: DataSource) -> AnyPublisher<[Xcode], Error> {
|
||||
switch dataSource {
|
||||
case .apple:
|
||||
return releasedXcodes().combineLatest(prereleaseXcodes())
|
||||
.receive(on: DispatchQueue.main)
|
||||
.map { releasedXcodes, prereleaseXcodes in
|
||||
// Starting with Xcode 11 beta 6, developer.apple.com/download and developer.apple.com/download/more both list some pre-release versions of Xcode.
|
||||
// Previously pre-release versions only appeared on developer.apple.com/download.
|
||||
// /download/more doesn't include build numbers, so we trust that if the version number and prerelease identifiers are the same that they're the same build.
|
||||
// If an Xcode version is listed on both sites then prefer the one on /download because the build metadata is used to compare against installed Xcodes.
|
||||
let xcodes = releasedXcodes.filter { releasedXcode in
|
||||
prereleaseXcodes.contains { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: releasedXcode.version) } == false
|
||||
} + prereleaseXcodes
|
||||
self.availableXcodes = xcodes
|
||||
try? self.cacheAvailableXcodes(xcodes)
|
||||
return xcodes
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
case .xcodeReleases:
|
||||
return xcodeReleases()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents(
|
||||
receiveOutput: { xcodes in
|
||||
try? self.cacheAvailableXcodes(xcodes)
|
||||
}
|
||||
)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +60,8 @@ extension XcodeList {
|
|||
}
|
||||
|
||||
extension XcodeList {
|
||||
// MARK: Apple
|
||||
|
||||
private func releasedXcodes() -> AnyPublisher<[Xcode], Error> {
|
||||
Current.network.dataTask(with: URLRequest.downloads)
|
||||
.map(\.data)
|
||||
|
|
@ -84,7 +95,7 @@ extension XcodeList {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func parsePrereleaseXcodes(from data: Data) throws -> [Xcode] {
|
||||
private func parsePrereleaseXcodes(from data: Data) throws -> [Xcode] {
|
||||
let body = String(data: data, encoding: .utf8)!
|
||||
let document = try SwiftSoup.parse(body)
|
||||
|
||||
|
|
@ -102,3 +113,36 @@ extension XcodeList {
|
|||
return [Xcode(version: version, url: url, filename: filename, releaseDate: DateFormatter.downloadsReleaseDate.date(from: releaseDateString))]
|
||||
}
|
||||
}
|
||||
|
||||
extension XcodeList {
|
||||
// MARK: XcodeReleases
|
||||
|
||||
private func xcodeReleases() -> AnyPublisher<[Xcode], Error> {
|
||||
Current.network.dataTask(with: URLRequest(url: URL(string: "https://xcodereleases.com/data.json")!))
|
||||
.map(\.data)
|
||||
.decode(type: [XCModel.Xcode].self, decoder: JSONDecoder())
|
||||
.map { xcReleasesXcodes in
|
||||
let xcodes = xcReleasesXcodes.compactMap { xcReleasesXcode -> Xcode? in
|
||||
guard
|
||||
let downloadURL = xcReleasesXcode.links?.download?.url,
|
||||
let version = Version(xcReleasesXcode: xcReleasesXcode)
|
||||
else { return nil }
|
||||
|
||||
let releaseDate = Calendar(identifier: .gregorian).date(from: DateComponents(
|
||||
year: xcReleasesXcode.date.year,
|
||||
month: xcReleasesXcode.date.month,
|
||||
day: xcReleasesXcode.date.day
|
||||
))
|
||||
|
||||
return Xcode(
|
||||
version: version,
|
||||
url: downloadURL,
|
||||
filename: String(downloadURL.path.suffix(fromLast: "/")),
|
||||
releaseDate: releaseDate
|
||||
)
|
||||
}
|
||||
return xcodes
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -360,6 +360,33 @@ SOFTWARE.\
|
|||
\
|
||||
\
|
||||
|
||||
\fs34 data\
|
||||
\
|
||||
|
||||
\fs26 MIT License\
|
||||
\
|
||||
Copyright (c) 2018 \
|
||||
\
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy\
|
||||
of this software and associated documentation files (the "Software"), to deal\
|
||||
in the Software without restriction, including without limitation the rights\
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\
|
||||
copies of the Software, and to permit persons to whom the Software is\
|
||||
furnished to do so, subject to the following conditions:\
|
||||
\
|
||||
The above copyright notice and this permission notice shall be included in all\
|
||||
copies or substantial portions of the Software.\
|
||||
\
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\
|
||||
SOFTWARE.\
|
||||
\
|
||||
\
|
||||
|
||||
\fs34 LegibleError\
|
||||
\
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import SwiftUI
|
|||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@AppStorage("dataSource") var dataSource: DataSource = .xcodeReleases
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
|
@ -21,6 +22,22 @@ struct SettingsView: View {
|
|||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
GroupBox(label: Text("Data Source")) {
|
||||
VStack(alignment: .leading) {
|
||||
Picker("Data Source", selection: $dataSource) {
|
||||
ForEach(DataSource.allCases) { dataSource in
|
||||
Text(dataSource.description)
|
||||
.tag(dataSource)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
|
||||
AttributedText(dataSourceFootnote)
|
||||
.font(.footnote)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
|
|
@ -28,6 +45,23 @@ struct SettingsView: View {
|
|||
.frame(width: 300)
|
||||
.frame(minHeight: 300)
|
||||
}
|
||||
|
||||
private var dataSourceFootnote: NSAttributedString {
|
||||
let string = """
|
||||
The Apple data source scrapes the Apple Developer website. It will always show the latest releases that are available, but is more fragile.
|
||||
|
||||
Xcode Releases is an unofficial list of Xcode releases. It's provided as well-formed data, contains extra information that is not readily available from Apple, and is less likely to break if Apple redesigns their developer website.
|
||||
"""
|
||||
let attributedString = NSMutableAttributedString(
|
||||
string: string,
|
||||
attributes: [
|
||||
.font: NSFont.preferredFont(forTextStyle: .footnote, options: [:]),
|
||||
.foregroundColor: NSColor.labelColor
|
||||
]
|
||||
)
|
||||
attributedString.addAttribute(.link, value: URL(string: "https://xcodereleases.com")!, range: NSRange(string.range(of: "Xcode Releases")!, in: string))
|
||||
return attributedString
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
|
|
|
|||
Loading…
Reference in a new issue