Compare commits

..

No commits in common. "main" and "3.5.2" have entirely different histories.
main ... 3.5.2

443 changed files with 31930 additions and 28675 deletions

View File

@ -18,14 +18,14 @@
[branch "upd/dev"]
remote = origin
merge = refs/heads/upd/dev
[remote "gitlink"]
url = https://gitlink.org.cn/vChewing/vChewing-macOS.git
fetch = +refs/heads/*:refs/remotes/gitlink/*
[branch "bleed/1.5.x"]
remote = origin
merge = refs/heads/bleed/1.5.x
[remote "gitcode"]
url = https://gitcode.net/vChewing/vChewing-macOS.git/
fetch = +refs/heads/*:refs/remotes/gitcode/*
[remote "gitlab"]
url = https://gitlab.com/vChewing/vChewing-macOS.git/
url = https://jihulab.com/vChewing/vChewing-macOS.git/
fetch = +refs/heads/*:refs/remotes/gitlab/*
[remote "github"]
url = https://github.com/vChewing/vChewing-macOS/
@ -34,7 +34,6 @@
url = https://gitee.com/vChewing/vChewing-macOS.git/
fetch = +refs/heads/*:refs/remotes/all/*
pushurl = https://gitee.com/vchewing/vChewing-macOS.git/
pushurl = https://gitlink.org.cn/vChewing/vChewing-macOS.git/
pushurl = https://gitcode.net/vChewing/vChewing-macOS.git/
pushurl = https://gitlab.com/vChewing/vChewing-macOS.git/
pushurl = https://jihulab.com/vChewing/vChewing-macOS.git/
pushurl = https://github.com/vChewing/vChewing-macOS/

View File

@ -1,4 +1,4 @@
name: debug-macOS-MainAssembly
name: Build-with-macOS-latest
on:
push:
branches: [ "main" ]
@ -7,16 +7,16 @@ on:
jobs:
build:
name: Build
runs-on: macOS-13
name: Build (latest)
runs-on: macOS-latest
env:
GIT_SSL_NO_VERIFY: true
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '^15.1'
xcode-version: latest-stable
- uses: actions/checkout@v1
- name: Clean
run: make spmClean
run: make clean
- name: Build
run: make spmDebug
run: git pull --all && git submodule sync; make update; make

View File

@ -9,7 +9,6 @@
// requirements defined in MIT License.
import Foundation
import SQLite3
// MARK: -
@ -28,14 +27,6 @@ fileprivate extension String {
}
}
// MARK: - String as SQL Command
fileprivate extension String {
@discardableResult func runAsSQLExec(dbPointer ptrDB: inout OpaquePointer?) -> Bool {
ptrDB != nil && sqlite3_exec(ptrDB, self, nil, nil, nil) == SQLITE_OK
}
}
// MARK: - StringView Ranges Extension (by Isaac Xen)
fileprivate extension String {
@ -126,125 +117,40 @@ func cnvPhonabetToASCII(_ incoming: String) -> String {
private let urlCurrentFolder = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
private let urlCHSRoot: String = "\(urlCurrentFolder.path)/components/chs/"
private let urlCHTRoot: String = "\(urlCurrentFolder.path)/components/cht/"
private let urlCHSRoot: String = "./components/chs/"
private let urlCHTRoot: String = "./components/cht/"
private let urlKanjiCore: String = "\(urlCurrentFolder.path)/components/common/char-kanji-core.txt"
private let urlMiscBPMF: String = "\(urlCurrentFolder.path)/components/common/char-misc-bpmf.txt"
private let urlMiscNonKanji: String = "\(urlCurrentFolder.path)/components/common/char-misc-nonkanji.txt"
private let urlKanjiCore: String = "./components/common/char-kanji-core.txt"
private let urlMiscBPMF: String = "./components/common/char-misc-bpmf.txt"
private let urlMiscNonKanji: String = "./components/common/char-misc-nonkanji.txt"
private let urlPunctuation: String = "\(urlCurrentFolder.path)/components/common/data-punctuations.txt"
private let urlSymbols: String = "\(urlCurrentFolder.path)/components/common/data-symbols.txt"
private let urlZhuyinwen: String = "\(urlCurrentFolder.path)/components/common/data-zhuyinwen.txt"
private let urlCNS: String = "\(urlCurrentFolder.path)/components/common/char-kanji-cns.txt"
private let urlPunctuation: String = "./components/common/data-punctuations.txt"
private let urlSymbols: String = "./components/common/data-symbols.txt"
private let urlZhuyinwen: String = "./components/common/data-zhuyinwen.txt"
private let urlCNS: String = "./components/common/char-kanji-cns.txt"
private let urlOutputCHS: String = "\(urlCurrentFolder.path)/data-chs.txt"
private let urlOutputCHT: String = "\(urlCurrentFolder.path)/data-cht.txt"
private let urlOutputCHS: String = "./data-chs.txt"
private let urlOutputCHT: String = "./data-cht.txt"
private let urlJSONSymbols: String = "\(urlCurrentFolder.path)/data-symbols.json"
private let urlJSONZhuyinwen: String = "\(urlCurrentFolder.path)/data-zhuyinwen.json"
private let urlJSONCNS: String = "\(urlCurrentFolder.path)/data-cns.json"
private let urlJSONSymbols: String = "./data-symbols.json"
private let urlJSONZhuyinwen: String = "./data-zhuyinwen.json"
private let urlJSONCNS: String = "./data-cns.json"
private let urlJSONCHS: String = "\(urlCurrentFolder.path)/data-chs.json"
private let urlJSONCHT: String = "\(urlCurrentFolder.path)/data-cht.json"
private let urlJSONBPMFReverseLookup: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup.json"
private let urlJSONBPMFReverseLookupCNS1: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS1.json"
private let urlJSONBPMFReverseLookupCNS2: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS2.json"
private let urlJSONBPMFReverseLookupCNS3: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS3.json"
private let urlJSONBPMFReverseLookupCNS4: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS4.json"
private let urlJSONBPMFReverseLookupCNS5: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS5.json"
private let urlJSONBPMFReverseLookupCNS6: String = "\(urlCurrentFolder.path)/data-bpmf-reverse-lookup-CNS6.json"
private let urlJSONCHS: String = "./data-chs.json"
private let urlJSONCHT: String = "./data-cht.json"
private let urlJSONBPMFReverseLookup: String = "./data-bpmf-reverse-lookup.json"
private let urlJSONBPMFReverseLookupCNS1: String = "./data-bpmf-reverse-lookup-CNS1.json"
private let urlJSONBPMFReverseLookupCNS2: String = "./data-bpmf-reverse-lookup-CNS2.json"
private let urlJSONBPMFReverseLookupCNS3: String = "./data-bpmf-reverse-lookup-CNS3.json"
private let urlJSONBPMFReverseLookupCNS4: String = "./data-bpmf-reverse-lookup-CNS4.json"
private let urlJSONBPMFReverseLookupCNS5: String = "./data-bpmf-reverse-lookup-CNS5.json"
private let urlJSONBPMFReverseLookupCNS6: String = "./data-bpmf-reverse-lookup-CNS6.json"
private var isReverseLookupDictionaryProcessed: Bool = false
private let urlSQLite: String = "\(urlCurrentFolder.path)/Build/Release/vChewingFactoryDatabase.sqlite"
private var mapReverseLookupForCheck: [String: [String]] = [:]
private var exceptedChars: Set<String> = .init()
private var ptrSQL: OpaquePointer?
var rangeMapJSONCHS: [String: [String]] = [:]
var rangeMapJSONCHT: [String: [String]] = [:]
var rangeMapSymbols: [String: [String]] = [:]
var rangeMapZhuyinwen: [String: [String]] = [:]
var rangeMapCNS: [String: [String]] = [:]
var rangeMapReverseLookup: [String: [String]] = [:]
/// Also use mapReverseLookupForCheck.
// MARK: -
func prepareDatabase() -> Bool {
let sqlMakeTableMACV = """
DROP TABLE IF EXISTS DATA_REV;
DROP TABLE IF EXISTS DATA_MAIN;
CREATE TABLE IF NOT EXISTS DATA_MAIN (
theKey TEXT NOT NULL,
theDataCHS TEXT,
theDataCHT TEXT,
theDataCNS TEXT,
theDataMISC TEXT,
theDataSYMB TEXT,
theDataCHEW TEXT,
PRIMARY KEY (theKey)
) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS DATA_REV (
theChar TEXT NOT NULL,
theReadings TEXT NOT NULL,
PRIMARY KEY (theChar)
) WITHOUT ROWID;
"""
guard sqlite3_open(":memory:", &ptrSQL) == SQLITE_OK else { return false }
guard sqlite3_exec(ptrSQL, "PRAGMA synchronous = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
guard sqlite3_exec(ptrSQL, "PRAGMA journal_mode = OFF;", nil, nil, nil) == SQLITE_OK else { return false }
guard sqlMakeTableMACV.runAsSQLExec(dbPointer: &ptrSQL) else { return false }
guard "begin;".runAsSQLExec(dbPointer: &ptrSQL) else { return false }
return true
}
@discardableResult func writeMainMapToSQL(_ theMap: [String: [String]], column columnName: String) -> Bool {
for (encryptedKey, arrValues) in theMap {
// SQL 西 ASCII 退''
let safeKey = encryptedKey.replacingOccurrences(of: "'", with: "''")
let valueText = arrValues.joined(separator: "\t").replacingOccurrences(of: "'", with: "''")
let sqlStmt = "INSERT INTO DATA_MAIN (theKey, \(columnName)) VALUES ('\(safeKey)', '\(valueText)') ON CONFLICT(theKey) DO UPDATE SET \(columnName)='\(valueText)';"
guard sqlStmt.runAsSQLExec(dbPointer: &ptrSQL) else {
print("Failed: " + sqlStmt)
return false
}
}
return true
}
@discardableResult func writeRevLookupMapToSQL(_ theMap: [String: [String]]) -> Bool {
for (encryptedKey, arrValues) in theMap {
// SQL 西 ASCII 退''
let safeKey = encryptedKey.replacingOccurrences(of: "'", with: "''")
let valueText = arrValues.joined(separator: "\t").replacingOccurrences(of: "'", with: "''")
let sqlStmt = "INSERT INTO DATA_REV (theChar, theReadings) VALUES ('\(safeKey)', '\(valueText)') ON CONFLICT(theChar) DO UPDATE SET theReadings='\(valueText)';"
guard sqlStmt.runAsSQLExec(dbPointer: &ptrSQL) else {
print("Failed: " + sqlStmt)
return false
}
}
return true
}
// MARK: - Dump SQLite3 Memory Database to File.
@discardableResult func dumpSQLDB() -> Bool {
var ptrSQLTarget: OpaquePointer?
defer { sqlite3_close_v2(ptrSQLTarget) }
guard sqlite3_open(urlSQLite, &ptrSQLTarget) == SQLITE_OK else { return false }
let ptrBackupObj = sqlite3_backup_init(ptrSQLTarget, "main", ptrSQL, "main")
if ptrBackupObj != nil {
sqlite3_backup_step(ptrBackupObj, -1)
sqlite3_backup_finish(ptrBackupObj)
}
return sqlite3_errcode(ptrSQLTarget) == SQLITE_OK
}
// MARK: -
func rawDictForPhrases(isCHS: Bool) -> [Unigram] {
@ -413,10 +319,8 @@ func rawDictForKanjis(isCHS: Bool) -> [Unigram] {
if !isReverseLookupDictionaryProcessed {
do {
isReverseLookupDictionaryProcessed = true
if compileJSON {
try JSONSerialization.data(withJSONObject: mapReverseLookupJSON, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookup))
}
try JSONSerialization.data(withJSONObject: mapReverseLookupJSON, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookup))
mapReverseLookupForCheck = mapReverseLookupUnencrypted
} catch {
NSLog(" - Core Reverse Lookup Data Generation Failed.")
@ -555,8 +459,10 @@ func fileOutput(isCHS: Bool) {
let i18n: String = isCHS ? "簡體中文" : "繁體中文"
var strPunctuation = ""
var rangeMapJSON: [String: [String]] = [:]
let pathOutput = URL(fileURLWithPath: isCHS ? urlOutputCHS : urlOutputCHT)
let jsonURL = URL(fileURLWithPath: isCHS ? urlJSONCHS : urlJSONCHT)
let pathOutput = urlCurrentFolder.appendingPathComponent(
isCHS ? urlOutputCHS : urlOutputCHT)
let jsonURL = urlCurrentFolder.appendingPathComponent(
isCHS ? urlJSONCHS : urlJSONCHT)
var strPrintLine = ""
//
do {
@ -626,18 +532,11 @@ func fileOutput(isCHS: Bool) {
NSLog(" - \(i18n): 要寫入檔案的 txt 內容編譯完畢。")
do {
try strPrintLine.write(to: pathOutput, atomically: true, encoding: .utf8)
if compileJSON {
try JSONSerialization.data(withJSONObject: rangeMapJSON, options: .sortedKeys).write(to: jsonURL)
}
if isCHS {
rangeMapJSONCHS = rangeMapJSON
} else {
rangeMapJSONCHT = rangeMapJSON
}
try JSONSerialization.data(withJSONObject: rangeMapJSON, options: .sortedKeys).write(to: jsonURL)
} catch {
NSLog(" - \(i18n): Error on writing strings to file: \(error)")
}
NSLog(" - \(i18n): JSON & TXT 寫入完成。")
NSLog(" - \(i18n): 寫入完成。")
if !arrFoundedDuplications.isEmpty {
NSLog(" - \(i18n): 尋得下述重複項目,請務必手動排查:")
print("-------------------")
@ -677,9 +576,7 @@ func commonFileOutput() {
let theKey = String(neta[1])
let theValue = String(neta[0])
if !neta[0].isEmpty, !neta[1].isEmpty, line.first != "#" {
let encryptedKey = cnvPhonabetToASCII(theKey)
mapSymbols[encryptedKey, default: []].append(theValue)
rangeMapSymbols[encryptedKey, default: []].append(theValue)
mapSymbols[cnvPhonabetToASCII(theKey), default: []].append(theValue)
}
}
}
@ -690,9 +587,7 @@ func commonFileOutput() {
let theKey = String(neta[1])
let theValue = String(neta[0])
if !neta[0].isEmpty, !neta[1].isEmpty, line.first != "#" {
let encryptedKey = cnvPhonabetToASCII(theKey)
mapZhuyinwen[encryptedKey, default: []].append(theValue)
rangeMapZhuyinwen[encryptedKey, default: []].append(theValue)
mapZhuyinwen[cnvPhonabetToASCII(theKey), default: []].append(theValue)
}
}
}
@ -703,33 +598,30 @@ func commonFileOutput() {
let theKey = String(neta[1])
let theValue = String(neta[0])
if !neta[0].isEmpty, !neta[1].isEmpty, line.first != "#" {
let encryptedKey = cnvPhonabetToASCII(theKey)
mapCNS[encryptedKey, default: []].append(theValue)
rangeMapCNS[encryptedKey, default: []].append(theValue)
mapCNS[cnvPhonabetToASCII(theKey), default: []].append(theValue)
json: if !theKey.contains("_"), !theKey.contains("-") {
rangeMapReverseLookup[theValue, default: []].append(encryptedKey)
if mapReverseLookupCNS1.keys.count <= 16500 {
mapReverseLookupCNS1[theValue, default: []].append(encryptedKey)
mapReverseLookupCNS1[theValue, default: []].append(cnvPhonabetToASCII(theKey))
break json
}
if mapReverseLookupCNS2.keys.count <= 16500 {
mapReverseLookupCNS2[theValue, default: []].append(encryptedKey)
mapReverseLookupCNS2[theValue, default: []].append(cnvPhonabetToASCII(theKey))
break json
}
if mapReverseLookupCNS3.keys.count <= 16500 {
mapReverseLookupCNS3[theValue, default: []].append(encryptedKey)
mapReverseLookupCNS3[theValue, default: []].append(cnvPhonabetToASCII(theKey))
break json
}
if mapReverseLookupCNS4.keys.count <= 16500 {
mapReverseLookupCNS4[theValue, default: []].append(encryptedKey)
mapReverseLookupCNS4[theValue, default: []].append(cnvPhonabetToASCII(theKey))
break json
}
if mapReverseLookupCNS5.keys.count <= 16500 {
mapReverseLookupCNS5[theValue, default: []].append(encryptedKey)
mapReverseLookupCNS5[theValue, default: []].append(cnvPhonabetToASCII(theKey))
break json
}
if mapReverseLookupCNS6.keys.count <= 16500 {
mapReverseLookupCNS6[theValue, default: []].append(encryptedKey)
mapReverseLookupCNS6[theValue, default: []].append(cnvPhonabetToASCII(theKey))
break json
}
}
@ -738,32 +630,62 @@ func commonFileOutput() {
}
NSLog(" - \(i18n): 要寫入檔案的內容編譯完畢。")
do {
if compileJSON {
try JSONSerialization.data(withJSONObject: mapSymbols, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONSymbols))
try JSONSerialization.data(withJSONObject: mapZhuyinwen, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONZhuyinwen))
try JSONSerialization.data(withJSONObject: mapCNS, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONCNS))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS1, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS1))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS2, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS2))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS3, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS3))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS4, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS4))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS5, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS5))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS6, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS6))
}
try JSONSerialization.data(withJSONObject: mapSymbols, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONSymbols))
try JSONSerialization.data(withJSONObject: mapZhuyinwen, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONZhuyinwen))
try JSONSerialization.data(withJSONObject: mapCNS, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONCNS))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS1, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS1))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS2, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS2))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS3, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS3))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS4, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS4))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS5, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS5))
try JSONSerialization.data(withJSONObject: mapReverseLookupCNS6, options: .sortedKeys).write(
to: URL(fileURLWithPath: urlJSONBPMFReverseLookupCNS6))
} catch {
NSLog(" - \(i18n): Error on writing strings to file: \(error)")
}
NSLog(" - \(i18n): 寫入完成。")
}
// MARK: -
func main() {
let globalQueue = DispatchQueue.global(qos: .default)
let group = DispatchGroup()
group.enter()
globalQueue.async {
NSLog("// 準備編譯符號表情ㄅ文語料檔案。")
commonFileOutput()
group.leave()
}
group.enter()
globalQueue.async {
NSLog("// 準備編譯繁體中文核心語料檔案。")
fileOutput(isCHS: false)
group.leave()
}
group.enter()
globalQueue.async {
NSLog("// 準備編譯簡體中文核心語料檔案。")
fileOutput(isCHS: true)
group.leave()
}
//
_ = group.wait(timeout: .distantFuture)
group.notify(queue: DispatchQueue.main) {
NSLog("// 全部辭典檔案建置完畢。")
}
}
main()
// MARK: -
func healthCheck(_ data: [Unigram]) -> String {
@ -1057,107 +979,3 @@ func healthCheck(_ data: [Unigram]) -> String {
result += "\n"
return result
}
// MARK: - Flags
struct TaskFlags: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let common = TaskFlags(rawValue: 1 << 0)
public static let chs = TaskFlags(rawValue: 1 << 1)
public static let cht = TaskFlags(rawValue: 1 << 2)
}
// MARK: -
var compileJSON = false
var compileSQLite = true
func main() {
let arguments = CommandLine.arguments.compactMap { $0.lowercased() }
let jsonConditionMet = arguments.contains(where: { $0 == "--json" || $0 == "json" })
if jsonConditionMet {
NSLog("// 接下來準備建置 JSON 格式的原廠辭典,同時生成用來偵錯的 TXT 副產物。")
compileJSON = true
compileSQLite = false
} else {
NSLog("// 接下來準備建置 SQLite 格式的原廠辭典,同時生成用來偵錯的 TXT 副產物。")
compileJSON = false
compileSQLite = true
}
let prepared = prepareDatabase()
if compileSQLite, !prepared {
NSLog("// SQLite 資料庫初期化失敗。")
exit(-1)
}
var taskFlags: TaskFlags = [.common, .chs, .cht] {
didSet {
guard taskFlags.isEmpty else { return }
NSLog("// 全部 TXT 辭典檔案建置完畢。")
if compileJSON {
NSLog("// 全部 JSON 辭典檔案建置完畢。")
}
if compileSQLite, prepared {
NSLog("// 開始整合反查資料。")
mapReverseLookupForCheck.forEach { key, values in
values.reversed().forEach { valueLiteral in
let value = cnvPhonabetToASCII(valueLiteral)
if !rangeMapReverseLookup[key, default: []].contains(value) {
rangeMapReverseLookup[key, default: []].insert(value, at: 0)
}
}
}
NSLog("// 反查資料整合完畢。")
NSLog("// 準備建置 SQL 資料庫。")
writeMainMapToSQL(rangeMapJSONCHS, column: "theDataCHS")
writeMainMapToSQL(rangeMapJSONCHT, column: "theDataCHT")
writeMainMapToSQL(rangeMapSymbols, column: "theDataSYMB")
writeMainMapToSQL(rangeMapZhuyinwen, column: "theDataCHEW")
writeMainMapToSQL(rangeMapCNS, column: "theDataCNS")
writeRevLookupMapToSQL(rangeMapReverseLookup)
let committed = "commit;".runAsSQLExec(dbPointer: &ptrSQL)
assert(committed)
let compressed = "VACUUM;".runAsSQLExec(dbPointer: &ptrSQL)
assert(compressed)
if !dumpSQLDB() {
NSLog("// SQLite 辭典傾印失敗。")
} else {
NSLog("// 全部 SQLite 辭典檔案建置完畢。")
}
sqlite3_close_v2(ptrSQL)
}
}
}
let globalQueue = DispatchQueue.global(qos: .default)
let group = DispatchGroup()
group.enter()
globalQueue.async {
NSLog("// 準備編譯符號表情ㄅ文語料檔案。")
commonFileOutput()
taskFlags.remove(.common)
group.leave()
}
group.enter()
globalQueue.async {
NSLog("// 準備編譯繁體中文核心語料檔案。")
fileOutput(isCHS: false)
taskFlags.remove(.cht)
group.leave()
}
group.enter()
globalQueue.async {
NSLog("// 準備編譯簡體中文核心語料檔案。")
fileOutput(isCHS: true)
taskFlags.remove(.chs)
group.leave()
}
//
group.wait()
}
main()

199
Installer/AppDelegate.swift Normal file
View File

@ -0,0 +1,199 @@
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import IMKUtils
import InputMethodKit
import SwiftExtension
public let kTargetBin = "vChewing"
public let kTargetBinPhraseEditor = "vChewingPhraseEditor"
public let kTargetType = "app"
public let kTargetBundle = "vChewing.app"
public let kTargetBundleWithComponents = "Library/Input%20Methods/vChewing.app"
public let realHomeDir = URL(
fileURLWithFileSystemRepresentation: getpwuid(getuid()).pointee.pw_dir, isDirectory: true, relativeTo: nil
)
public let urlDestinationPartial = realHomeDir.appendingPathComponent("Library/Input Methods")
public let urlTargetPartial = realHomeDir.appendingPathComponent(kTargetBundleWithComponents)
public let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS")
.appendingPathComponent(kTargetBin)
public let kDestinationPartial = urlDestinationPartial.path
public let kTargetPartialPath = urlTargetPartial.path
public let kTargetFullBinPartialPath = urlTargetFullBinPartial.path
public let kTranslocationRemovalTickInterval: TimeInterval = 0.5
public let kTranslocationRemovalDeadline: TimeInterval = 60.0
@NSApplicationMain
@objc(AppDelegate)
class AppDelegate: NSWindowController, NSApplicationDelegate {
@IBOutlet var installButton: NSButton!
@IBOutlet var cancelButton: NSButton!
@IBOutlet var progressSheet: NSWindow!
@IBOutlet var progressIndicator: NSProgressIndicator!
@IBOutlet var appVersionLabel: NSTextField!
@IBOutlet var appCopyrightLabel: NSTextField!
@IBOutlet var appEULAContent: NSTextView!
var installingVersion = ""
var translocationRemovalStartTime: Date?
var currentVersionNumber: Int = 0
let imeURLInstalled = realHomeDir.appendingPathComponent("Library/Input Methods/vChewing.app")
var allRegisteredInstancesOfThisInputMethod: [TISInputSource] {
guard let components = Bundle(url: imeURLInstalled)?.infoDictionary?["ComponentInputModeDict"] as? [String: Any],
let tsInputModeListKey = components["tsInputModeListKey"] as? [String: Any]
else {
return []
}
return tsInputModeListKey.keys.compactMap { TISInputSource.generate(from: $0) }
}
func runAlertPanel(title: String, message: String, buttonTitle: String) {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = title
alert.informativeText = message
alert.addButton(withTitle: buttonTitle)
alert.runModal()
}
func applicationDidFinishLaunching(_: Notification) {
guard
let window = window,
let cell = installButton.cell as? NSButtonCell,
let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String,
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String,
let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String,
let eulaContentUpstream = Bundle.main.infoDictionary?["CFUpstreamEULAContent"] as? String
else {
NSSound.beep()
NSLog("The vChewing App Installer failed its initial guard-let process on appDidFinishLaunching().")
return
}
self.installingVersion = installingVersion
cancelButton.nextKeyView = installButton
installButton.nextKeyView = cancelButton
window.defaultButtonCell = cell
appCopyrightLabel.stringValue = copyrightLabel
appEULAContent.string = eulaContent + "\n" + eulaContentUpstream
appVersionLabel.stringValue = "\(versionString) Build \(installingVersion)"
window.title = "\(window.title) (v\(versionString), Build \(installingVersion))"
window.standardWindowButton(.closeButton)?.isHidden = true
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
window.standardWindowButton(.zoomButton)?.isHidden = true
window.titlebarAppearsTransparent = true
if FileManager.default.fileExists(atPath: kTargetPartialPath) {
let currentBundle = Bundle(path: kTargetPartialPath)
let shortVersion = currentBundle?.infoDictionary?["CFBundleShortVersionString"] as? String
let currentVersion = currentBundle?.infoDictionary?[kCFBundleVersionKey as String] as? String
currentVersionNumber = (currentVersion as NSString?)?.integerValue ?? 0
if shortVersion != nil, let currentVersion = currentVersion,
currentVersion.compare(installingVersion, options: .numeric) == .orderedAscending
{
// Upgrading confirmed.
installButton.title = NSLocalizedString("Upgrade", comment: "")
}
}
window.center()
window.orderFront(self)
NSApp.popup()
}
@IBAction func agreeAndInstallAction(_: AnyObject) {
cancelButton.isEnabled = false
installButton.isEnabled = false
removeThenInstallInputMethod()
}
@objc func timerTick(_ timer: Timer) {
guard let window = window else { return }
let elapsed = Date().timeIntervalSince(translocationRemovalStartTime ?? Date())
if elapsed >= kTranslocationRemovalDeadline {
timer.invalidate()
window.endSheet(progressSheet, returnCode: .cancel)
} else if Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath) == false {
progressIndicator.doubleValue = 1.0
timer.invalidate()
window.endSheet(progressSheet, returnCode: .continue)
}
}
func endAppWithDelay() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
NSApp.terminate(self)
}
}
@IBAction func cancelAction(_: AnyObject) {
NSApp.terminate(self)
}
func windowWillClose(_: Notification) {
NSApp.terminate(self)
}
func shell(_ command: String) throws -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
if #available(macOS 10.13, *) {
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
} else {
task.launchPath = "/bin/zsh"
}
task.standardInput = nil
if #available(macOS 10.13, *) {
try task.run()
} else {
task.launch()
}
var output = ""
do {
let data = try pipe.fileHandleForReading.readToEnd()
if let data = data, let str = String(data: data, encoding: .utf8) {
output.append(str)
}
} catch {
return ""
}
return output
}
}
// MARK: - NSApp Activation Helper
// This is to deal with changes brought by macOS 14.
private extension NSApplication {
func popup() {
#if compiler(>=5.9) && canImport(AppKit, _version: "14.0")
if #available(macOS 14.0, *) {
NSApp.activate()
} else {
NSApp.activate(ignoringOtherApps: true)
}
#else
NSApp.activate(ignoringOtherApps: true)
#endif
}
}

View File

@ -0,0 +1,209 @@
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import InputMethodKit
extension AppDelegate {
func removeThenInstallInputMethod() {
// if !FileManager.default.fileExists(atPath: kTargetPartialPath) {
// installInputMethod(
// previousExists: false, previousVersionNotFullyDeactivatedWarning: false
// )
// return
// }
guard let window = window else { return }
let shouldWaitForTranslocationRemoval =
Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath)
&& window.responds(to: #selector(NSWindow.beginSheet(_:completionHandler:)))
//
do {
let sourceDir = kDestinationPartial
let fileManager = FileManager.default
let fileURLString = sourceDir + "/" + kTargetBundle
let fileURL = URL(fileURLWithPath: fileURLString)
//
if fileManager.fileExists(atPath: fileURLString) {
//
try fileManager.trashItem(at: fileURL, resultingItemURL: nil)
} else {
NSLog("File does not exist")
}
} catch let error as NSError {
NSLog("An error took place: \(error)")
}
let killTask = Process()
killTask.launchPath = "/usr/bin/killall"
killTask.arguments = [kTargetBin]
killTask.launch()
killTask.waitUntilExit()
let killTask2 = Process()
killTask2.launchPath = "/usr/bin/killall"
killTask2.arguments = [kTargetBinPhraseEditor]
killTask2.launch()
killTask2.waitUntilExit()
if shouldWaitForTranslocationRemoval {
progressIndicator.startAnimation(self)
window.beginSheet(progressSheet) { returnCode in
DispatchQueue.main.async {
if returnCode == .continue {
self.installInputMethod(
previousExists: true,
previousVersionNotFullyDeactivatedWarning: false
)
} else {
self.installInputMethod(
previousExists: true,
previousVersionNotFullyDeactivatedWarning: true
)
}
}
}
translocationRemovalStartTime = Date()
Timer.scheduledTimer(
timeInterval: kTranslocationRemovalTickInterval, target: self,
selector: #selector(timerTick(_:)), userInfo: nil, repeats: true
)
} else {
installInputMethod(
previousExists: false, previousVersionNotFullyDeactivatedWarning: false
)
}
}
func installInputMethod(
previousExists _: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool
) {
guard
let targetBundle = Bundle.main.path(forResource: kTargetBin, ofType: kTargetType)
else {
return
}
let cpTask = Process()
cpTask.launchPath = "/bin/cp"
print(kDestinationPartial)
cpTask.arguments = [
"-R", targetBundle, kDestinationPartial,
]
cpTask.launch()
cpTask.waitUntilExit()
if cpTask.terminationStatus != 0 {
runAlertPanel(
title: NSLocalizedString("Install Failed", comment: ""),
message: NSLocalizedString("Cannot copy the file to the destination.", comment: ""),
buttonTitle: NSLocalizedString("Cancel", comment: "")
)
endAppWithDelay()
}
_ = try? shell("/usr/bin/xattr -drs com.apple.quarantine \(kTargetPartialPath)")
guard let theBundle = Bundle(url: imeURLInstalled),
let imeIdentifier = theBundle.bundleIdentifier
else {
endAppWithDelay()
return
}
let imeBundleURL = theBundle.bundleURL
if allRegisteredInstancesOfThisInputMethod.isEmpty {
NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).")
let status = (TISRegisterInputSource(imeBundleURL as CFURL) == noErr)
if !status {
let message = String(
format: NSLocalizedString(
"Cannot find input source %@ after registration.", comment: ""
),
imeIdentifier
)
runAlertPanel(
title: NSLocalizedString("Fatal Error", comment: ""), message: message,
buttonTitle: NSLocalizedString("Abort", comment: "")
)
endAppWithDelay()
return
}
if allRegisteredInstancesOfThisInputMethod.isEmpty {
let message = String(
format: NSLocalizedString(
"Cannot find input source %@ after registration.", comment: ""
),
imeIdentifier
)
runAlertPanel(
title: NSLocalizedString("Fatal Error", comment: ""), message: message,
buttonTitle: NSLocalizedString("Abort", comment: "")
)
}
}
var mainInputSourceEnabled = false
allRegisteredInstancesOfThisInputMethod.forEach { neta in
let isActivated = neta.isActivated
defer {
// 使
// 使使使
mainInputSourceEnabled = mainInputSourceEnabled || isActivated
}
if isActivated { return }
// WARNING: macOS 12 may return false positives, hence forced activation.
if neta.activate() {
NSLog("Input method enabled: \(imeIdentifier)")
} else {
NSLog("Failed to enable input method: \(imeIdentifier)")
}
}
// Alert Panel
let ntfPostInstall = NSAlert()
if warning {
ntfPostInstall.messageText = NSLocalizedString("Attention", comment: "")
ntfPostInstall.informativeText = NSLocalizedString(
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional.",
comment: ""
)
ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: ""))
} else {
if !mainInputSourceEnabled {
ntfPostInstall.messageText = NSLocalizedString("Warning", comment: "")
ntfPostInstall.informativeText = NSLocalizedString(
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.",
comment: ""
)
ntfPostInstall.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
} else {
ntfPostInstall.messageText = NSLocalizedString(
"Installation Successful", comment: ""
)
ntfPostInstall.informativeText = NSLocalizedString(
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.",
comment: ""
)
ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: ""))
}
}
ntfPostInstall.beginSheetModal(for: window!) { _ in
self.endAppWithDelay()
}
}
}

View File

@ -1,171 +0,0 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import InputMethodKit
import SwiftUI
public let kTargetBin = "vChewing"
public let kTargetBinPhraseEditor = "vChewingPhraseEditor"
public let kTargetType = "app"
public let kTargetBundle = "vChewing.app"
public let kTargetBundleWithComponents = "Library/Input%20Methods/vChewing.app"
public let kTISInputSourceID = "org.atelierInmu.inputmethod.vChewing"
let imeURLInstalled = realHomeDir.appendingPathComponent("Library/Input Methods/vChewing.app")
public let realHomeDir = URL(
fileURLWithFileSystemRepresentation: getpwuid(getuid()).pointee.pw_dir, isDirectory: true, relativeTo: nil
)
public let urlDestinationPartial = realHomeDir.appendingPathComponent("Library/Input Methods")
public let urlTargetPartial = realHomeDir.appendingPathComponent(kTargetBundleWithComponents)
public let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS")
.appendingPathComponent(kTargetBin)
public let kDestinationPartial = urlDestinationPartial.path
public let kTargetPartialPath = urlTargetPartial.path
public let kTargetFullBinPartialPath = urlTargetFullBinPartial.path
public let kTranslocationRemovalTickInterval: TimeInterval = 0.5
public let kTranslocationRemovalDeadline: TimeInterval = 60.0
public let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String ?? "BAD_INSTALLING_VER"
public let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "BAD_VER_STR"
public let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String ?? "BAD_COPYRIGHT_LABEL"
public let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String ?? "BAD_EULA_CONTENT"
public let eulaContentUpstream = Bundle.main.infoDictionary?["CFUpstreamEULAContent"] as? String ?? "BAD_EULA_UPSTREAM"
public var mainWindowTitle: String {
"i18n:installer.INSTALLER_APP_TITLE_FULL".i18n + " (v\(versionString), Build \(installingVersion))"
}
var allRegisteredInstancesOfThisInputMethod: [TISInputSource] {
guard let components = Bundle(url: imeURLInstalled)?.infoDictionary?["ComponentInputModeDict"] as? [String: Any],
let tsInputModeListKey = components["tsInputModeListKey"] as? [String: Any]
else {
return []
}
return TISInputSource.match(modeIDs: tsInputModeListKey.keys.map(\.description))
}
// MARK: - NSApp Activation Helper
// This is to deal with changes brought by macOS 14.
public extension NSApplication {
func popup() {
#if compiler(>=5.9) && canImport(AppKit, _version: "14.0")
if #available(macOS 14.0, *) {
NSApp.activate()
} else {
NSApp.activate(ignoringOtherApps: true)
}
#else
NSApp.activate(ignoringOtherApps: true)
#endif
}
}
// MARK: - KeyWindow Finder
public extension NSApplication {
var keyWindows: [NSWindow] {
NSApp.windows.filter(\.isKeyWindow)
}
}
// MARK: - NSApp End With Delay
public extension NSApplication {
func terminateWithDelay() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { [weak self] in
if let this = self {
this.terminate(this)
}
}
}
}
// MARK: - Alert Message & Title Structure
public struct AlertIntel {}
public enum AlertType: String, Identifiable {
public var id: String { rawValue }
case nothing, installationFailed, missingAfterRegistration, postInstallAttention, postInstallWarning, postInstallOK
var title: LocalizedStringKey {
switch self {
case .nothing: return ""
case .installationFailed: return "Install Failed"
case .missingAfterRegistration: return "Fatal Error"
case .postInstallAttention: return "Attention"
case .postInstallWarning: return "Warning"
case .postInstallOK: return "Installation Successful"
}
}
var message: String {
switch self {
case .nothing: return ""
case .installationFailed:
return "Cannot copy the file to the destination.".i18n
case .missingAfterRegistration:
return String(
format: "Cannot find input source %@ after registration.".i18n,
kTISInputSourceID
)
case .postInstallAttention:
return "vChewing is upgraded, but please log out or reboot for the new version to be fully functional.".i18n
case .postInstallWarning:
return "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.".i18n
case .postInstallOK:
return "vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.".i18n
}
}
}
private extension StringLiteralType {
var i18n: String { NSLocalizedString(description, comment: "") }
}
// MARK: - Shell
public extension NSApplication {
func shell(_ command: String) throws -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
if #available(macOS 10.13, *) {
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
} else {
task.launchPath = "/bin/zsh"
}
task.standardInput = nil
if #available(macOS 10.13, *) {
try task.run()
} else {
task.launch()
}
var output = ""
do {
let data = try pipe.fileHandleForReading.readToEnd()
if let data = data, let str = String(data: data, encoding: .utf8) {
output.append(str)
}
} catch {
return ""
}
return output
}
}

View File

@ -1,161 +0,0 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import SwiftUI
public struct MainView: View {
static let strCopyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String ?? "BAD_COPYRIGHT_LABEL"
@State var pendingSheetPresenting = false
@State var isShowingAlertForFailedInstallation = false
@State var isShowingAlertForMissingPostInstall = false
@State var isShowingPostInstallNotification = false
@State var currentAlertContent: AlertType = .nothing
@State var isCancelButtonEnabled = true
@State var isAgreeButtonEnabled = true
@State var isPreviousVersionNotFullyDeactivated = false
@State var isTranslocationFinished: Bool?
@State var isUpgrading: Bool = false
var translocationRemovalStartTime: Date?
@State var timeRemaining = 60
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
public init() {
if FileManager.default.fileExists(atPath: kTargetPartialPath) {
let currentBundle = Bundle(path: kTargetPartialPath)
let shortVersion = currentBundle?.infoDictionary?["CFBundleShortVersionString"] as? String
let currentVersion = currentBundle?.infoDictionary?[kCFBundleVersionKey as String] as? String
if shortVersion != nil, let currentVersion = currentVersion,
currentVersion.compare(installingVersion, options: .numeric) == .orderedAscending
{
isUpgrading = true
}
}
}
public var body: some View {
GroupBox {
VStack(alignment: .leading, spacing: 6) {
VStack(alignment: .leading) {
HStack(alignment: .center) {
if let icon = NSImage(named: "IconSansMargin") {
Image(nsImage: icon).resizable().frame(width: 90, height: 90)
}
VStack(alignment: .leading) {
HStack {
Text("i18n:installer.APP_NAME").fontWeight(.heavy).lineLimit(1)
Text("v\(versionString) Build \(installingVersion)").lineLimit(1)
}.fixedSize()
Text("i18n:installer.APP_DERIVED_FROM").font(.custom("Tahoma", size: 11))
Text(Self.strCopyrightLabel).font(.custom("Tahoma", size: 11))
Text("i18n:installer.DEV_CREW").font(.custom("Tahoma", size: 11)).padding([.vertical], 2)
}
}
GroupBox(label: Text("i18n:installer.LICENSE_TITLE")) {
ScrollView(.vertical, showsIndicators: true) {
HStack {
Text(eulaContent + "\n" + eulaContentUpstream).textSelection(.enabled)
.frame(maxWidth: 455)
.font(.custom("Tahoma", size: 11))
Spacer()
}
}.padding(4).frame(height: 128)
}
Text("i18n:installer.EULA_PROMPT_NOTICE").bold().padding(.bottom, 2)
}
Divider()
HStack(alignment: .top) {
Text("i18n:installer.DISCLAIMER_TEXT")
.font(.custom("Tahoma", size: 11))
.opacity(0.5)
.frame(maxWidth: .infinity)
VStack(spacing: 4) {
Button { installationButtonClicked() } label: {
Text(isUpgrading ? "i18n:installer.DO_APP_UPGRADE" : "i18n:installer.ACCEPT_INSTALLATION")
.bold().frame(width: 114)
}
.keyboardShortcut(.defaultAction)
.disabled(!isCancelButtonEnabled)
Button(role: .cancel) { NSApp.terminateWithDelay() } label: {
Text("i18n:installer.CANCEL_INSTALLATION").frame(width: 114)
}
.keyboardShortcut(.cancelAction)
.disabled(!isAgreeButtonEnabled)
}.fixedSize(horizontal: true, vertical: true)
}
Spacer()
}
.font(.custom("Tahoma", size: 12))
.padding(4)
}
// ALERTS
.alert(AlertType.installationFailed.title, isPresented: $isShowingAlertForFailedInstallation) {
Button(role: .cancel) { NSApp.terminateWithDelay() } label: { Text("Cancel") }
} message: {
Text(AlertType.installationFailed.message)
}
.alert(AlertType.missingAfterRegistration.title, isPresented: $isShowingAlertForMissingPostInstall) {
Button(role: .cancel) { NSApp.terminateWithDelay() } label: { Text("Abort") }
} message: {
Text(AlertType.missingAfterRegistration.message)
}
.alert(currentAlertContent.title, isPresented: $isShowingPostInstallNotification) {
Button(role: .cancel) { NSApp.terminateWithDelay() } label: {
Text(currentAlertContent == .postInstallWarning ? "Continue" : "OK")
}
} message: {
Text(currentAlertContent.message)
}
// SHEET FOR STOPPING THE OLD VERSION
.sheet(isPresented: $pendingSheetPresenting) {
// TODO: Tasks after sheet gets closed by `dismiss()`.
} content: {
Text("i18n:installer.STOPPING_THE_OLD_VERSION").frame(width: 407, height: 144)
.onReceive(timer) { _ in
if timeRemaining > 0 {
if Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath) == false {
pendingSheetPresenting = false
isTranslocationFinished = true
installInputMethod(
previousExists: true,
previousVersionNotFullyDeactivatedWarning: false
)
}
timeRemaining -= 1
} else {
pendingSheetPresenting = false
isTranslocationFinished = false
installInputMethod(
previousExists: true,
previousVersionNotFullyDeactivatedWarning: true
)
}
}
}
// OTHER
.padding(12)
.frame(width: 533, alignment: .topLeading)
.navigationTitle(mainWindowTitle)
.fixedSize()
.foregroundStyle(Color(nsColor: NSColor.textColor))
.background(Color(nsColor: NSColor.windowBackgroundColor))
.clipShape(RoundedRectangle(cornerRadius: 16))
.frame(minWidth: 533, idealWidth: 533, maxWidth: 533,
minHeight: 386, idealHeight: 386, maxHeight: 386,
alignment: .top)
}
func installationButtonClicked() {
isCancelButtonEnabled = false
isAgreeButtonEnabled = false
removeThenInstallInputMethod()
}
}

View File

@ -1,138 +0,0 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import IMKUtils
import InputMethodKit
public extension MainView {
func removeThenInstallInputMethod() {
let shouldWaitForTranslocationRemoval = Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath)
//
do {
let sourceDir = kDestinationPartial
let fileManager = FileManager.default
let fileURLString = sourceDir + "/" + kTargetBundle
let fileURL = URL(fileURLWithPath: fileURLString)
//
if fileManager.fileExists(atPath: fileURLString) {
//
try fileManager.trashItem(at: fileURL, resultingItemURL: nil)
} else {
NSLog("File does not exist")
}
} catch let error as NSError {
NSLog("An error took place: \(error)")
}
let killTask = Process()
killTask.launchPath = "/usr/bin/killall"
killTask.arguments = [kTargetBin]
killTask.launch()
killTask.waitUntilExit()
let killTask2 = Process()
killTask2.launchPath = "/usr/bin/killall"
killTask2.arguments = [kTargetBinPhraseEditor]
killTask2.launch()
killTask2.waitUntilExit()
if shouldWaitForTranslocationRemoval {
pendingSheetPresenting = true
} else {
installInputMethod(
previousExists: false, previousVersionNotFullyDeactivatedWarning: false
)
}
}
func installInputMethod(
previousExists _: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool
) {
guard
let targetBundle = Bundle.main.path(forResource: kTargetBin, ofType: kTargetType)
else {
return
}
let cpTask = Process()
cpTask.launchPath = "/bin/cp"
print(kDestinationPartial)
cpTask.arguments = [
"-R", targetBundle, kDestinationPartial,
]
cpTask.launch()
cpTask.waitUntilExit()
if cpTask.terminationStatus != 0 {
isShowingAlertForFailedInstallation = true
NSApp.terminateWithDelay()
}
_ = try? NSApp.shell("/usr/bin/xattr -drs com.apple.quarantine \(kTargetPartialPath)")
guard let theBundle = Bundle(url: imeURLInstalled),
let imeIdentifier = theBundle.bundleIdentifier
else {
NSApp.terminateWithDelay()
return
}
let imeBundleURL = theBundle.bundleURL
if allRegisteredInstancesOfThisInputMethod.isEmpty {
NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).")
let status = (TISRegisterInputSource(imeBundleURL as CFURL) == noErr)
if !status {
isShowingAlertForMissingPostInstall = true
NSApp.terminateWithDelay()
}
if allRegisteredInstancesOfThisInputMethod.isEmpty {
let message = String(
format: NSLocalizedString(
"Cannot find input source %@ after registration.", comment: ""
) + "(#D41J0U8U)",
imeIdentifier
)
NSLog(message)
}
}
var mainInputSourceEnabled = false
allRegisteredInstancesOfThisInputMethod.forEach { neta in
let isActivated = neta.isActivated
defer {
// 使
// 使使使
mainInputSourceEnabled = mainInputSourceEnabled || isActivated
}
if isActivated { return }
// WARNING: macOS 12 may return false positives, hence forced activation.
if neta.activate() {
NSLog("Input method enabled: \(imeIdentifier)")
} else {
NSLog("Failed to enable input method: \(imeIdentifier)")
}
}
// Alert Panel
if warning {
currentAlertContent = .postInstallAttention
} else if !mainInputSourceEnabled {
currentAlertContent = .postInstallWarning
} else {
currentAlertContent = .postInstallOK
}
isShowingPostInstallNotification = true
NSApp.terminateWithDelay()
}
}

View File

@ -0,0 +1,366 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21225" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21225"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="494" id="495"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<menu title="AMainMenu" systemMenu="main" id="29">
<items>
<menuItem title="vChewing Installer" id="56">
<menu key="submenu" title="vChewing Installer" systemMenu="apple" id="57">
<items>
<menuItem title="About vChewing Installer" id="58">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-2" id="142"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="236">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Services" id="131">
<menu key="submenu" title="Services" systemMenu="services" id="130"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="144">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Hide vChewing Installer" keyEquivalent="h" id="134">
<connections>
<action selector="hide:" target="-1" id="367"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="145">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="368"/>
</connections>
</menuItem>
<menuItem title="Show All" id="150">
<connections>
<action selector="unhideAllApplications:" target="-1" id="370"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="149">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Quit vChewing Installer" keyEquivalent="q" id="136">
<connections>
<action selector="terminate:" target="-3" id="449"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="File" id="83"/>
<menuItem title="Edit" id="LJX-Bb-mhU">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="6QY-kP-PQQ">
<items>
<menuItem title="Undo" keyEquivalent="z" id="5tq-5G-Yoy">
<connections>
<action selector="undo:" target="-1" id="P9n-jj-WpM"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="GRe-Pk-1EX">
<connections>
<action selector="redo:" target="-1" id="cbT-AB-slM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="cYt-uT-CAh"/>
<menuItem title="Cut" keyEquivalent="x" id="zAh-7y-AvL">
<connections>
<action selector="cut:" target="-1" id="arZ-EA-CgM"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="WoU-zb-uKy">
<connections>
<action selector="copy:" target="-1" id="0JC-Jc-0Xl"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="Fid-E7-Ykc">
<connections>
<action selector="paste:" target="-1" id="fVk-V0-Sbq"/>
</connections>
</menuItem>
<menuItem title="Delete" id="Ier-IT-JZa">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="X7x-wD-fWC"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="ZsT-7a-SE6">
<connections>
<action selector="selectAll:" target="-1" id="iwd-aI-lml"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
<point key="canvasLocation" x="139" y="154"/>
</menu>
<window title="vChewing Installer" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" animationBehavior="default" titlebarAppearsTransparent="YES" id="371">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
<rect key="contentRect" x="335" y="390" width="533" height="457"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="875"/>
<value key="minSize" type="size" width="533" height="457"/>
<value key="maxSize" type="size" width="533" height="457"/>
<view key="contentView" id="372">
<rect key="frame" x="0.0" y="0.0" width="533" height="457"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" imageHugsTitle="YES" translatesAutoresizingMaskIntoConstraints="NO" id="575">
<rect key="frame" x="378" y="101" width="147" height="32"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="133" id="S4F-Qe-xuk"/>
</constraints>
<buttonCell key="cell" type="push" title="I Accept" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="576">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" size="13" name="Tahoma-Bold"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
</buttonCell>
<connections>
<action selector="agreeAndInstallAction:" target="494" id="708"/>
</connections>
</button>
<scrollView horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" findBarPosition="belowContent" translatesAutoresizingMaskIntoConstraints="NO" id="YCR-wo-M5a">
<rect key="frame" x="91" y="165" width="427" height="196"/>
<clipView key="contentView" drawsBackground="NO" id="NrY-FL-PVu" userLabel="appEULAContentClip">
<rect key="frame" x="1" y="1" width="425" height="194"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView editable="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" findStyle="bar" smartInsertDelete="YES" id="47J-tO-8TZ" userLabel="appEULAContent">
<rect key="frame" x="0.0" y="-2" width="425" height="194"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES" heightSizable="YES" flexibleMaxY="YES"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<size key="minSize" width="425" height="194"/>
<size key="maxSize" width="427" height="10000000"/>
<attributedString key="textStorage">
<fragment content="Placeholder for EULA Texts.">
<attributes>
<color key="NSColor" name="textColor" catalog="System" colorSpace="catalog"/>
<font key="NSFont" metaFont="systemLight" size="11"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0"/>
</attributes>
</fragment>
</attributedString>
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
</textView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="YOZ-MC-EF2">
<rect key="frame" x="-100" y="-100" width="240" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" controlSize="mini" horizontal="NO" id="E5B-3B-faV">
<rect key="frame" x="412" y="1" width="14" height="194"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ir5-sQ-sJc">
<rect key="frame" x="89" y="443" width="130" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="vChewing for macOS" id="GNc-8S-1VG" userLabel="appNameLabel">
<font key="font" size="12" name="Tahoma-Bold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="bzR-Oa-BZa">
<rect key="frame" x="89" y="428" width="362" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Was derived from OpenVanilla McBopopmofo Project (MIT-License)." id="QYf-Nf-hoi">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="03l-rN-zf9">
<rect key="frame" x="89" y="413" width="297" height="14"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="293" id="v2b-OK-WGD"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" title="Placeholder for showing copyright information." id="eo3-TK-0rB" userLabel="appCopyrightLabel">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="XLb-mv-73s">
<rect key="frame" x="89" y="391" width="431" height="14"/>
<textFieldCell key="cell" title="Placeholder for detailed credits." id="VW8-s5-Wpn">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="Yyh-Nw-Sba">
<rect key="frame" x="15" y="137" width="503" height="5"/>
</box>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="k5O-zZ-gQY">
<rect key="frame" x="89" y="369" width="431" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="MIT-NTL License:" id="AVS-ih-FXM">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="miu-08-dZk">
<rect key="frame" x="13" y="47" width="360" height="84"/>
<constraints>
<constraint firstAttribute="width" constant="356" id="pu3-zr-hJy"/>
</constraints>
<textFieldCell key="cell" id="Q9M-ni-kUM">
<font key="font" size="12" name="Tahoma"/>
<string key="title">DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database.</string>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView translatesAutoresizingMaskIntoConstraints="NO" id="Ked-gt-bjE">
<rect key="frame" x="15" y="147" width="63" height="310"/>
<constraints>
<constraint firstAttribute="width" constant="63" id="fgC-vo-Ho8"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyDown" image="AboutBanner" id="akk-zO-Abm"/>
</imageView>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="z1m-8k-Z63">
<rect key="frame" x="218" y="443" width="126" height="14"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="122" id="yKq-Fv-W1J"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" title="version_placeholder" id="JRP-At-H9q" userLabel="appVersionLabel">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nul-TQ-gOI">
<rect key="frame" x="89" y="148" width="431" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="By installing the software, you must accept the terms above." id="mf8-6e-z7X">
<font key="font" size="12" name="Tahoma-Bold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" imageHugsTitle="YES" translatesAutoresizingMaskIntoConstraints="NO" id="592">
<rect key="frame" x="378" y="74" width="147" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="593">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" size="13" name="Tahoma"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="cancelAction:" target="494" id="707"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="575" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="miu-08-dZk" secondAttribute="trailing" constant="8" symbolic="YES" id="18X-Qf-xUC"/>
<constraint firstItem="Yyh-Nw-Sba" firstAttribute="top" secondItem="Ked-gt-bjE" secondAttribute="bottom" constant="7" id="1Hx-8o-xpF"/>
<constraint firstItem="Yyh-Nw-Sba" firstAttribute="top" secondItem="nul-TQ-gOI" secondAttribute="bottom" constant="8" symbolic="YES" id="1Mz-Yp-lqA"/>
<constraint firstItem="575" firstAttribute="leading" secondItem="592" secondAttribute="leading" id="2Kf-DA-DXH"/>
<constraint firstItem="Ked-gt-bjE" firstAttribute="leading" secondItem="372" secondAttribute="leading" constant="15" id="2ne-pt-ddK"/>
<constraint firstItem="03l-rN-zf9" firstAttribute="leading" secondItem="XLb-mv-73s" secondAttribute="leading" id="6Mv-X8-W55"/>
<constraint firstItem="nul-TQ-gOI" firstAttribute="trailing" secondItem="Yyh-Nw-Sba" secondAttribute="trailing" id="6yu-Wm-g26"/>
<constraint firstItem="592" firstAttribute="top" secondItem="575" secondAttribute="bottom" constant="7" id="7Q7-30-gTO"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="bzR-Oa-BZa" secondAttribute="trailing" constant="20" symbolic="YES" id="8td-FZ-tnM"/>
<constraint firstItem="ir5-sQ-sJc" firstAttribute="baseline" secondItem="z1m-8k-Z63" secondAttribute="baseline" id="9AX-QJ-G9U"/>
<constraint firstItem="ir5-sQ-sJc" firstAttribute="leading" secondItem="Ked-gt-bjE" secondAttribute="trailing" constant="13" id="Brw-UI-0WK"/>
<constraint firstItem="k5O-zZ-gQY" firstAttribute="trailing" secondItem="YCR-wo-M5a" secondAttribute="trailing" id="DfC-Ke-tb5"/>
<constraint firstItem="k5O-zZ-gQY" firstAttribute="leading" secondItem="YCR-wo-M5a" secondAttribute="leading" id="FIo-Op-SV8"/>
<constraint firstItem="Yyh-Nw-Sba" firstAttribute="leading" secondItem="miu-08-dZk" secondAttribute="leading" id="H4v-4O-xZY"/>
<constraint firstItem="YCR-wo-M5a" firstAttribute="trailing" secondItem="nul-TQ-gOI" secondAttribute="trailing" id="HX6-hi-PJs"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="z1m-8k-Z63" secondAttribute="trailing" constant="20" symbolic="YES" id="Hg9-8d-O7s"/>
<constraint firstItem="z1m-8k-Z63" firstAttribute="leading" secondItem="ir5-sQ-sJc" secondAttribute="trailing" constant="3" id="LSc-gD-CbY"/>
<constraint firstItem="575" firstAttribute="top" secondItem="Yyh-Nw-Sba" secondAttribute="bottom" constant="11" id="Nw2-bH-vTF"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="03l-rN-zf9" secondAttribute="trailing" constant="20" symbolic="YES" id="PRC-Y1-rIz"/>
<constraint firstItem="Ked-gt-bjE" firstAttribute="leading" secondItem="Yyh-Nw-Sba" secondAttribute="leading" id="SKi-gn-JeS"/>
<constraint firstItem="XLb-mv-73s" firstAttribute="trailing" secondItem="k5O-zZ-gQY" secondAttribute="trailing" id="VOo-Q9-rki"/>
<constraint firstItem="Ked-gt-bjE" firstAttribute="top" secondItem="372" secondAttribute="top" id="WPX-gk-uqh"/>
<constraint firstItem="XLb-mv-73s" firstAttribute="top" secondItem="03l-rN-zf9" secondAttribute="bottom" constant="8" symbolic="YES" id="bJX-f1-1PU"/>
<constraint firstItem="Ked-gt-bjE" firstAttribute="top" secondItem="ir5-sQ-sJc" secondAttribute="top" id="caT-rm-xEa"/>
<constraint firstItem="bzR-Oa-BZa" firstAttribute="top" secondItem="ir5-sQ-sJc" secondAttribute="bottom" constant="1" id="dyJ-9C-f56"/>
<constraint firstItem="bzR-Oa-BZa" firstAttribute="leading" secondItem="03l-rN-zf9" secondAttribute="leading" id="etY-2E-2Sa"/>
<constraint firstItem="YCR-wo-M5a" firstAttribute="leading" secondItem="nul-TQ-gOI" secondAttribute="leading" id="fl0-wm-8Pa"/>
<constraint firstItem="miu-08-dZk" firstAttribute="top" secondItem="Yyh-Nw-Sba" secondAttribute="bottom" constant="8" symbolic="YES" id="lY7-Se-lpo"/>
<constraint firstItem="XLb-mv-73s" firstAttribute="leading" secondItem="k5O-zZ-gQY" secondAttribute="leading" id="qzW-qc-9yQ"/>
<constraint firstItem="575" firstAttribute="trailing" secondItem="592" secondAttribute="trailing" id="sIp-L2-QLj"/>
<constraint firstItem="575" firstAttribute="trailing" secondItem="Yyh-Nw-Sba" secondAttribute="trailing" id="tap-mY-bvB"/>
<constraint firstItem="nul-TQ-gOI" firstAttribute="top" secondItem="YCR-wo-M5a" secondAttribute="bottom" constant="3" id="tqc-zq-Egb"/>
<constraint firstItem="YCR-wo-M5a" firstAttribute="top" secondItem="k5O-zZ-gQY" secondAttribute="bottom" constant="8" symbolic="YES" id="u3L-Nh-ELP"/>
<constraint firstItem="k5O-zZ-gQY" firstAttribute="top" secondItem="XLb-mv-73s" secondAttribute="bottom" constant="8" symbolic="YES" id="uxg-3X-XtF"/>
<constraint firstItem="ir5-sQ-sJc" firstAttribute="leading" secondItem="bzR-Oa-BZa" secondAttribute="leading" id="vkK-Vc-WOf"/>
<constraint firstItem="Yyh-Nw-Sba" firstAttribute="centerX" secondItem="372" secondAttribute="centerX" id="xqG-pX-j0d"/>
<constraint firstItem="03l-rN-zf9" firstAttribute="top" secondItem="bzR-Oa-BZa" secondAttribute="bottom" constant="1" id="yKI-MD-nsC"/>
</constraints>
</view>
<connections>
<outlet property="delegate" destination="494" id="706"/>
</connections>
<point key="canvasLocation" x="-306.5" y="-230"/>
</window>
<customObject id="494" customClass="AppDelegate">
<connections>
<outlet property="appCopyrightLabel" destination="03l-rN-zf9" id="XS5-cZ-k9H"/>
<outlet property="appEULAContent" destination="47J-tO-8TZ" id="kRU-X2-8kX"/>
<outlet property="appVersionLabel" destination="z1m-8k-Z63" id="75X-uy-0Iz"/>
<outlet property="cancelButton" destination="592" id="710"/>
<outlet property="installButton" destination="575" id="709"/>
<outlet property="progressIndicator" destination="deb-uT-yNv" id="Cpk-6Z-0rj"/>
<outlet property="progressSheet" destination="gHl-Hx-eQn" id="gD4-XO-YO1"/>
<outlet property="window" destination="371" id="532"/>
</connections>
</customObject>
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="gHl-Hx-eQn">
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="283" y="305" width="480" height="180"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="875"/>
<view key="contentView" id="wAe-c8-Vh9">
<rect key="frame" x="0.0" y="0.0" width="480" height="180"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<progressIndicator wantsLayer="YES" maxValue="1" style="bar" translatesAutoresizingMaskIntoConstraints="NO" id="deb-uT-yNv">
<rect key="frame" x="20" y="67" width="440" height="20"/>
</progressIndicator>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VDL-Yq-heb">
<rect key="frame" x="18" y="94" width="444" height="17"/>
<constraints>
<constraint firstAttribute="height" constant="17" id="MLj-KG-mL8"/>
</constraints>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Stopping the old version. This may take up to one minute…" id="nTo-dx-qfZ">
<font key="font" size="13" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="VDL-Yq-heb" firstAttribute="trailing" secondItem="deb-uT-yNv" secondAttribute="trailing" id="DCe-Xh-ee1"/>
<constraint firstItem="deb-uT-yNv" firstAttribute="top" secondItem="VDL-Yq-heb" secondAttribute="bottom" constant="8" symbolic="YES" id="HUE-gU-UFS"/>
<constraint firstItem="VDL-Yq-heb" firstAttribute="top" secondItem="wAe-c8-Vh9" secondAttribute="top" constant="69" id="IwI-63-e9H"/>
<constraint firstItem="VDL-Yq-heb" firstAttribute="leading" secondItem="deb-uT-yNv" secondAttribute="leading" id="UUz-sT-D9I"/>
<constraint firstItem="VDL-Yq-heb" firstAttribute="leading" secondItem="wAe-c8-Vh9" secondAttribute="leading" constant="20" symbolic="YES" id="Vgg-bw-6wt"/>
<constraint firstAttribute="trailing" secondItem="VDL-Yq-heb" secondAttribute="trailing" constant="20" symbolic="YES" id="ft0-oZ-8HD"/>
</constraints>
</view>
<point key="canvasLocation" x="529" y="-282"/>
</window>
<customObject id="420" customClass="NSFontManager"/>
</objects>
<resources>
<image name="AboutBanner" width="63" height="310"/>
</resources>
</document>

View File

@ -1,30 +1,19 @@
"Abort" = "Abort";
"Attention" = "Attention";
"vChewing Input Method" = "vChewing Input Method";
"Upgrade" = "Accept & Upgrade";
"Cancel" = "Cancel";
"Cannot activate the input method." = "Cannot activate the input method.";
"Cannot copy the file to the destination." = "Cannot copy the file to the destination.";
"Cannot find input source %@ after registration." = "Cannot find input source %@ after registration.";
"Cannot register input source %@ at %@." = "Cannot register input source %@ at %@.";
"Continue" = "Continue";
"Fatal Error" = "Fatal Error";
"i18n:installer.ACCEPT_INSTALLATION" = "I Accept";
"i18n:installer.APP_DERIVED_FROM" = "Was derived from OpenVanilla McBopopmofo Project (MIT-License).";
"i18n:installer.APP_NAME" = "vChewing for macOS";
"i18n:installer.CANCEL_INSTALLATION" = "Cancel";
"i18n:installer.DEV_CREW" = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nWalking algorithm by Lukhnos Liu (from Gramambular 2, MIT-License).";
"i18n:installer.DISCLAIMER_TEXT" = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database.";
"i18n:installer.DO_APP_UPGRADE" = "Accept & Upgrade";
"i18n:installer.EULA_PROMPT_NOTICE" = "By installing the software, you must accept the terms above.";
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "vChewing Installer";
"i18n:installer.INSTALLER_APP_TITLE" = "vChewing Installer";
"i18n:installer.LICENSE_TITLE" = "MIT-NTL License:";
"i18n:installer.STOPPING_THE_OLD_VERSION" = "Stopping the old version. This may take up to one minute…";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.";
"Install Failed" = "Install Failed";
"Installation Successful" = "Installation Successful";
"OK" = "OK";
"Stopping the old version. This may take up to one minute…" = "Stopping the old version. This may take up to one minute…";
"vChewing Input Method" = "vChewing Input Method";
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.";
"Stopping the old version. This may take up to one minute…" = "Stopping the old version. This may take up to one minute…";
"Attention" = "Attention";
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing is upgraded, but please log out or reboot for the new version to be fully functional.";
"Fatal Error" = "Fatal Error";
"Abort" = "Abort";
"Cannot register input source %@ at %@." = "Cannot register input source %@ at %@.";
"Cannot find input source %@ after registration." = "Cannot find input source %@ after registration.";
"Warning" = "Warning";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.";
"Continue" = "Continue";

View File

@ -0,0 +1,72 @@
/* Class = "NSMenu"; title = "AMainMenu"; ObjectID = "29"; */
"29.title" = "AMainMenu";
/* Class = "NSMenuItem"; title = "vChewing Installer"; ObjectID = "56"; */
"56.title" = "vChewing Installer";
/* Class = "NSMenu"; title = "vChewing Installer"; ObjectID = "57"; */
"57.title" = "vChewing Installer";
/* Class = "NSMenuItem"; title = "About vChewing Installer"; ObjectID = "58"; */
"58.title" = "About vChewing Installer";
/* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */
"83.title" = "File";
/* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */
"130.title" = "Services";
/* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */
"131.title" = "Services";
/* Class = "NSMenuItem"; title = "Hide vChewing Installer"; ObjectID = "134"; */
"134.title" = "Hide vChewing Installer";
/* Class = "NSMenuItem"; title = "Quit vChewing Installer"; ObjectID = "136"; */
"136.title" = "Quit vChewing Installer";
/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */
"145.title" = "Hide Others";
/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */
"150.title" = "Show All";
/* Class = "NSWindow"; title = "vChewing Installer"; ObjectID = "371"; */
"371.title" = "vChewing Installer";
/* Class = "NSButtonCell"; title = "I Accept"; ObjectID = "576"; */
"576.title" = "I Accept";
/* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "593"; */
"593.title" = "Cancel";
/* Class = "NSTextFieldCell"; title = "MIT-NTL License:"; ObjectID = "AVS-ih-FXM"; */
"AVS-ih-FXM.title" = "MIT-NTL License:";
/* Class = "NSTextFieldCell"; title = "vChewing for macOS"; ObjectID = "GNc-8S-1VG"; */
"GNc-8S-1VG.title" = "vChewing for macOS";
/* Class = "NSTextFieldCell"; title = "version_placeholder"; ObjectID = "JRP-At-H9q"; */
// "JRP-At-H9q.title" = "version_placeholder";
/* Class = "NSTextFieldCell"; title = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database."; ObjectID = "Q9M-ni-kUM"; */
"Q9M-ni-kUM.title" = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database.";
/* Class = "NSTextFieldCell"; title = "Was derived from OpenVanilla McBopopmofo Project (MIT-License)."; ObjectID = "QYf-Nf-hoi"; */
"QYf-Nf-hoi.title" = "Was derived from OpenVanilla McBopopmofo Project (MIT-License).";
/* Class = "NSTextFieldCell"; title = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nWalking algorithm by Lukhnos Liu (from Gramambular 2, MIT-License)."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nWalking algorithm by Lukhnos Liu (from Gramambular 2, MIT-License).\nApp-style installer is derived from OpenVanilla (MIT-License).";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
// "eo3-TK-0rB.title" = "Placeholder for showing copyright information.";
/* Class = "NSWindow"; title = "Window"; ObjectID = "gHl-Hx-eQn"; */
"gHl-Hx-eQn.title" = "Window";
/* Class = "NSTextFieldCell"; title = "By installing the software, click the \"I Accept\" to the terms above:"; ObjectID = "mf8-6e-z7X"; */
"mf8-6e-z7X.title" = "By installing the software, you must accept the terms above.";
/* Class = "NSTextFieldCell"; title = "Stopping the old version. This may take up to one minute…"; ObjectID = "nTo-dx-qfZ"; */
"nTo-dx-qfZ.title" = "Stopping the old version. This may take up to one minute…";

View File

@ -1,30 +1,19 @@
"Abort" = "中止";
"Attention" = "ご注意";
"vChewing Input Method" = "威注音入力アプリ";
"Upgrade" = "承認と更新";
"Cancel" = "取消";
"Cannot activate the input method." = "入力アプリ、起動失敗。";
"Cannot copy the file to the destination." = "目標へファイルのコピーできません。";
"Cannot find input source %@ after registration." = "登録済みですが「%@」は見つけませんでした。";
"Cannot register input source %@ at %@." = "「%2$@」で入力アプリ「\"%1$@\"」の実装は失敗しました。";
"Continue" = "続行";
"Fatal Error" = "致命錯乱";
"i18n:installer.ACCEPT_INSTALLATION" = "承認する";
"i18n:installer.APP_DERIVED_FROM" = "曾て OpenVanilla 小麦注音プロジェクト (MIT-License) から派生。";
"i18n:installer.APP_NAME" = "vChewing for macOS";
"i18n:installer.CANCEL_INSTALLATION" = "取消";
"i18n:installer.DEV_CREW" = "macOS 版威注音の開発Shiki Suen, Isaac Xen, Hiraku Wang, など。\n威注音語彙データの維持Shiki Suen。\nウォーキング算法Lukhnos Liu (Gramambular 2, MIT-License)。";
"i18n:installer.DISCLAIMER_TEXT" = "免責事項vChewing Project は、OpenVanilla と協力関係や提携関係にあるわけではなく、OpenVanilla が小麦注音プロジェクトに同梱した辞書データについて、vChewing Project は一切責任負い兼ねる。特定な地政学的・観念形態的な内容は、vChewing アプリの世界的な普及に妨害する恐れがあるため、vChewing 公式辞書データに不収録。";
"i18n:installer.DO_APP_UPGRADE" = "承認と更新";
"i18n:installer.EULA_PROMPT_NOTICE" = "このアプリを実装するために、上記の条約を承認すべきである。";
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "威注音入力 実装用アプリ";
"i18n:installer.INSTALLER_APP_TITLE" = "威注音入力 実装用アプリ";
"i18n:installer.LICENSE_TITLE" = "MIT商標不許可ライセンス (MIT-NTL License):";
"i18n:installer.STOPPING_THE_OLD_VERSION" = "古いバージョンを強制停止中。1分かかると恐れ入りますが……";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "入力アプリの自動起動はうまく出来なかったかもしれません。ご自分で「システム環境設定→キーボード→入力ソース」で起動してください。";
"Install Failed" = "実装失敗。";
"Installation Successful" = "実装完了";
"OK" = "うむ";
"Stopping the old version. This may take up to one minute…" = "古いバージョンを強制停止中。1分かかると恐れ入りますが……";
"vChewing Input Method" = "威注音入力アプリ";
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音入力、利用準備完了。\n\nこのシステムユーザーアカウントで初めて実装した場合、再ログインしてください。";
"Stopping the old version. This may take up to one minute…" = "古いバージョンを強制停止中。1分かかると恐れ入りますが……";
"Attention" = "ご注意";
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "威注音入力の更新は実装完了しましたが、うまく作動できるために、このパソコンの再起動および再ログインが必要だと恐れ入ります。";
"Fatal Error" = "致命錯乱";
"Abort" = "中止";
"Cannot register input source %@ at %@." = "「%2$@」で入力アプリ「\"%1$@\"」の実装は失敗しました。";
"Cannot find input source %@ after registration." = "登録済みですが「%@」は見つけませんでした。";
"Warning" = "お知らせ";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "入力アプリの自動起動はうまく出来なかったかもしれません。ご自分で「システム環境設定→キーボード→入力ソース」で起動してください。";
"Continue" = "続行";

View File

@ -0,0 +1,72 @@
/* Class = "NSMenu"; title = "AMainMenu"; ObjectID = "29"; */
"29.title" = "AMainMenu";
/* Class = "NSMenuItem"; title = "vChewing Installer"; ObjectID = "56"; */
"56.title" = "威注音入力 実装用アプリ";
/* Class = "NSMenu"; title = "vChewing Installer"; ObjectID = "57"; */
"57.title" = "威注音入力 実装用アプリ";
/* Class = "NSMenuItem"; title = "About vChewing Installer"; ObjectID = "58"; */
"58.title" = "威注音入力 実装用アプリについて…";
/* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */
"83.title" = "ファイル";
/* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */
"130.title" = "サービス";
/* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */
"131.title" = "サービス";
/* Class = "NSMenuItem"; title = "Hide vChewing Installer"; ObjectID = "134"; */
"134.title" = "全ウィンドウ隠す";
/* Class = "NSMenuItem"; title = "Quit vChewing Installer"; ObjectID = "136"; */
"136.title" = "威注音入力 実装用アプリ を終了";
/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */
"145.title" = "他のアプリのウィンドウを隠す";
/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */
"150.title" = "隠したウィンドウを全部表示する";
/* Class = "NSWindow"; title = "vChewing Installer"; ObjectID = "371"; */
"371.title" = "威注音入力 実装用アプリ";
/* Class = "NSButtonCell"; title = "I Accept"; ObjectID = "576"; */
"576.title" = "承認する";
/* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "593"; */
"593.title" = "取消";
/* Class = "NSTextFieldCell"; title = "3-Clause BSD License:"; ObjectID = "AVS-ih-FXM"; */
"AVS-ih-FXM.title" = "MIT商標不許可ライセンス (MIT-NTL License):";
/* Class = "NSTextFieldCell"; title = "vChewing for macOS"; ObjectID = "GNc-8S-1VG"; */
"GNc-8S-1VG.title" = "vChewing for macOS";
/* Class = "NSTextFieldCell"; title = "version_placeholder"; ObjectID = "JRP-At-H9q"; */
"JRP-At-H9q.title" = "version_placeholder";
/* Class = "NSTextFieldCell"; title = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database."; ObjectID = "Q9M-ni-kUM"; */
"Q9M-ni-kUM.title" = "免責事項vChewing Project は、OpenVanilla と協力関係や提携関係にあるわけではなく、OpenVanilla が小麦注音プロジェクトに同梱した辞書データについて、vChewing Project は一切責任負い兼ねる。特定な地政学的・観念形態的な内容は、vChewing アプリの世界的な普及に妨害する恐れがあるため、vChewing 公式辞書データに不収録。";
/* Class = "NSTextFieldCell"; title = "Was derived from OpenVanilla McBopopmofo Project (MIT-License)."; ObjectID = "QYf-Nf-hoi"; */
"QYf-Nf-hoi.title" = "曾て OpenVanilla 小麦注音プロジェクト (MIT-License) から派生。";
/* Class = "NSTextFieldCell"; title = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nWalking algorithm by Lukhnos Liu (from Gramambular 2, MIT-License)."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "macOS 版威注音の開発Shiki Suen, Isaac Xen, Hiraku Wang, など。\n威注音語彙データの維持Shiki Suen。\nウォーキング算法Lukhnos Liu (Gramambular 2, MIT-License)。\nApp フォーマットで出来た実装アプリは OpenVanilla (MIT-License) から受け継ぎたものである。";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
"eo3-TK-0rB.title" = "Placeholder for showing copyright information.";
/* Class = "NSWindow"; title = "Window"; ObjectID = "gHl-Hx-eQn"; */
"gHl-Hx-eQn.title" = "Window";
/* Class = "NSTextFieldCell"; title = "By installing the software, you must accept the terms above."; ObjectID = "mf8-6e-z7X"; */
"mf8-6e-z7X.title" = "このアプリを実装するために、上記の条約を承認すべきである。";
/* Class = "NSTextFieldCell"; title = "Stopping the old version. This may take up to one minute…"; ObjectID = "nTo-dx-qfZ"; */
"nTo-dx-qfZ.title" = "古いバージョンを強制停止中。1分かかると恐れ入りますが……";

View File

@ -1,30 +1,19 @@
"Abort" = "放弃安装";
"Attention" = "请注意";
"vChewing Input Method" = "威注音输入法";
"Upgrade" = "接受并升级";
"Cancel" = "取消";
"Cannot activate the input method." = "无法启用输入法。";
"Cannot copy the file to the destination." = "无法将输入法拷贝至目的地。";
"Cannot find input source %@ after registration." = "在注册完输入法 \"%@\" 之后仍然无法找到该输入法。";
"Cannot register input source %@ at %@." = "无法从档案位置 %2$@ 安装输入法 \"%1$@\"。";
"Continue" = "继续";
"Fatal Error" = "安装错误";
"i18n:installer.ACCEPT_INSTALLATION" = "我接受";
"i18n:installer.APP_DERIVED_FROM" = "该专案曾由 OpenVanilla 小麦注音专案 (MIT-License) 衍生而来。";
"i18n:installer.APP_NAME" = "vChewing for macOS";
"i18n:installer.CANCEL_INSTALLATION" = "取消安装";
"i18n:installer.DEV_CREW" = "威注音 macOS 程式研发Shiki Suen, Isaac Xen, Hiraku Wang, 等。\n威注音词库维护Shiki Suen。\n爬轨算法Lukhnos Liu (Gramambular 2, MIT-License)。";
"i18n:installer.DISCLAIMER_TEXT" = "免责声明:威注音专案对小麦注音官方专案内赠的小麦注音原版词库内容不负任何责任。威注音输入法专用的威注音官方词库不包含任何「会在法理上妨碍威注音在全球传播」的「与地缘政治及政治意识形态有关的」内容。威注音专案与 OpenVanilla 专案之间无合作关系、无隶属关系。";
"i18n:installer.DO_APP_UPGRADE" = "接受并升级";
"i18n:installer.EULA_PROMPT_NOTICE" = "若要安装该软件,请接受上述条款。";
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "威注音输入法安装程式";
"i18n:installer.INSTALLER_APP_TITLE" = "威注音安装程式";
"i18n:installer.LICENSE_TITLE" = "麻理去商标授权合约 (MIT-NTL License):";
"i18n:installer.STOPPING_THE_OLD_VERSION" = "等待旧版完全停用,大约需要一分钟…";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "输入法已经安装好,但可能没有完全启用。请从「系统偏好设定」 > 「键盘」 > 「输入方式」分页加入输入法。";
"Install Failed" = "安装失败";
"Installation Successful" = "安装成功";
"OK" = "确定";
"Stopping the old version. This may take up to one minute…" = "正在试图结束正在运行的旧版输入法,大概需要一分钟…";
"vChewing Input Method" = "威注音输入法";
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音输入法安装成功。\n\n若是在當前使用者帳戶內首次安裝的話請重新登入。";
"Stopping the old version. This may take up to one minute…" = "正在试图结束正在运行的旧版输入法,大概需要一分钟…";
"Attention" = "请注意";
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing 安装完成,但建议您登出或重新开机,以便顺利使用新版。";
"Fatal Error" = "安装错误";
"Abort" = "放弃安装";
"Cannot register input source %@ at %@." = "无法从档案位置 %2$@ 安装输入法 \"%1$@\"。";
"Cannot find input source %@ after registration." = "在注册完输入法 \"%@\" 仍然无法找到输入法。";
"Warning" = "安装不完整";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "输入法已经安装好,但可能没有完全启用。请从「系统偏好设定」 > 「键盘」 > 「输入方式」分页加入输入法。";
"Continue" = "继续";

View File

@ -0,0 +1,72 @@
/* Class = "NSMenu"; title = "AMainMenu"; ObjectID = "29"; */
"29.title" = "AMainMenu";
/* Class = "NSMenuItem"; title = "vChewing Installer"; ObjectID = "56"; */
"56.title" = "威注音安装程式";
/* Class = "NSMenu"; title = "vChewing Installer"; ObjectID = "57"; */
"57.title" = "威注音安装程式";
/* Class = "NSMenuItem"; title = "About vChewing Installer"; ObjectID = "58"; */
"58.title" = "关于威注音安装程式";
/* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */
"83.title" = "档案";
/* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */
"130.title" = "服务";
/* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */
"131.title" = "服务";
/* Class = "NSMenuItem"; title = "Hide vChewing Installer"; ObjectID = "134"; */
"134.title" = "隐藏威注音安装程式";
/* Class = "NSMenuItem"; title = "Quit vChewing Installer"; ObjectID = "136"; */
"136.title" = "结束威注音安装程式";
/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */
"145.title" = "隐藏其他程式";
/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */
"150.title" = "显示所有程式";
/* Class = "NSWindow"; title = "vChewing Installer"; ObjectID = "371"; */
"371.title" = "威注音输入法安装程式";
/* Class = "NSButtonCell"; title = "I Accept"; ObjectID = "576"; */
"576.title" = "我接受";
/* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "593"; */
"593.title" = "取消安装";
/* Class = "NSTextFieldCell"; title = "MIT-NTL License:"; ObjectID = "AVS-ih-FXM"; */
"AVS-ih-FXM.title" = "麻理去商标授权合约 (MIT-NTL License):";
/* Class = "NSTextFieldCell"; title = "vChewing for macOS"; ObjectID = "GNc-8S-1VG"; */
"GNc-8S-1VG.title" = "vChewing for macOS";
/* Class = "NSTextFieldCell"; title = "version_placeholder"; ObjectID = "JRP-At-H9q"; */
// "JRP-At-H9q.title" = "version_placeholder";
/* Class = "NSTextFieldCell"; title = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database."; ObjectID = "Q9M-ni-kUM"; */
"Q9M-ni-kUM.title" = "免责声明:威注音专案对小麦注音官方专案内赠的小麦注音原版词库内容不负任何责任。威注音输入法专用的威注音官方词库不包含任何「会在法理上妨碍威注音在全球传播」的「与地缘政治及政治意识形态有关的」内容。威注音专案与 OpenVanilla 专案之间无合作关系、无隶属关系。";
/* Class = "NSTextFieldCell"; title = "Was derived from OpenVanilla McBopopmofo Project (MIT-License)."; ObjectID = "QYf-Nf-hoi"; */
"QYf-Nf-hoi.title" = "该专案曾由 OpenVanilla 小麦注音专案 (MIT-License) 衍生而来。";
/* Class = "NSTextFieldCell"; title = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nWalking algorithm by Lukhnos Liu (from Gramambular 2, MIT-License)."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "威注音 macOS 程式研发Shiki Suen, Isaac Xen, Hiraku Wang, 等。\n威注音词库维护Shiki Suen。\n爬轨算法Lukhnos Liu (Gramambular 2, MIT-License)。\nApp 格式的安装程式继承自 OpenVanilla (MIT-License)。";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
// "eo3-TK-0rB.title" = "Placeholder for showing copyright information.";
/* Class = "NSWindow"; title = "Window"; ObjectID = "gHl-Hx-eQn"; */
"gHl-Hx-eQn.title" = "视窗";
/* Class = "NSTextFieldCell"; title = "By installing the software, click the \"I Accept\" to the terms above:"; ObjectID = "mf8-6e-z7X"; */
"mf8-6e-z7X.title" = "若要安装该软件,请接受上述条款。";
/* Class = "NSTextFieldCell"; title = "Stopping the old version. This may take up to one minute…"; ObjectID = "nTo-dx-qfZ"; */
"nTo-dx-qfZ.title" = "等待旧版完全停用,大约需要一分钟…";

View File

@ -1,30 +1,19 @@
"Abort" = "放棄安裝";
"Attention" = "請注意";
"vChewing Input Method" = "威注音輸入法";
"Upgrade" = "接受並升級";
"Cancel" = "取消";
"Cannot activate the input method." = "無法啟用輸入法。";
"Cannot copy the file to the destination." = "無法將輸入法拷貝至目的地。";
"Cannot find input source %@ after registration." = "在註冊完輸入法 \"%@\" 之後仍然無法找到該輸入法。";
"Cannot register input source %@ at %@." = "無法從檔案位置 %2$@ 安裝輸入法 \"%1$@\"。";
"Continue" = "繼續";
"Fatal Error" = "安裝錯誤";
"i18n:installer.ACCEPT_INSTALLATION" = "我接受";
"i18n:installer.APP_DERIVED_FROM" = "該專案曾由 OpenVanilla 小麥注音專案 (MIT-License) 衍生而來。";
"i18n:installer.APP_NAME" = "vChewing for macOS";
"i18n:installer.CANCEL_INSTALLATION" = "取消安裝";
"i18n:installer.DEV_CREW" = "威注音 macOS 程式研發Shiki Suen, Isaac Xen, Hiraku Wang, 等。\n威注音詞庫維護Shiki Suen。\n爬軌算法Lukhnos Liu (Gramambular 2, MIT-License)。";
"i18n:installer.DISCLAIMER_TEXT" = "免責聲明:威注音專案對小麥注音官方專案內贈的小麥注音原版詞庫內容不負任何責任。威注音輸入法專用的威注音官方詞庫不包含任何「會在法理上妨礙威注音在全球傳播」的「與地緣政治及政治意識形態有關的」內容。威註音專案與 OpenVanilla 專案之間無合作關係、無隸屬關係。";
"i18n:installer.DO_APP_UPGRADE" = "接受並升級";
"i18n:installer.EULA_PROMPT_NOTICE" = "若要安裝該軟體,請接受上述條款。";
"i18n:installer.INSTALLER_APP_TITLE_FULL" = "威注音輸入法安裝程式";
"i18n:installer.INSTALLER_APP_TITLE" = "威注音安裝程式";
"i18n:installer.LICENSE_TITLE" = "麻理去商標授權合約 (MIT-NTL License):";
"i18n:installer.STOPPING_THE_OLD_VERSION" = "等待舊版完全停用,大約需要一分鐘…";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "輸入法已經安裝好,但可能沒有完全啟用。請從「系統偏好設定」 > 「鍵盤」 > 「輸入方式」分頁加入輸入法。";
"Install Failed" = "安裝失敗";
"Installation Successful" = "安裝成功";
"OK" = "確定";
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音輸入法安裝成功。\n\n若是在当前使用者帐户内首次安装的话请重新登入。";
"Stopping the old version. This may take up to one minute…" = "正在試圖結束正在運行的舊版輸入法,大概需要一分鐘…";
"vChewing Input Method" = "威注音輸入法";
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "威注音輸入法安裝成功。\n\n若是在當前使用者帳戶內首次安裝的話請重新登入。";
"Attention" = "請注意";
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing 安裝完成,但建議您登出或重新開機,以便順利使用新版。";
"Fatal Error" = "安裝錯誤";
"Abort" = "放棄安裝";
"Cannot register input source %@ at %@." = "無法從檔案位置 %2$@ 安裝輸入法 \"%1$@\"。";
"Cannot find input source %@ after registration." = "在註冊完輸入法 \"%@\" 仍然無法找到輸入法。";
"Warning" = "安裝不完整";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "輸入法已經安裝好,但可能沒有完全啟用。請從「系統偏好設定」 > 「鍵盤」 > 「輸入方式」分頁加入輸入法。";
"Continue" = "繼續";

View File

@ -0,0 +1,72 @@
/* Class = "NSMenu"; title = "AMainMenu"; ObjectID = "29"; */
"29.title" = "AMainMenu";
/* Class = "NSMenuItem"; title = "vChewing Installer"; ObjectID = "56"; */
"56.title" = "威注音安裝程式";
/* Class = "NSMenu"; title = "vChewing Installer"; ObjectID = "57"; */
"57.title" = "威注音安裝程式";
/* Class = "NSMenuItem"; title = "About vChewing Installer"; ObjectID = "58"; */
"58.title" = "關於威注音安裝程式";
/* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */
"83.title" = "檔案";
/* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */
"130.title" = "服務";
/* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */
"131.title" = "服務";
/* Class = "NSMenuItem"; title = "Hide vChewing Installer"; ObjectID = "134"; */
"134.title" = "隱藏威注音安裝程式";
/* Class = "NSMenuItem"; title = "Quit vChewing Installer"; ObjectID = "136"; */
"136.title" = "結束威注音安裝程式";
/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */
"145.title" = "隱藏其他程式";
/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */
"150.title" = "顯示所有程式";
/* Class = "NSWindow"; title = "vChewing Installer"; ObjectID = "371"; */
"371.title" = "威注音輸入法安裝程式";
/* Class = "NSButtonCell"; title = "I Accept"; ObjectID = "576"; */
"576.title" = "我接受";
/* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "593"; */
"593.title" = "取消安裝";
/* Class = "NSTextFieldCell"; title = "MIT-NTL License:"; ObjectID = "AVS-ih-FXM"; */
"AVS-ih-FXM.title" = "麻理去商標授權合約 (MIT-NTL License):";
/* Class = "NSTextFieldCell"; title = "vChewing for macOS"; ObjectID = "GNc-8S-1VG"; */
"GNc-8S-1VG.title" = "vChewing for macOS";
/* Class = "NSTextFieldCell"; title = "version_placeholder"; ObjectID = "JRP-At-H9q"; */
// "JRP-At-H9q.title" = "version_placeholder";
/* Class = "NSTextFieldCell"; title = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database."; ObjectID = "Q9M-ni-kUM"; */
"Q9M-ni-kUM.title" = "免責聲明:威注音專案對小麥注音官方專案內贈的小麥注音原版詞庫內容不負任何責任。威注音輸入法專用的威注音官方詞庫不包含任何「會在法理上妨礙威注音在全球傳播」的「與地緣政治及政治意識形態有關的」內容。威註音專案與 OpenVanilla 專案之間無合作關係、無隸屬關係。";
/* Class = "NSTextFieldCell"; title = "Was derived from OpenVanilla McBopopmofo Project (MIT-License)."; ObjectID = "QYf-Nf-hoi"; */
"QYf-Nf-hoi.title" = "該專案曾由 OpenVanilla 小麥注音專案 (MIT-License) 衍生而來。";
/* Class = "NSTextFieldCell"; title = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nWalking algorithm by Lukhnos Liu (from Gramambular 2, MIT-License)."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "威注音 macOS 程式研發Shiki Suen, Isaac Xen, Hiraku Wang, 等。\n威注音詞庫維護Shiki Suen。\n爬軌算法Lukhnos Liu (Gramambular 2, MIT-License)。\nApp 格式的安裝程式繼承自 OpenVanilla (MIT-License)。";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
// "eo3-TK-0rB.title" = "Placeholder for showing copyright information.";
/* Class = "NSWindow"; title = "Window"; ObjectID = "gHl-Hx-eQn"; */
"gHl-Hx-eQn.title" = "視窗";
/* Class = "NSTextFieldCell"; title = "By installing the software, click the \"I Accept\" to the terms above:"; ObjectID = "mf8-6e-z7X"; */
"mf8-6e-z7X.title" = "若要安裝該軟體,請接受上述條款。";
/* Class = "NSTextFieldCell"; title = "Stopping the old version. This may take up to one minute…"; ObjectID = "nTo-dx-qfZ"; */
"nTo-dx-qfZ.title" = "等待舊版完全停用,大約需要一分鐘…";

View File

@ -19,4 +19,4 @@ OS_Version=$(sw_vers -productVersion)
##### fi
# Finally, register the input method:
/Users/"${login_user}"/Library/Input\ Methods/"${TARGET}".app/Contents/MacOS/"${TARGET}" install || true
/Users/"${login_user}"/Library/Input\ Methods/"${TARGET}".app/Contents/MacOS/"${TARGET}" install --all || true

View File

@ -13,10 +13,10 @@ if [ "${login_user}" = root ]; then
rm -rf /Library/Keyboard\ Layouts/vChewing\ MiTAC.keylayout || true
fi
rm -rf ~/Library/Input\ Methods/vChewing.app || true
rm -rf ~/Library/Keyboard\ Layouts/vChewingKeyLayout.bundle || true
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ Dachen.keylayout || true
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ ETen.keylayout || true
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ FakeSeigyou.keylayout || true
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ IBM.keylayout || true
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ MiTAC.keylayout || true
rm -rf ~/Library/Input\ Methods/vChewing.app
rm -rf ~/Library/Keyboard\ Layouts/vChewingKeyLayout.bundle
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ Dachen.keylayout
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ ETen.keylayout
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ FakeSeigyou.keylayout
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ IBM.keylayout
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ MiTAC.keylayout

View File

@ -1,60 +0,0 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import SwiftUI
@main
struct vChewingInstallerApp: App {
var body: some Scene {
WindowGroup {
ZStack(alignment: .center) {
LinearGradient(
gradient: Gradient(
colors: [
Color(red: 0, green: 0, blue: 0xF4 / 255),
.black,
]
),
startPoint: .top, endPoint: .bottom
).overlay(alignment: .topLeading) {
Text("vChewing Input Method")
.font(.system(size: 30))
.italic().bold()
.padding()
.foregroundStyle(Color.white)
.shadow(color: .black, radius: 0, x: 5, y: 5)
}
MainView()
.shadow(color: .black, radius: 3, x: 0, y: 0)
}.frame(width: 1000, height: 630)
.onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
NSApp.windows.forEach { window in
window.titlebarAppearsTransparent = true
window.setContentSize(.init(width: 1000, height: 630))
window.standardWindowButton(.closeButton)?.isHidden = true
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
window.standardWindowButton(.zoomButton)?.isHidden = true
window.styleMask.remove(.resizable)
window.orderFront(self)
}
}
.onDisappear {
NSApp.terminate(self)
}
}
.commands {
CommandGroup(replacing: .newItem) {}
CommandGroup(replacing: .appInfo) {}
CommandGroup(replacing: .help) {}
CommandGroup(replacing: .appVisibility) {}
CommandGroup(replacing: .systemServices) {}
}
}
}

View File

@ -1,33 +0,0 @@
#!/usr/bin/env swift
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
let strDataPath = "./"
func handleFiles(_ handler: @escaping ((url: URL, fileName: String)) -> Void) {
let rawURLs = FileManager.default.enumerator(at: URL(fileURLWithPath: strDataPath), includingPropertiesForKeys: nil)?.compactMap { $0 as? URL }
rawURLs?.forEach { url in
guard let fileName = url.pathComponents.last, fileName.lowercased() == "localizable.strings" else { return }
handler((url, fileName))
}
}
handleFiles { url, fileName in
guard let rawStr = try? String(contentsOf: url, encoding: .utf8) else { return }
let locale = Locale(identifier: "zh@collation=stroke")
do {
try rawStr.components(separatedBy: .newlines).filter { !$0.isEmpty }.sorted {
$0.compare($1, locale: locale) == .orderedAscending
}.joined(separator: "\n").description.appending("\n").write(to: url, atomically: false, encoding: .utf8)
} catch {
print("!! Error writing to \(fileName)")
}
}

View File

@ -11,24 +11,6 @@ BUILD_SETTINGS += ARCHS="$(ARCHS)"
BUILD_SETTINGS += ONLY_ACTIVE_ARCH=NO
endif
spmDebug:
swift build -c debug --package-path ./Packages/vChewing_MainAssembly/
spmRelease:
swift build -c release --package-path ./Packages/vChewing_MainAssembly/
spmLintFormat:
make lint --file=./Packages/Makefile || true
make format --file=./Packages/Makefile || true
spmClean:
@for currentDir in $$(ls ./Packages/); do \
if [ -d $$a ]; then \
echo "processing folder $$currentDir"; \
swift package clean --package-path ./Packages/$$currentDir || true; \
fi; \
done;
release:
xcodebuild -project vChewing.xcodeproj -scheme vChewingInstaller -configuration Release $(BUILD_SETTINGS) build
@ -60,7 +42,6 @@ install-release: permission-check
.PHONY: clean
clean:
make clean --file=./Packages/Makefile || true
xcodebuild -scheme vChewingInstaller -configuration Debug $(BUILD_SETTINGS) clean
xcodebuild -scheme vChewingInstaller -configuration Release $(BUILD_SETTINGS) clean
make clean --file=./Source/Data/Makefile || true

View File

@ -1 +0,0 @@
BasedOnStyle: Microsoft

View File

@ -1,104 +0,0 @@
# SwiftFormat config compliant with Google Swift Guideline
# https://google.github.io/swift/#control-flow-statements
# Specify version used in a project
--swiftversion 5.5
# Rules explicitly required by the guideline
--rules \
blankLinesAroundMark, \
blankLinesAtEndOfScope, \
blankLinesAtStartOfScope, \
blankLinesBetweenScopes, \
braces, \
consecutiveBlankLines, \
consecutiveSpaces, \
duplicateImports, \
elseOnSameLine, \
emptyBraces, \
enumNamespaces, \
extensionAccessControl, \
hoistPatternLet, \
indent, \
leadingDelimiters, \
linebreakAtEndOfFile, \
markTypes, \
organizeDeclarations, \
redundantInit, \
redundantParens, \
redundantPattern, \
redundantRawValues, \
redundantType, \
redundantVoidReturnType, \
semicolons, \
sortedImports, \
sortedSwitchCases, \
spaceAroundBraces, \
spaceAroundBrackets, \
spaceAroundComments, \
spaceAroundGenerics, \
spaceAroundOperators, \
spaceAroundParens, \
spaceInsideBraces, \
spaceInsideBrackets, \
spaceInsideComments, \
spaceInsideGenerics, \
spaceInsideParens, \
todos, \
trailingClosures, \
trailingCommas, \
trailingSpace, \
typeSugar, \
void, \
wrap, \
wrapArguments, \
wrapAttributes, \
#
#
# Additional rules not mentioned in the guideline, but helping to keep the codebase clean
# Quoting the guideline:
# Common themes among the rules in this section are:
# avoid redundancy, avoid ambiguity, and prefer implicitness over explicitness
# unless being explicit improves readability and/or reduces ambiguity.
#
#
andOperator, \
isEmpty, \
redundantBackticks, \
redundantBreak, \
redundantExtensionACL, \
redundantGet, \
redundantLetError, \
redundantNilInit, \
redundantObjc, \
redundantReturn, \
redundantSelf, \
strongifiedSelf
# Options for basic rules
--extensionacl on-declarations
--funcattributes prev-line
--indent 2
--maxwidth 100
--typeattributes prev-line
--varattributes prev-line
--voidtype tuple
--wraparguments before-first
--wrapparameters before-first
--wrapcollections before-first
--wrapreturntype if-multiline
--wrapconditions after-first
# Option for additional rules
--self init-only
# Excluded folders
--exclude Pods,**/UNTESTED_TODO,vendor,fastlane
# https://github.com/NoemiRozpara/Google-SwiftFormat-Config

View File

@ -4,7 +4,7 @@ import PackageDescription
let package = Package(
name: "FolderMonitor",
platforms: [
.macOS(.v11),
.macOS(.v10_11),
],
products: [
.library(

View File

@ -38,15 +38,14 @@ public class FolderMonitor {
)
// Define the block to call when a file change is detected.
folderMonitorSource?.setEventHandler { [weak self] in
guard let self = self else { return }
self.folderDidChange?()
self?.folderDidChange?()
}
// Define a cancel handler to ensure the directory is closed when the source is cancelled.
folderMonitorSource?.setCancelHandler { [weak self] in
guard let self = self else { return }
close(self.monitoredFolderFileDescriptor)
self.monitoredFolderFileDescriptor = -1
self.folderMonitorSource = nil
guard let strongSelf = self else { return }
close(strongSelf.monitoredFolderFileDescriptor)
strongSelf.monitoredFolderFileDescriptor = -1
strongSelf.folderMonitorSource = nil
}
// Start monitoring the directory via the source.
folderMonitorSource?.resume()

View File

@ -4,7 +4,7 @@ import PackageDescription
let package = Package(
name: "NSAttributedTextView",
platforms: [
.macOS(.v11),
.macOS(.v10_11),
],
products: [
.library(
@ -13,18 +13,14 @@ let package = Package(
),
],
dependencies: [
.package(path: "../vChewing_OSFrameworkImpl"),
.package(path: "../vChewing_CocoaExtension"),
],
targets: [
.target(
name: "NSAttributedTextView",
dependencies: [
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
]
),
.testTarget(
name: "NSAttributedTextViewTests",
dependencies: ["NSAttributedTextView"]
),
]
)

View File

@ -6,7 +6,7 @@
// Modified by The vChewing Project in order to use it with AppKit.
import AppKit
import OSFrameworkImpl
import CocoaExtension
import SwiftUI
@available(macOS 10.15, *)
@ -42,17 +42,6 @@ public struct HText: NSViewRepresentable {
}
public class NSAttributedTextView: NSView {
private static let sharedTextField: NSTextField = {
let result = NSTextField()
result.isSelectable = false
result.isEditable = false
result.isBordered = false
result.backgroundColor = .clear
result.allowsEditingTextAttributes = false
result.preferredMaxLayoutWidth = result.frame.width
return result
}()
public enum writingDirection: String {
case horizontal
case vertical
@ -72,18 +61,6 @@ public class NSAttributedTextView: NSView {
}
}
public init() {
super.init(frame: .zero)
#if compiler(>=5.9) && canImport(AppKit, _version: "14.0")
clipsToBounds = true // View
#endif
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func attributedStringValue(areaCalculation: Bool = false) -> NSAttributedString {
var newAttributes = attributes
let isVertical: Bool = !(direction == .horizontal)
@ -111,7 +88,6 @@ public class NSAttributedTextView: NSView {
public var backgroundColor: NSColor = .controlBackgroundColor
public var attributes: [NSAttributedString.Key: Any] = [
.kern: 0,
.verticalGlyphForm: true,
.font: NSFont.systemFont(ofSize: NSFont.systemFontSize),
.foregroundColor: NSColor.textColor,
@ -132,18 +108,19 @@ public class NSAttributedTextView: NSView {
default: return attributedStringValue(areaCalculation: true)
}
}()
Self.sharedTextField.attributedStringValue = attrString
Self.sharedTextField.sizeToFit()
var textWH = Self.sharedTextField.fittingSize
var textWH = attrString.boundingDimension
if direction != .horizontal {
textWH.height = max(ceil(1.03 * textWH.height), ceil(NSFont.systemFontSize * 1.1))
textWH.height *= 1.03
textWH.height = max(textWH.height, NSFont.systemFontSize * 1.1)
textWH.height = ceil(textWH.height)
textWH = .init(width: textWH.height, height: textWH.width)
}
return .init(origin: .zero, size: textWH)
}
override public func draw(_ rect: CGRect) {
guard let currentNSGraphicsContext = NSGraphicsContext.current else { return }
let context = NSGraphicsContext.current?.cgContext
guard let context = context else { return }
let setter = CTFramesetterCreateWithAttributedString(attributedStringValue())
let path = CGPath(rect: rect, transform: nil)
let theCTFrameProgression: CTFrameProgression = {
@ -163,16 +140,7 @@ public class NSAttributedTextView: NSView {
let bgPath: NSBezierPath = .init(roundedRect: rect, xRadius: 0, yRadius: 0)
bgPath.fill()
currentRect = rect
if #unavailable(macOS 10.10) {
// NSGraphicsContext.current?.cgContext macOS 10.10 Yosemite
//
let contextPtr: Unmanaged<CGContext>? = Unmanaged.fromOpaque(currentNSGraphicsContext.graphicsPort)
let theContext: CGContext? = contextPtr?.takeUnretainedValue()
guard let theContext = theContext else { return }
CTFrameDraw(newFrame, theContext)
} else {
CTFrameDraw(newFrame, currentNSGraphicsContext.cgContext)
}
CTFrameDraw(newFrame, context)
}
}

View File

@ -1,62 +0,0 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import Foundation
@testable import NSAttributedTextView
import OSFrameworkImpl
import Shared
import XCTest
class MainAssemblyTests: XCTestCase {
func testView() throws {
let testCtl: testController = .init()
var rect = testCtl.attrView.shrinkFrame()
var bigRect = rect
bigRect.size.width += NSFont.systemFontSize
bigRect.size.height += NSFont.systemFontSize
rect.origin.x += ceil(NSFont.systemFontSize / 2)
rect.origin.y += ceil(NSFont.systemFontSize / 2)
testCtl.attrView.frame = rect
testCtl.window?.setFrame(bigRect, display: true)
testCtl.window?.orderFront(nil)
testCtl.attrView.draw(testCtl.attrView.frame)
testCtl.window?.setIsVisible(true)
}
}
class testController: NSWindowController {
var attrView: NSAttributedTextView
init() {
let contentRect = NSRect(x: 128.0, y: 128.0, width: 300.0, height: 20.0)
let styleMask: NSWindow.StyleMask = [.borderless, .nonactivatingPanel]
let panel = NSPanel(
contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false
)
panel.level = NSWindow.Level(Int(max(CGShieldingWindowLevel(), kCGPopUpMenuWindowLevel)) + 2)
panel.hasShadow = true
panel.backgroundColor = NSColor.clear
panel.isOpaque = false
panel.isMovable = false
panel.contentView?.wantsLayer = true
panel.contentView?.layer?.cornerRadius = 7
panel.contentView?.layer?.backgroundColor = NSColor.controlBackgroundColor.cgColor
attrView = NSAttributedTextView()
attrView.backgroundColor = NSColor.clear
attrView.textColor = NSColor.textColor
attrView.needsDisplay = true
attrView.text = "114514"
panel.contentView?.addSubview(attrView)
super.init(window: panel)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -1,4 +0,0 @@
// Ref: https://stackoverflow.com/a/75870807/4162914
#import <IOKit/IOKitLib.h>
#import <IOKit/hid/IOHIDBase.h>

View File

@ -1,33 +0,0 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "SwiftyCapsLockToggler",
platforms: [
.macOS(.v11),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "SwiftyCapsLockToggler",
targets: ["SwiftyCapsLockToggler"]
),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "CapsLockToggler",
path: "Framework",
cSettings: [
.headerSearchPath("include"),
]
),
.target(
name: "SwiftyCapsLockToggler",
dependencies: ["CapsLockToggler"]
),
]
)

View File

@ -1,64 +0,0 @@
// Ref: https://stackoverflow.com/a/75870807/4162914
// #import <IOKit/IOKitLib.h>
// #import <IOKit/hid/IOHIDBase.h>
import CapsLockToggler
public enum CapsLockToggler {
public static func toggle() {
try? IOKit.handleHIDSystemService { ioConnect in
var state = false
IOHIDGetModifierLockState(ioConnect, Int32(kIOHIDCapsLockState), &state)
state.toggle()
IOHIDSetModifierLockState(ioConnect, Int32(kIOHIDCapsLockState), state)
}
}
public static var isOn: Bool {
var state = false
try? IOKit.handleHIDSystemService { ioConnect in
IOHIDGetModifierLockState(ioConnect, Int32(kIOHIDCapsLockState), &state)
}
return state
}
public static func turnOff() {
try? IOKit.handleHIDSystemService { ioConnect in
IOHIDSetModifierLockState(ioConnect, Int32(kIOHIDCapsLockState), false)
}
}
}
// Refactored by Shiki Suen (MIT License)
public enum IOKit {
public static func handleHIDSystemService(_ taskHandler: @escaping (io_connect_t) -> Void) throws {
let ioService: io_service_t = IOServiceGetMatchingService(0, IOServiceMatching(kIOHIDSystemClass))
var connect: io_connect_t = 0
let x = IOServiceOpen(ioService, mach_task_self_, UInt32(kIOHIDParamConnectType), &connect)
if let errorOne = Mach.KernReturn(rawValue: x), errorOne != .success {
throw errorOne
}
taskHandler(connect)
let y = IOServiceClose(connect)
if let errorTwo = Mach.KernReturn(rawValue: y), errorTwo != .success {
throw errorTwo
}
}
}
// Refactored by Shiki Suen (MIT License)
public enum Mach {
public enum KernReturn: Int32, Error {
case success = 0
case invalidAddress = 1
case protectionFailure = 2
case noSpace = 3
case invalidArgument = 4
case failure = 5
case resourceShortage = 6
case notReceiver = 7
case noAccess = 8
case memoryFailure = 9
}
}

View File

@ -4,7 +4,7 @@ import PackageDescription
let package = Package(
name: "BookmarkManager",
platforms: [
.macOS(.v10_13),
.macOS(.v10_11),
],
products: [
.library(

View File

@ -14,17 +14,14 @@ public class BookmarkManager {
return
}
do {
var data: Data?
if #unavailable(macOS 10.13) {
data = NSKeyedArchiver.archivedData(withRootObject: bookmarkDic)
} else {
data = try NSKeyedArchiver.archivedData(withRootObject: bookmarkDic, requiringSecureCoding: false)
if #available(macOS 10.13, *) {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: bookmarkDic, requiringSecureCoding: false)
try data.write(to: bookmarkURL)
NSLog("Did save data to url")
} catch {
NSLog("Couldn't save bookmarks")
}
try data?.write(to: bookmarkURL)
NSLog("Did save data to url")
} catch {
NSLog("Couldn't save bookmarks")
}
}
@ -37,23 +34,9 @@ public class BookmarkManager {
if fileExists(url) {
do {
let fileData = try Data(contentsOf: url)
if #available(macOS 11.0, *) {
if let fileBookmarks = try NSKeyedUnarchiver.unarchivedDictionary(ofKeyClass: NSURL.self, objectClass: NSData.self, from: fileData) as [URL: Data]? {
for bookmark in fileBookmarks {
restoreBookmark(key: bookmark.key, value: bookmark.value)
}
}
} else if #available(macOS 10.11, *) {
if let fileBookmarks = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(fileData) as! [URL: Data]? {
for bookmark in fileBookmarks {
restoreBookmark(key: bookmark.key, value: bookmark.value)
}
}
} else {
if let fileBookmarks = NSKeyedUnarchiver.unarchiveObject(with: fileData) as! [URL: Data]? {
for bookmark in fileBookmarks {
restoreBookmark(key: bookmark.key, value: bookmark.value)
}
if let fileBookmarks = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(fileData) as! [URL: Data]? {
for bookmark in fileBookmarks {
restoreBookmark(key: bookmark.key, value: bookmark.value)
}
}
} catch {
@ -99,7 +82,12 @@ public class BookmarkManager {
}
private func getBookmarkURL() -> URL? {
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).last?.appendingPathComponent("Bookmarks.dict")
let urls = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
if let appSupportURL = urls.last {
let url = appSupportURL.appendingPathComponent("Bookmarks.dict")
return url
}
return nil
}
private func fileExists(_ url: URL) -> Bool {

View File

@ -1,29 +0,0 @@
+.PHONY: all
all: debug
debug:
swift build -c debug --package-path ./vChewing_MainAssembly/
release:
swift build -c release --package-path ./vChewing_MainAssembly/
clean:
@for currentDir in $$(ls ./); do \
if [ -d $$a ]; then \
echo "processing folder $$currentDir"; \
swift package clean --package-path ./$$currentDir || true; \
fi; \
done;
.PHONY: lint format
lintFormat: lint format
format:
@swiftformat --swiftversion 5.5 --indent 2 ./
lint:
@git ls-files --exclude-standard | grep -E '\.swift$$' | swiftlint --fix --autocorrect
.PHONY: permission-check install-debug install-release

View File

@ -4,7 +4,7 @@ import PackageDescription
let package = Package(
name: "ShiftKeyUpChecker",
platforms: [
.macOS(.v11),
.macOS(.v10_11),
],
products: [
.library(

View File

@ -3,6 +3,7 @@
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import AppKit
import Carbon
private extension Date {
static func - (lhs: Date, rhs: Date) -> TimeInterval {
@ -11,8 +12,6 @@ private extension Date {
}
public struct ShiftKeyUpChecker {
// MARK: -
public init(useLShift: Bool = false, useRShift: Bool = false) {
toggleWithLShift = useLShift
toggleWithRShift = useRShift
@ -25,7 +24,7 @@ public struct ShiftKeyUpChecker {
public var enabled: Bool { toggleWithLShift || toggleWithRShift }
private var checkModifier: NSEvent.ModifierFlags { .shift }
private var checkModifier: NSEvent.ModifierFlags { NSEvent.ModifierFlags.shift }
private var checkKeyCode: [UInt16] {
var result = [UInt16]()
if toggleWithLShift { result.append(lShiftKeyCode) }
@ -33,31 +32,46 @@ public struct ShiftKeyUpChecker {
return result
}
// MARK: -
private let delayInterval = 0.3
///
private let delayInterval = 0.2
private var previousKeyCode: UInt16?
private var lastTime: Date = .init()
private mutating func registerModifierKeyDown(event: NSEvent) {
var isKeyDown: Bool = event.type == .flagsChanged
// ModifierFlags OptionSet使 contains true false
isKeyDown = isKeyDown && event.modifierFlags.intersection(.deviceIndependentFlagsMask) == checkModifier
isKeyDown = isKeyDown && checkKeyCode.contains(event.keyCode)
lastTime = isKeyDown ? .init() : .init(timeInterval: .infinity * -1, since: Date())
previousKeyCode = isKeyDown ? event.keyCode : nil
private var shiftIsBeingPressed = false
private mutating func checkModifierKeyUp(event: NSEvent) -> Bool {
guard checkKeyCode.contains(event.keyCode) else { return false }
if event.type == .flagsChanged,
event.modifierFlags.intersection(.deviceIndependentFlagsMask) == .init(rawValue: 0),
Date() - lastTime <= delayInterval, shiftIsBeingPressed
{
// modifier keyup event
lastTime = Date(timeInterval: -3600 * 4, since: Date())
return true
}
return false
}
// To confirm that only the shift key is "pressed-and-released".
private mutating func checkModifierKeyDown(event: NSEvent) -> Bool {
let isLeftShift = event.modifierFlags.rawValue & UInt(NX_DEVICELSHIFTKEYMASK) != 0
let isRightShift = event.modifierFlags.rawValue & UInt(NX_DEVICERSHIFTKEYMASK) != 0
print("isLeftShift: \(isLeftShift), isRightShift: \(isRightShift)")
let isKeyDown =
event.type == .flagsChanged
&& checkModifier.contains(event.modifierFlags.intersection(.deviceIndependentFlagsMask))
&& checkKeyCode.contains(event.keyCode)
if isKeyDown {
// modifier keydown event
lastTime = Date()
if event.modifierFlags == .shift { shiftIsBeingPressed = true }
} else {
lastTime = Date(timeInterval: -3600 * 4, since: Date())
shiftIsBeingPressed = false
}
return false
}
// To confirm that the shift key is "pressed-and-released".
public mutating func check(_ event: NSEvent) -> Bool {
var met: Bool = event.type == .flagsChanged
met = met && checkKeyCode.contains(event.keyCode)
met = met && event.keyCode == previousKeyCode // KeyCode
met = met && event.modifierFlags.intersection(.deviceIndependentFlagsMask).isEmpty
met = met && Date() - lastTime <= delayInterval
_ = met ? lastTime = Date(timeInterval: .infinity * -1, since: Date()) : registerModifierKeyDown(event: event)
return met
checkModifierKeyUp(event: event) || checkModifierKeyDown(event: event)
}
}

View File

@ -4,7 +4,7 @@ import PackageDescription
let package = Package(
name: "LineReader",
platforms: [
.macOS(.v11),
.macOS(.v10_11),
],
products: [
.library(

View File

@ -1,6 +1,7 @@
// (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
import Foundation
import SwiftExtension
public class LineReader {
let encoding: String.Encoding

View File

@ -0,0 +1,5 @@
version: 1.8.2
builder:
configs:
- platform: ios
documentation_targets: [SwiftUIBackports]

View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2021 Shaps Benkau
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.

View File

@ -0,0 +1,22 @@
// swift-tools-version: 5.5
import PackageDescription
let package = Package(
name: "SwiftUIBackports",
platforms: [
.iOS(.v13),
.tvOS(.v13),
.watchOS(.v6),
.macOS(.v10_11),
],
products: [
.library(
name: "SwiftUIBackports",
targets: ["SwiftUIBackports"]
),
],
targets: [
.target(name: "SwiftUIBackports"),
],
swiftLanguageVersions: [.v5]
)

View File

@ -0,0 +1,131 @@
## NOTICE
- This package copy is clang-formatted according to vChewing's clang-format style, with removal of certain dependencies / features not-required by vChewing.
- **If you want to use this package**, you might want to consult its original repository: https://github.com/shaps80/SwiftUIBackports
![watchOS](https://img.shields.io/badge/watchOS-DE1F51)
![macOS](https://img.shields.io/badge/macOS-EE751F)
![tvOS](https://img.shields.io/badge/tvOS-00B9BB)
![ios](https://img.shields.io/badge/iOS-0C62C7)
[![swift](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fshaps80%2FSwiftUIBackports%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/shaps80/SwiftUIBackports)
# SwiftUI Backports
Introducing a collection of SwiftUI backports to make your iOS development easier.
Many backports support iOS 13+ but where UIKIt features were introduced in later versions, the same will be applicable to these backports, to keep parity with UIKit.
In some cases, I've also included additional APIs that bring more features to your SwiftUI development.
> Note, **all** backports will be API-matching to Apple's offical APIs, any additional features will be provided separately.
All backports are fully documented, in most cases using Apple's own documentation for consistency. Please refer to the header docs or Apple's original documentation for more details.
There is also a [Demo project](https://github.com/shaps80/SwiftUIBackportsDemo) available where you can see full demonstrations of all backports and additional features, including reference code to help you get started.
> Lastly, I hope this repo also serves as a great resource for _how_ you can backport effectively with minimal hacks 👍
## Sponsor
Building useful libraries like these, takes time away from my family. I build these tools in my spare time because I feel its important to give back to the community. Please consider [Sponsoring](https://github.com/sponsors/shaps80) me as it helps keep me working on useful libraries like these 😬
You can also give me a follow and a 'thanks' anytime.
[![Twitter](https://img.shields.io/badge/Twitter-@shaps-4AC71B)](http://twitter.com/shaps)
## Usage
The library adopts a backport design by [Dave DeLong](https://davedelong.com/blog/2021/10/09/simplifying-backwards-compatibility-in-swift/) that makes use of a single type to improve discoverability and maintainability when the time comes to remove your backport implementations, in favour of official APIs.
Backports of pure types, can easily be discovered under the `Backport` namespace. Similarly, modifiers are discoverable under the `.backport` namespace.
> Unfortuantely `Environment` backports cannot be access this way, in those cases the Apple API values will be prefixed with `backport` to simplify discovery.
Types:
```swift
@Backport.AppStorage("filter-enabled")
private var filterEnabled: Bool = false
```
Modifier:
```swift
Button("Show Prompt") {
showPrompt = true
}
.sheet(isPresented: $showPrompt) {
Prompt()
.backport.presentationDetents([.medium, .large])
}
```
Environment:
```swift
@Environment(\.backportRefresh) private var refreshAction
```
## Backports
**SwiftUI**
- `asyncImage`
- `AppStorage`
- `background` ViewBuilder API
- `DismissAction`
- `DynamicTypeSize`
`Label`
`LabeledContent`
- `NavigationDestination` uses a standard NavigationView
- `navigationTitle` newer API
- `overlay` ViewBuilder API
- `onChange`
- `openURL`
- `ProgressView`
- `presentationDetents`
- `presentationDragIndicator`
- `quicklookPreview`
- `requestReview`
- `Refreshable` includes pull-to-refresh 
- `ScaledMetric`
- `StateObject`
- `scrollDisabled`
- `scrollDismissesKeyboard`
- `scrollIndicators`
- `Section(_ header:)`
- `task` async/await modifier
**UIKit**
- `UIHostingConfiguration` simplifies embedding SwiftUI in `UICollectionViewCell` and `UITableViewCell`
## Extras
**Modal Presentations**
Adding this to your presented view, you can use the provided closure to present an `ActionSheet` to a user when they attempt to dismiss interactively. You can also use this to disable interactive dismissals entirely.
```swift
presentation(isModal: true) { /* attempt */ }
```
**FittingGeometryReader**
A custom `GeometryReader` implementation that correctly auto-sizes itself to its content. This is useful in many cases where you need a `GeometryReader` but don't want it to implicitly take up its parent View's bounds.
**FittingScrollView**
A custom `ScrollView` that respects `Spacer`'s when the content is not scrollable. This is useful when you need to place a view at the edges of your scrollview while its content is small enough to not require scrolling. Another great use case is vertically centered content that becomes `top` aligned once the content requires scrolling.
**PageView**
A pure SwiftUI implementation of a page-based view, using the native `TabView` and my custom `FittingGeometryReader` to size itself correctly. Since this uses a `TabView` under-the-hood, this allows you to use the same APIs and features from that view.
## Installation
You can install manually (by copying the files in the `Sources` directory) or using Swift Package Manager (**preferred**)
To install using Swift Package Manager, add this to the `dependencies` section of your `Package.swift` file:
`.package(url: "https://github.com/shaps80/SwiftUIBackports.git", .upToNextMinor(from: "1.0.0"))`

View File

@ -0,0 +1,62 @@
import ObjectiveC
import SwiftUI
@available(macOS 10.15, *)
/// Provides a convenient method for backporting API,
/// including types, functions, properties, property wrappers and more.
///
/// To backport a SwiftUI Label for example, you could apply the
/// following extension:
///
/// extension Backport where Content == Any {
/// public struct Label<Title, Icon> { }
/// }
///
/// Now if we want to provide further extensions to our backport type,
/// we need to ensure we retain the `Content == Any` generic requirement:
///
/// extension Backport.Label where Content == Any, Title == Text, Icon == Image {
/// public init<S: StringProtocol>(_ title: S, systemName: String) { }
/// }
///
/// In addition to types, we can also provide backports for properties
/// and methods:
///
/// extension Backport.Label where Content: View {
/// func onChange<Value: Equatable>(of value: Value, perform action: (Value) -> Void) -> some View {
/// // `content` provides access to the extended type
/// content.modifier(OnChangeModifier(value, action))
/// }
/// }
///
public struct Backport<Wrapped> {
/// The underlying content this backport represents.
public let content: Wrapped
@available(macOS 10.15, *)
/// Initializes a new Backport for the specified content.
/// - Parameter content: The content (type) that's being backported
public init(_ content: Wrapped) {
self.content = content
}
}
@available(macOS 10.15, *)
public extension View {
/// Wraps a SwiftUI `View` that can be extended to provide backport functionality.
var backport: Backport<Self> { .init(self) }
}
@available(macOS 10.15, *)
public extension NSObjectProtocol {
/// Wraps an `NSObject` that can be extended to provide backport functionality.
var backport: Backport<Self> { .init(self) }
}
@available(macOS 10.15, *)
public extension AnyTransition {
/// Wraps an `AnyTransition` that can be extended to provide backport functionality.
static var backport: Backport<AnyTransition> {
Backport(.identity)
}
}

View File

@ -0,0 +1,52 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
/// A geometry reader that automatically sizes its height to 'fit' its content.
public struct FittingGeometryReader<Content>: View where Content: View {
@State private var height: CGFloat = 10 // must be non-zero
private var content: (GeometryProxy) -> Content
@available(macOS 10.15, *)
public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content) {
self.content = content
}
@available(macOS 10.15, *)
public var body: some View {
GeometryReader { geo in
content(geo)
.fixedSize(horizontal: false, vertical: true)
.modifier(SizeModifier())
.onPreferenceChange(SizePreferenceKey.self) {
height = $0.height
}
}
.frame(height: height)
}
}
@available(macOS 10.15, *)
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
@available(macOS 10.15, *)
private struct SizeModifier: ViewModifier {
func body(content: Content) -> some View {
content.overlay(
GeometryReader { geo in
Color.clear.preference(
key: SizePreferenceKey.self,
value: geo.size
)
}
)
}
}

View File

@ -0,0 +1,37 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
/// A scrollview that behaves more similarly to a `VStack` when its content size is small enough.
public struct FittingScrollView<Content: View>: View {
private let content: Content
private let showsIndicators: Bool
@available(macOS 10.15, *)
/// A new scrollview
/// - Parameters:
/// - showsIndicators: If true, the scroll view will show indicators when necessary
/// - content: The content for this scroll view
public init(showsIndicators: Bool = true, @ViewBuilder content: () -> Content) {
self.showsIndicators = showsIndicators
self.content = content()
}
@available(macOS 10.15, *)
public var body: some View {
GeometryReader { geo in
SwiftUI.ScrollView(showsIndicators: showsIndicators) {
VStack(spacing: 10) {
content
}
.frame(
maxWidth: geo.size.width,
minHeight: geo.size.height
)
}
}
}
}

View File

@ -0,0 +1,47 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(tvOS, deprecated: 13)
@available(macOS, deprecated: 10.15)
@available(watchOS, deprecated: 6)
extension View {
/// Sets whether this presentation should act as a `modal`, preventing interactive dismissals
/// - Parameter isModal: If `true` the user will not be able to interactively dismiss
@ViewBuilder
@available(iOS, deprecated: 13, renamed: "backport.interactiveDismissDisabled(_:)")
public func presentation(isModal: Bool) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
backport.interactiveDismissDisabled(isModal)
} else {
self
}
#else
self
#endif
}
@available(macOS 10.15, *)
/// Provides fine-grained control over the dismissal.
/// - Parameters:
/// - isModal: If `true`, the user will not be able to interactively dismiss
/// - onAttempt: A closure that will be called when an interactive dismiss attempt occurs.
/// You can use this as an opportunity to present an ActionSheet to prompt the user.
@ViewBuilder
@available(iOS, deprecated: 13, renamed: "backport.interactiveDismissDisabled(_:onAttempt:)")
public func presentation(isModal: Bool = true, _ onAttempt: @escaping () -> Void) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
backport.interactiveDismissDisabled(isModal, onAttempt: onAttempt)
} else {
self
}
#else
self
#endif
}
}

View File

@ -0,0 +1,152 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
private extension EnvironmentValues {
func containsValue(forKey key: String) -> Bool {
value(forKey: key) != nil
}
func value<T>(forKey key: String, from mirror: Mirror, as _: T.Type) -> T? {
// Found a match
if let value = mirror.descendant("value", "some") {
if let typedValue = value as? T {
print("Found value")
return typedValue
} else {
print(
"Value for key '\(key)' in the environment is of type '\(type(of: value))', but we expected '\(String(describing: T.self))'."
)
}
} else {
print(
"Found key '\(key)' in the environment, but it doesn't have the expected structure. The type hierarchy may have changed in your SwiftUI version."
)
}
return nil
}
/// Extracts a value from the environment by the name of its associated EnvironmentKey.
/// Can be used to grab private environment values such as foregroundColor ("ForegroundColorKey").
func value<T>(forKey key: String, as _: T.Type) -> T? {
if let mirror = value(forKey: key) as? Mirror {
return value(forKey: key, from: mirror, as: T.self)
} else if let value = value(forKey: key) as? T {
return value
} else {
return nil
}
}
func value(forKey key: String) -> Any? {
func keyFromTypeName(typeName: String) -> String? {
let expectedPrefix = "TypedElement<EnvironmentPropertyKey<"
guard typeName.hasPrefix(expectedPrefix) else {
print("Wrong prefix")
return nil
}
let rest = typeName.dropFirst(expectedPrefix.count)
let expectedSuffix = ">>"
guard rest.hasSuffix(expectedSuffix) else {
print("Wrong suffix")
return nil
}
let middle = rest.dropLast(expectedSuffix.count)
return String(middle)
}
/// `environmentMember` has type (for example) `TypedElement<EnvironmentPropertyKey<ForegroundColorKey>>`
/// TypedElement.value contains the value of the key.
func extract(startingAt environmentNode: Any) -> Any? {
let mirror = Mirror(reflecting: environmentNode)
let typeName = String(describing: type(of: environmentNode))
if let nodeKey = keyFromTypeName(typeName: typeName) {
if key == nodeKey {
return mirror
}
}
// Environment values are stored in a doubly linked list. The "before" and "after" keys point
// to the next environment member.
if let linkedListMirror = mirror.superclassMirror,
let nextNode = linkedListMirror.descendant("after", "some")
{
return extract(startingAt: nextNode)
}
return nil
}
let mirror = Mirror(reflecting: self)
if let firstEnvironmentValue = mirror.descendant("_plist", "elements", "some") {
if let node = extract(startingAt: firstEnvironmentValue) {
return node
} else {
return nil
}
} else {
return nil
}
}
}
@available(macOS 10.15, *)
@propertyWrapper
internal struct StringlyTypedEnvironment<Value> {
final class Store<StoredValue>: ObservableObject {
var value: StoredValue?
}
@Environment(\.self) private var env
@ObservedObject private var store = Store<Value>()
var key: String
init(key: String) {
self.key = key
}
private(set) var wrappedValue: Value? {
get { store.value }
nonmutating set { store.value = newValue }
}
}
@available(macOS 10.15, *)
extension StringlyTypedEnvironment: DynamicProperty {
func update() {
wrappedValue = env.value(forKey: key, as: Value.self)
}
}
@available(macOS 10.15, *)
@propertyWrapper
internal struct EnvironmentContains: DynamicProperty {
final class Store: ObservableObject {
var contains: Bool = false
}
@Environment(\.self) private var env
var key: String
@ObservedObject private var store = Store()
init(key: String) {
self.key = key
}
var wrappedValue: Bool {
get { store.contains }
nonmutating set { store.contains = newValue }
}
func update() {
wrappedValue = env.containsValue(forKey: key)
}
}

View File

@ -0,0 +1,44 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
/*
The following code is for debugging purposes only!
*/
#if DEBUG
@available(macOS 10.15, *)
extension EnvironmentValues: CustomDebugStringConvertible {
public var debugDescription: String {
"\(self)"
.trimmingCharacters(in: .init(["[", "]"]))
.replacingOccurrences(of: "EnvironmentPropertyKey", with: "")
.replacingOccurrences(of: ", ", with: "\n")
}
}
@available(macOS 10.15, *)
struct EnvironmentOutputModifier: ViewModifier {
@Environment(\.self) private var environment
func body(content: Content) -> some View {
content
.onAppear {
print(environment.debugDescription)
}
}
}
@available(macOS 10.15, *)
extension View {
func printEnvironment() -> some View {
modifier(EnvironmentOutputModifier())
}
}
#endif

View File

@ -0,0 +1,175 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
#if os(iOS)
internal typealias PlatformView = UIView
internal typealias PlatformViewController = UIViewController
#elseif os(macOS)
internal typealias PlatformView = NSView
internal typealias PlatformViewController = NSViewController
#endif
#if os(iOS) || os(macOS)
extension PlatformView {
func ancestor<ViewType: PlatformView>(ofType _: ViewType.Type) -> ViewType? {
var view = superview
while let s = view {
if let typed = s as? ViewType {
return typed
}
view = s.superview
}
return nil
}
var host: PlatformView? {
var view = superview
while let s = view {
if NSStringFromClass(type(of: s)).contains("ViewHost") {
return s
}
view = s.superview
}
return nil
}
func sibling<ViewType: PlatformView>(ofType type: ViewType.Type) -> ViewType? {
guard let superview = superview, let index = superview.subviews.firstIndex(of: self) else { return nil }
var views = superview.subviews
views.remove(at: index)
for subview in views.reversed() {
if let typed = subview.child(ofType: type) {
return typed
}
}
return nil
}
func child<ViewType: PlatformView>(ofType type: ViewType.Type) -> ViewType? {
for subview in subviews {
if let typed = subview as? ViewType {
return typed
} else if let typed = subview.child(ofType: type) {
return typed
}
}
return nil
}
}
internal struct Inspector {
var hostView: PlatformView
var sourceView: PlatformView
var sourceController: PlatformViewController
func ancestor<ViewType: PlatformView>(ofType _: ViewType.Type) -> ViewType? {
hostView.ancestor(ofType: ViewType.self)
}
func sibling<ViewType: PlatformView>(ofType _: ViewType.Type) -> ViewType? {
hostView.sibling(ofType: ViewType.self)
}
}
@available(macOS 10.15, *)
extension View {
private func inject<Wrapped>(_ content: Wrapped) -> some View where Wrapped: View {
overlay(content.frame(width: 0, height: 0))
}
func inspect<ViewType: PlatformView>(
selector: @escaping (_ inspector: Inspector) -> ViewType?, customize: @escaping (ViewType) -> Void
) -> some View {
inject(InspectionView(selector: selector, customize: customize))
}
}
@available(macOS 10.15, *)
private struct InspectionView<ViewType: PlatformView>: View {
let selector: (Inspector) -> ViewType?
let customize: (ViewType) -> Void
var body: some View {
Representable(parent: self)
}
}
private class SourceView: PlatformView {
required init() {
super.init(frame: .zero)
isHidden = true
#if os(iOS)
isUserInteractionEnabled = false
#endif
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
#endif
#if os(iOS)
extension InspectionView {
struct Representable: UIViewRepresentable {
let parent: InspectionView
func makeUIView(context _: Context) -> UIView { .init() }
func updateUIView(_ view: UIView, context _: Context) {
DispatchQueue.main.async {
guard let host = view.host else { return }
let inspector = Inspector(
hostView: host,
sourceView: view,
sourceController: view.parentController
?? view.window?.rootViewController
?? UIViewController()
)
guard let targetView = parent.selector(inspector) else { return }
parent.customize(targetView)
}
}
}
}
#elseif os(macOS)
@available(macOS 10.15, *)
extension InspectionView {
struct Representable: NSViewRepresentable {
let parent: InspectionView
func makeNSView(context _: Context) -> NSView {
.init(frame: .zero)
}
func updateNSView(_ view: NSView, context _: Context) {
DispatchQueue.main.async {
guard let host = view.host else { return }
let inspector = Inspector(
hostView: host,
sourceView: view,
sourceController: view.parentController ?? NSViewController(nibName: nil, bundle: nil)
)
guard let targetView = parent.selector(inspector) else { return }
parent.customize(targetView)
}
}
}
}
#endif

View File

@ -0,0 +1,40 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
#if os(iOS)
import UIKit
public extension UIView {
var parentController: UIViewController? {
var responder: UIResponder? = self
while !(responder is UIViewController), superview != nil {
if let next = responder?.next {
responder = next
}
}
return responder as? UIViewController
}
}
#endif
#if os(macOS)
import AppKit
@available(macOS 10.15, *)
public extension NSView {
var parentController: NSViewController? {
var responder: NSResponder? = self
while !(responder is NSViewController), superview != nil {
if let next = responder?.nextResponder {
responder = next
}
}
return responder as? NSViewController
}
}
#endif

View File

@ -0,0 +1,47 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
#if os(iOS) || os(tvOS)
/*
Since UICollectionView is not designed to support SwiftUI out of the box,
we need to use a little trick to get the SwiftUI View's to ignore safeArea
insets, otherwise our cell's will not always layout correctly.
*/
extension UIHostingController {
convenience init(rootView: Content, ignoreSafeArea: Bool) {
self.init(rootView: rootView)
if ignoreSafeArea {
disableSafeArea()
}
}
func disableSafeArea() {
guard let viewClass = object_getClass(view) else { return }
let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
} else {
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
.zero
}
class_addMethod(
viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets),
method_getTypeEncoding(method)
)
}
objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}
#endif

View File

@ -0,0 +1,15 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
#if os(iOS)
import UIKit
extension UIApplication {
static var activeScene: UIWindowScene? {
shared.connectedScenes
.first { $0.activationState == .foregroundActive }
as? UIWindowScene
}
}
#endif

View File

@ -0,0 +1,408 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// A property wrapper type that reflects a value from `Store` and
/// invalidates a view on a change in value in that store.
@propertyWrapper
public struct AppStorage<Value>: DynamicProperty {
@ObservedObject
private var _value: RefStorage<Value>
private let commitHandler: (Value) -> Void
public var wrappedValue: Value {
get { _value.value }
nonmutating set {
commitHandler(newValue)
_value.value = newValue
}
}
public var projectedValue: Binding<Value> {
Binding(
get: { wrappedValue },
set: { wrappedValue = $0 }
)
}
private init(
value: Value, store: UserDefaults, key: String, get: @escaping (Any?) -> Value?, set: @escaping (Value) -> Void
) {
_value = RefStorage(value: value, store: store, key: key, transform: get)
commitHandler = set
}
}
}
@available(macOS 10.15, *)
public extension Backport.AppStorage {
/// Creates a property that can read and write to a boolean user default.
///
/// - Parameters:
/// - wrappedValue: The default value if a boolean value is not specified
/// for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Bool {
let value = store.value(forKey: key) as? Value ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write to an integer user default.
///
/// - Parameters:
/// - wrappedValue: The default value if an integer value is not specified
/// for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Int {
let value = store.value(forKey: key) as? Value ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write to a double user default.
///
/// - Parameters:
/// - wrappedValue: The default value if a double value is not specified
/// for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Double {
let value = store.value(forKey: key) as? Value ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write to a string user default.
///
/// - Parameters:
/// - wrappedValue: The default value if a string value is not specified
/// for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == String {
let value = store.value(forKey: key) as? Value ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write to a [string] user default.
///
/// - Parameters:
/// - wrappedValue: The default value if a string value is not specified
/// for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == [String] {
let value = store.value(forKey: key) as? Value ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write to a [String: Bool] user default.
///
/// - Parameters:
/// - wrappedValue: The default value if a string value is not specified
/// for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == [String: Bool] {
let value = store.value(forKey: key) as? Value ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write to a url user default.
///
/// - Parameters:
/// - wrappedValue: The default value if a url value is not specified for
/// the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == URL {
let value = store.url(forKey: key) ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { ($0 as? String).flatMap(URL.init) },
set: { store.set($0.absoluteString, forKey: key) }
)
}
/// Creates a property that can read and write to a user default as data.
///
/// Avoid storing large data blobs in store, such as image data,
/// as it can negatively affect performance of your app.
///
/// - Parameters:
/// - wrappedValue: The default value if a data value is not specified for
/// the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Data {
let value = store.value(forKey: key) as? Data ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
}
@available(macOS 10.15, *)
public extension Backport.AppStorage where Wrapped == Any, Value: ExpressibleByNilLiteral {
/// Creates a property that can read and write an Optional boolean user
/// default.
///
/// Defaults to nil if there is no restored value.
///
/// - Parameters:
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(_ key: String, store: UserDefaults = .standard) where Value == Bool? {
let value = store.value(forKey: key) as? Value ?? .none
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write an Optional integer user
/// default.
///
/// Defaults to nil if there is no restored value.
///
/// - Parameters:
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(_ key: String, store: UserDefaults = .standard) where Value == Int? {
let value = store.value(forKey: key) as? Value ?? .none
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write an Optional double user
/// default.
///
/// Defaults to nil if there is no restored value.
///
/// - Parameters:
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(_ key: String, store: UserDefaults = .standard) where Value == Double? {
let value = store.value(forKey: key) as? Value ?? .none
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write an Optional string user
/// default.
///
/// Defaults to nil if there is no restored value.
///
/// - Parameters:
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(_ key: String, store: UserDefaults = .standard) where Value == String? {
let value = store.value(forKey: key) as? Value ?? .none
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
/// Creates a property that can read and write an Optional URL user
/// default.
///
/// Defaults to nil if there is no restored value.
///
/// - Parameters:
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(_ key: String, store: UserDefaults = .standard) where Value == URL? {
let value = store.url(forKey: key) ?? .none
self.init(
value: value, store: store, key: key,
get: { ($0 as? String).flatMap(URL.init) },
set: { store.set($0?.absoluteString, forKey: key) }
)
}
/// Creates a property that can read and write an Optional data user
/// default.
///
/// Defaults to nil if there is no restored value.
///
/// - Parameters:
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(_ key: String, store: UserDefaults = .standard) where Value == Data? {
let value = store.value(forKey: key) as? Value ?? .none
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.set($0, forKey: key) }
)
}
}
@available(macOS 10.15, *)
public extension Backport.AppStorage where Wrapped == Any, Value: RawRepresentable {
/// Creates a property that can read and write to a string user default,
/// transforming that to `RawRepresentable` data type.
///
/// A common usage is with enumerations:
///
/// enum MyEnum: String {
/// case a
/// case b
/// case c
/// }
///
/// @AppStorage("MyEnumValue") private var value = MyEnum.a
///
/// - Parameters:
/// - wrappedValue: The default value if a string value
/// is not specified for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value.RawValue == String {
let rawValue = store.value(forKey: key) as? Value.RawValue
let value = rawValue.flatMap(Value.init) ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.setValue($0.rawValue, forKey: key) }
)
}
/// Creates a property that can read and write to an integer user default,
/// transforming that to `RawRepresentable` data type.
///
/// A common usage is with enumerations:
///
/// enum MyEnum: Int {
/// case a
/// case b
/// case c
/// }
///
/// @AppStorage("MyEnumValue") private var value = MyEnum.a
///
/// - Parameters:
/// - wrappedValue: The default value if an integer value
/// is not specified for the given key.
/// - key: The key to read and write the value to in the store
/// store.
/// - store: The store to read and write to. A value
/// of `nil` will use the user default store from the environment.
init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value.RawValue == Int {
let rawValue = store.value(forKey: key) as? Value.RawValue
let value = rawValue.flatMap(Value.init) ?? wrappedValue
self.init(
value: value, store: store, key: key,
get: { $0 as? Value },
set: { store.setValue($0.rawValue, forKey: key) }
)
}
}
@available(macOS 10.15, *)
private final class RefStorage<Value>: NSObject, ObservableObject {
@Published
fileprivate var value: Value
private let defaultValue: Value
private let store: UserDefaults
private let key: String
private let transform: (Any?) -> Value?
deinit {
store.removeObserver(self, forKeyPath: key)
}
init(value: Value, store: UserDefaults, key: String, transform: @escaping (Any?) -> Value?) {
self.value = value
defaultValue = value
self.store = store
self.key = key
self.transform = transform
super.init()
store.addObserver(self, forKeyPath: key, options: .new, context: nil)
}
override func observeValue(
forKeyPath _: String?,
of _: Any?,
change: [NSKeyValueChangeKey: Any]?,
context _: UnsafeMutableRawPointer?
) {
value = change?[.newKey].flatMap(transform) ?? defaultValue
}
}

View File

@ -0,0 +1,226 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 15.0)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// Loads and displays an image from the specified URL.
///
/// Until the image loads, SwiftUI displays a default placeholder. When
/// the load operation completes successfully, SwiftUI updates the
/// view to show the loaded image. If the operation fails, SwiftUI
/// continues to display the placeholder. The following example loads
/// and displays an icon from an example server:
///
/// asyncImage(url: URL(string: "https://example.com/icon.png"))
///
/// If you want to customize the placeholder or apply image-specific
/// modifiers --- like ``Image/resizable(capInsets:resizingMode:)`` ---
/// to the loaded image, use the ``init(url:scale:content:placeholder:)``
/// initializer instead.
///
/// - Parameters:
/// - url: The URL of the image to display.
/// - scale: The scale to use for the image. The default is `1`. Set a
/// different value when loading images designed for higher resolution
/// displays. For example, set a value of `2` for an image that you
/// would name with the `@2x` suffix if stored in a file on disk.
@ViewBuilder
public static func asyncImage(url: URL?, scale: CGFloat = 1) -> some View {
_asyncImage(url: url, scale: scale)
}
/// Loads and displays a modifiable image from the specified URL using
/// a custom placeholder until the image loads.
///
/// Until the image loads, SwiftUI displays the placeholder view that
/// you specify. When the load operation completes successfully, SwiftUI
/// updates the view to show content that you specify, which you
/// create using the loaded image. For example, you can show a green
/// placeholder, followed by a tiled version of the loaded image:
///
/// asyncImage(url: URL(string: "https://example.com/icon.png")) { image in
/// image.resizable(resizingMode: .tile)
/// } placeholder: {
/// Color.green
/// }
///
/// If the load operation fails, SwiftUI continues to display the
/// placeholder. To be able to display a different view on a load error,
/// use the ``init(url:scale:transaction:content:)`` initializer instead.
///
/// - Parameters:
/// - url: The URL of the image to display.
/// - scale: The scale to use for the image. The default is `1`. Set a
/// different value when loading images designed for higher resolution
/// displays. For example, set a value of `2` for an image that you
/// would name with the `@2x` suffix if stored in a file on disk.
/// - content: A closure that takes the loaded image as an input, and
/// returns the view to show. You can return the image directly, or
/// modify it as needed before returning it.
/// - placeholder: A closure that returns the view to show until the
/// load operation completes successfully.
@ViewBuilder
public static func asyncImage<I: View, P: View>(
url: URL?, scale: CGFloat = 1, @ViewBuilder content: @escaping (Image) -> I,
@ViewBuilder placeholder: @escaping () -> P
) -> some View {
_asyncImage(url: url, scale: scale, content: content, placeholder: placeholder)
}
/// Loads and displays a modifiable image from the specified URL in phases.
///
/// If you set the asynchronous image's URL to `nil`, or after you set the
/// URL to a value but before the load operation completes, the phase is
/// ``asyncImagePhase/empty``. After the operation completes, the phase
/// becomes either ``asyncImagePhase/failure(_:)`` or
/// ``asyncImagePhase/success(_:)``. In the first case, the phase's
/// ``asyncImagePhase/error`` value indicates the reason for failure.
/// In the second case, the phase's ``asyncImagePhase/image`` property
/// contains the loaded image. Use the phase to drive the output of the
/// `content` closure, which defines the view's appearance:
///
/// asyncImage(url: URL(string: "https://example.com/icon.png")) { phase in
/// if let image = phase.image {
/// image // Displays the loaded image.
/// } else if phase.error != nil {
/// Color.red // Indicates an error.
/// } else {
/// Color.blue // Acts as a placeholder.
/// }
/// }
///
/// To add transitions when you change the URL, apply an identifier to the
/// ``asyncImage``.
///
/// - Parameters:
/// - url: The URL of the image to display.
/// - scale: The scale to use for the image. The default is `1`. Set a
/// different value when loading images designed for higher resolution
/// displays. For example, set a value of `2` for an image that you
/// would name with the `@2x` suffix if stored in a file on disk.
/// - transaction: The transaction to use when the phase changes.
/// - content: A closure that takes the load phase as an input, and
/// returns the view to display for the specified phase.
@ViewBuilder
public static func asyncImage<Content: View>(
url: URL?, scale: CGFloat = 1, transaction: Transaction = Transaction(),
@ViewBuilder content: @escaping (asyncImagePhase) -> Content
) -> some View {
_asyncImage(url: url, scale: scale, transaction: transaction, content: content)
}
/// The current phase of the asynchronous image loading operation.
///
/// When you create an ``asyncImage`` instance with the
/// ``asyncImage/init(url:scale:transaction:content:)`` initializer, you define
/// the appearance of the view using a `content` closure. SwiftUI calls the
/// closure with a phase value at different points during the load operation
/// to indicate the current state. Use the phase to decide what to draw.
/// For example, you can draw the loaded image if it exists, a view that
/// indicates an error, or a placeholder:
///
/// asyncImage(url: URL(string: "https://example.com/icon.png")) { phase in
/// if let image = phase.image {
/// image // Displays the loaded image.
/// } else if phase.error != nil {
/// Color.red // Indicates an error.
/// } else {
/// Color.blue // Acts as a placeholder.
/// }
/// }
public enum asyncImagePhase {
/// No image is loaded.
case empty
/// An image succesfully loaded.
case success(Image)
/// An image failed to load with an error.
case failure(Error)
/// The loaded image, if any.
public var image: Image? {
guard case let .success(image) = self else { return nil }
return image
}
/// The error that occurred when attempting to load an image, if any.
public var error: Error? {
guard case let .failure(error) = self else { return nil }
return error
}
}
// An iOS 13+ async/await backport implementation
private struct _asyncImage<Content: View>: View {
@State private var phase: asyncImagePhase = .empty
var url: URL?
var scale: CGFloat = 1
var transaction: Transaction = .init()
var content: (Backport<Any>.asyncImagePhase) -> Content
public var body: some View {
ZStack {
content(phase)
}
.backport.task(id: url) {
do {
guard !Task.isCancelled, let url = url else { return }
let (data, _) = try await URLSession.shared.backport.data(from: url)
guard !Task.isCancelled else { return }
#if os(macOS)
if let image = NSImage(data: data) {
withTransaction(transaction) {
phase = .success(Image(nsImage: image))
}
}
#else
if let image = UIImage(data: data, scale: scale) {
withTransaction(transaction) {
phase = .success(Image(uiImage: image))
}
}
#endif
} catch {
phase = .failure(error)
}
}
}
init(url: URL?, scale: CGFloat = 1) where Content == AnyView {
self.url = url
self.scale = scale
content = { AnyView($0.image) }
}
init<I, P>(
url: URL?, scale: CGFloat = 1, @ViewBuilder content: @escaping (Image) -> I,
@ViewBuilder placeholder: @escaping () -> P
) where Content == _ConditionalContent<I, P> {
self.url = url
self.scale = scale
transaction = Transaction()
self.content = { phase -> _ConditionalContent<I, P> in
if let image = phase.image {
return ViewBuilder.buildEither(first: content(image))
} else {
return ViewBuilder.buildEither(second: placeholder())
}
}
}
init(
url: URL?, scale: CGFloat = 1, transaction: Transaction = Transaction(),
@ViewBuilder content: @escaping (Backport<Any>.asyncImagePhase) -> Content
) {
self.url = url
self.scale = scale
self.transaction = transaction
self.content = content
}
}
}

View File

@ -0,0 +1,136 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
public extension Backport where Wrapped: View {
/// Layers the views that you specify behind this view.
///
/// Use this modifier to place one or more views behind another view.
/// For example, you can place a collection of stars beind a ``Text`` view:
///
/// Text("ABCDEF")
/// .background(alignment: .leading) { Star(color: .red) }
/// .background(alignment: .center) { Star(color: .green) }
/// .background(alignment: .trailing) { Star(color: .blue) }
///
/// The example above assumes that you've defined a `Star` view with a
/// parameterized color:
///
/// struct Star: View {
/// var color: Color
///
/// var body: some View {
/// Image(systemName: "star.fill")
/// .foregroundStyle(color)
/// }
/// }
///
/// By setting different `alignment` values for each modifier, you make the
/// stars appear in different places behind the text:
///
/// ![A screenshot of the letters A, B, C, D, E, and F written in front of
/// three stars. The stars, from left to right, are red, green, and
/// blue.](View-background-2)
///
/// If you specify more than one view in the `content` closure, the modifier
/// collects all of the views in the closure into an implicit ``ZStack``,
/// taking them in order from back to front. For example, you can layer a
/// vertical bar behind a circle, with both of those behind a horizontal
/// bar:
///
/// Color.blue
/// .frame(width: 200, height: 10) // Creates a horizontal bar.
/// .background {
/// Color.green
/// .frame(width: 10, height: 100) // Creates a vertical bar.
/// Circle()
/// .frame(width: 50, height: 50)
/// }
///
/// Both the background modifier and the implicit ``ZStack`` composed from
/// the background content --- the circle and the vertical bar --- use a
/// default ``Alignment/center`` alignment. The vertical bar appears
/// centered behind the circle, and both appear as a composite view centered
/// behind the horizontal bar:
///
/// ![A screenshot of a circle with a horizontal blue bar layered on top
/// and a vertical green bar layered underneath. All of the items are center
/// aligned.](View-background-3)
///
/// If you specify an alignment for the background, it applies to the
/// implicit stack rather than to the individual views in the closure. You
/// can see this if you add the ``Alignment/leading`` alignment:
///
/// Color.blue
/// .frame(width: 200, height: 10)
/// .background(alignment: .leading) {
/// Color.green
/// .frame(width: 10, height: 100)
/// Circle()
/// .frame(width: 50, height: 50)
/// }
///
/// The vertical bar and the circle move as a unit to align the stack
/// with the leading edge of the horizontal bar, while the
/// vertical bar remains centered on the circle:
///
/// ![A screenshot of a horizontal blue bar in front of a circle, which
/// is in front of a vertical green bar. The horizontal bar and the circle
/// are center aligned with each other; the left edges of the circle
/// and the horizontal are aligned.](View-background-3a)
///
/// To control the placement of individual items inside the `content`
/// closure, either use a different background modifier for each item, as
/// the earlier example of stars under text demonstrates, or add an explicit
/// ``ZStack`` inside the content closure with its own alignment:
///
/// Color.blue
/// .frame(width: 200, height: 10)
/// .background(alignment: .leading) {
/// ZStack(alignment: .leading) {
/// Color.green
/// .frame(width: 10, height: 100)
/// Circle()
/// .frame(width: 50, height: 50)
/// }
/// }
///
/// The stack alignment ensures that the circle's leading edge aligns with
/// the vertical bar's, while the background modifier aligns the composite
/// view with the horizontal bar:
///
/// ![A screenshot of a horizontal blue bar in front of a circle, which
/// is in front of a vertical green bar. All items are aligned on their
/// left edges.](View-background-4)
///
/// You can achieve layering without a background modifier by putting both
/// the modified view and the background content into a ``ZStack``. This
/// produces a simpler view hierarchy, but it changes the layout priority
/// that SwiftUI applies to the views. Use the background modifier when you
/// want the modified view to dominate the layout.
///
/// If you want to specify a ``ShapeStyle`` like a
/// ``HierarchicalShapeStyle`` or a ``Material`` as the background, use
/// ``View/background(_:ignoresSafeAreaEdges:)`` instead.
/// To specify a ``Shape`` or ``InsettableShape``, use
/// ``View/background(_:in:fillStyle:)-89n7j`` or
/// ``View/background(_:in:fillStyle:)-20tq5``, respectively.
///
/// - Parameters:
/// - alignment: The alignment that the modifier uses to position the
/// implicit ``ZStack`` that groups the background views. The default
/// is ``Alignment/center``.
/// - content: A ``ViewBuilder`` that you use to declare the views to draw
/// behind this view, stacked in a cascading order from bottom to top.
/// The last view that you list appears at the front of the stack.
///
/// - Returns: A view that uses the specified content as a background.
func background<Content: View>(alignment: Alignment = .center, @ViewBuilder _ content: () -> Content)
-> some View
{
self.content.background(content(), alignment: alignment)
}
}

View File

@ -0,0 +1,191 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
public extension EnvironmentValues {
/// An action that dismisses the current presentation.
///
/// Use this environment value to get the ``Backport.DismissAction`` instance
/// for the current ``Environment``. Then call the instance
/// to perform the dismissal. You call the instance directly because
/// it defines a ``Backport.DismissAction/callAsFunction()``
/// method that Swift calls when you call the instance.
///
/// For example, you can create a button that calls the ``Backport.DismissAction``:
///
/// private struct SheetContents: View {
/// @Environment(\.backportDismiss) private var dismiss
///
/// var body: some View {
/// Button("Done") {
/// dismiss()
/// }
/// }
/// }
///
/// If you present the `SheetContents` view in a sheet, the user can dismiss
/// the sheet by tapping or clicking the sheet's button:
///
/// private struct DetailView: View {
/// @State private var isSheetPresented = false
///
/// var body: some View {
/// Button("Show Sheet") {
/// isSheetPresented = true
/// }
/// .sheet(isPresented: $isSheetPresented) {
/// SheetContents()
/// }
/// }
/// }
///
/// Be sure that you define the action in the appropriate environment.
/// For example, don't reorganize the `DetailView` in the example above
/// so that it creates the `dismiss` property and calls it from the
/// ``View/sheet(item:onDismiss:content:)`` view modifier's `content`
/// closure:
///
/// private struct DetailView: View {
/// @State private var isSheetPresented = false
/// @Environment(\.backportDismiss) private var dismiss // Applies to DetailView.
///
/// var body: some View {
/// Button("Show Sheet") {
/// isSheetPresented = true
/// }
/// .sheet(isPresented: $isSheetPresented) {
/// Button("Done") {
/// dismiss() // Fails to dismiss the sheet.
/// }
/// }
/// }
/// }
///
/// If you do this, the sheet fails to dismiss because the action applies
/// to the environment where you declared it, which is that of the detail
/// view, rather than the sheet. In fact, if you've presented the detail
/// view in a ``NavigationView``, the dismissal pops the detail view
/// the navigation stack.
///
/// The dismiss action has no effect on a view that isn't currently
/// presented. If you need to query whether SwiftUI is currently presenting
/// a view, read the ``EnvironmentValues/backportIsPresented`` environment value.
var backportDismiss: Backport<Any>.DismissAction {
.init(presentation: presentationMode)
}
@available(macOS 10.15, *)
/// A Boolean value that indicates whether the view associated with this
/// environment is currently presented.
///
/// You can read this value like any of the other ``EnvironmentValues``
/// by creating a property with the ``Environment`` property wrapper:
///
/// @Environment(\.backportIsPresented) private var isPresented
///
/// Read the value inside a view if you need to know when SwiftUI
/// presents that view. For example, you can take an action when SwiftUI
/// presents a view by using the ``View/onChange(of:perform:)``
/// modifier:
///
/// .onChange(of: isPresented) { isPresented in
/// if isPresented {
/// // Do something when first presented.
/// }
/// }
///
/// This behaves differently than ``View/onAppear(perform:)``, which
/// SwiftUI can call more than once for a given presentation, like
/// when you navigate back to a view that's already in the
/// navigation hierarchy.
///
/// To dismiss the currently presented view, use
/// ``EnvironmentValues/backportDismiss``.
var backportIsPresented: Bool {
presentationMode.wrappedValue.isPresented
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(tvOS, deprecated: 15)
@available(watchOS, deprecated: 8)
extension Backport where Wrapped: Any {
/// An action that dismisses a presentation.
///
/// Use the ``EnvironmentValues/dismiss`` environment value to get the instance
/// of this structure for a given ``Environment``. Then call the instance
/// to perform the dismissal. You call the instance directly because
/// it defines a ``DismissAction/callAsFunction()``
/// method that Swift calls when you call the instance.
///
/// For example, you can create a button that calls the ``DismissAction``:
///
/// private struct SheetContents: View {
/// @Environment(\.backportDismiss) private var dismiss
///
/// var body: some View {
/// Button("Done") {
/// dismiss()
/// }
/// }
/// }
///
/// If you present the `SheetContents` view in a sheet, the user can dismiss
/// the sheet by tapping or clicking the sheet's button:
///
/// private struct DetailView: View {
/// @State private var isSheetPresented = false
///
/// var body: some View {
/// Button("Show Sheet") {
/// isSheetPresented = true
/// }
/// .sheet(isPresented: $isSheetPresented) {
/// SheetContents()
/// }
/// }
/// }
///
/// Be sure that you define the action in the appropriate environment.
/// For example, don't reorganize the `DetailView` in the example above
/// so that it creates the `dismiss` property and calls it from the
/// ``View/sheet(item:onDismiss:content:)`` view modifier's `content`
/// closure:
///
/// private struct DetailView: View {
/// @State private var isSheetPresented = false
/// @Environment(\.backportDismiss) private var dismiss // Applies to DetailView.
///
/// var body: some View {
/// Button("Show Sheet") {
/// isSheetPresented = true
/// }
/// .sheet(isPresented: $isSheetPresented) {
/// Button("Done") {
/// dismiss() // Fails to dismiss the sheet.
/// }
/// }
/// }
/// }
///
/// If you do this, the sheet fails to dismiss because the action applies
/// to the environment where you declared it, which is that of the detail
/// view, rather than the sheet. In fact, if you've presented the detail
/// view in a ``NavigationView``, the dismissal pops the detail view
/// from the navigation stack.
///
/// The dismiss action has no effect on a view that isn't currently
/// presented. If you need to query whether SwiftUI is currently presenting
/// a view, read the ``EnvironmentValues/backportIsPresented`` environment value.
public struct DismissAction {
var presentation: Binding<PresentationMode>
public func callAsFunction() {
presentation.wrappedValue.dismiss()
}
}
}

View File

@ -0,0 +1,64 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
private struct BackportDynamicTypeKey: EnvironmentKey {
static var defaultValue: Backport.DynamicTypeSize = .large
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 15)
@available(tvOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(watchOS, deprecated: 8)
extension EnvironmentValues {
/// Sets the Dynamic Type size within the view to the given value.
///
/// As an example, you can set a Dynamic Type size in `ContentView` to be
/// ``DynamicTypeSize/xLarge`` (this can be useful in previews to see your
/// content at a different size) like this:
///
/// ContentView()
/// .backport.dynamicTypeSize(.xLarge)
///
/// If a Dynamic Type size range is applied after setting a value,
/// the value is limited by that range:
///
/// ContentView() // Dynamic Type size will be .large
/// .backport.dynamicTypeSize(...DynamicTypeSize.large)
/// .backport.dynamicTypeSize(.xLarge)
///
/// When limiting the Dynamic Type size, consider if adding a
/// large content view with ``View/accessibilityShowsLargeContentViewer()``
/// would be appropriate.
///
/// - Parameter size: The size to set for this view.
///
/// - Returns: A view that sets the Dynamic Type size to the specified
/// `size`.
public var backportDynamicTypeSize: Backport<Any>.DynamicTypeSize {
get { .init(self[keyPath: \.sizeCategory]) }
set { self[keyPath: \.sizeCategory] = newValue.sizeCategory }
}
}
@available(macOS 10.15, *)
private struct DynamicTypeRangeKey: EnvironmentKey {
static var defaultValue: Range<Backport<Any>.DynamicTypeSize> {
.init(uncheckedBounds: (lower: .xSmall, upper: .accessibility5))
}
}
@available(macOS 10.15, *)
extension EnvironmentValues {
var dynamicTypeRange: Range<Backport<Any>.DynamicTypeSize> {
get { self[DynamicTypeRangeKey.self] }
set {
let current = self[DynamicTypeRangeKey.self]
self[DynamicTypeRangeKey.self] = current.clamped(to: newValue)
}
}
}

View File

@ -0,0 +1,116 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
public extension Backport where Wrapped: View {
/// Sets the Dynamic Type size within the view to the given value.
///
/// As an example, you can set a Dynamic Type size in `ContentView` to be
/// ``DynamicTypeSize/xLarge`` (this can be useful in previews to see your
/// content at a different size) like this:
///
/// ContentView()
/// .dynamicTypeSize(.xLarge)
///
/// If a Dynamic Type size range is applied after setting a value,
/// the value is limited by that range:
///
/// ContentView() // Dynamic Type size will be .large
/// .dynamicTypeSize(...DynamicTypeSize.large)
/// .dynamicTypeSize(.xLarge)
///
/// When limiting the Dynamic Type size, consider if adding a
/// large content view with ``View/accessibilityShowsLargeContentViewer()``
/// would be appropriate.
///
/// - Parameter size: The size to set for this view.
///
/// - Returns: A view that sets the Dynamic Type size to the specified
/// `size`.
@ViewBuilder
func dynamicTypeSize(_ size: Backport<Any>.DynamicTypeSize) -> some View {
content.environment(\.backportDynamicTypeSize, size)
}
@available(macOS 10.15, *)
/// Limits the Dynamic Type size within the view to the given range.
///
/// As an example, you can constrain the maximum Dynamic Type size in
/// `ContentView` to be no larger than ``DynamicTypeSize/large``:
///
/// ContentView()
/// .dynamicTypeSize(...DynamicTypeSize.large)
///
/// If the Dynamic Type size is limited to multiple ranges, the result is
/// their intersection:
///
/// ContentView() // Dynamic Type sizes are from .small to .large
/// .dynamicTypeSize(.small...)
/// .dynamicTypeSize(...DynamicTypeSize.large)
///
/// A specific Dynamic Type size can still be set after a range is applied:
///
/// ContentView() // Dynamic Type size is .xLarge
/// .dynamicTypeSize(.xLarge)
/// .dynamicTypeSize(...DynamicTypeSize.large)
///
/// When limiting the Dynamic Type size, consider if adding a
/// large content view with ``View/accessibilityShowsLargeContentViewer()``
/// would be appropriate.
///
/// - Parameter range: The range of sizes that are allowed in this view.
///
/// - Returns: A view that constrains the Dynamic Type size of this view
/// within the specified `range`.
@ViewBuilder
func dynamicTypeSize<T>(_ range: T) -> some View
where T: RangeExpression, T.Bound == Backport<Any>.DynamicTypeSize
{
if let range = range as? Range<T.Bound> {
content
.modifier(DynamicTypeRangeModifier())
.environment(\.dynamicTypeRange, range)
} else if let range = range as? ClosedRange<T.Bound> {
content
.modifier(DynamicTypeRangeModifier())
.environment(\.dynamicTypeRange, .init(uncheckedBounds: (lower: range.lowerBound, upper: range.upperBound)))
} else if let range = range as? PartialRangeFrom<T.Bound> {
content
.modifier(DynamicTypeRangeModifier())
.environment(\.dynamicTypeRange, .init(uncheckedBounds: (range.lowerBound, .accessibility5)))
} else if let range = range as? PartialRangeUpTo<T.Bound> {
content
.modifier(DynamicTypeRangeModifier())
.environment(\.dynamicTypeRange, .init(uncheckedBounds: (.xSmall, range.upperBound)))
} else if let range = range as? PartialRangeThrough<T.Bound> {
content
.modifier(DynamicTypeRangeModifier())
.environment(\.dynamicTypeRange, .init(uncheckedBounds: (.xSmall, range.upperBound)))
} else {
content
.modifier(DynamicTypeRangeModifier())
}
}
}
@available(macOS 10.15, *)
private struct DynamicTypeRangeModifier: ViewModifier {
@Environment(\.dynamicTypeRange) private var range
@Environment(\.backportDynamicTypeSize) private var size
@available(macOS 10.15, *)
private var resolvedSize: Backport<Any>.DynamicTypeSize {
print(range)
return range.contains(size)
? size
: max(range.lowerBound, min(range.upperBound, size))
}
@available(macOS 10.15, *)
func body(content: Content) -> some View {
content.environment(\.backportDynamicTypeSize, resolvedSize)
}
}

View File

@ -0,0 +1,225 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 15)
@available(tvOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(watchOS, deprecated: 8)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// A Dynamic Type size, which specifies how large scalable content should be.
///
/// For more information about Dynamic Type sizes in iOS, see iOS Human Interface Guidelines >
/// [Dynamic Type Sizes](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/typography/#dynamic-type-sizes).
/// For more information about Dynamic Type sizes in watchOS, see watchOS Human Interface Guidelines >
/// [Dynamic Type Sizes](https://developer.apple.com/design/human-interface-guidelines/watchos/visual/typography/#dynamic-type-sizes).
public enum DynamicTypeSize: Hashable, Comparable, CaseIterable {
/// An extra small size.
case xSmall
/// A small size.
case small
/// A medium size.
case medium
/// A large size.
case large
/// An extra large size.
case xLarge
/// An extra extra large size.
case xxLarge
/// An extra extra extra large size.
case xxxLarge
/// The first accessibility size.
case accessibility1
/// The second accessibility size.
case accessibility2
/// The third accessibility size.
case accessibility3
/// The fourth accessibility size.
case accessibility4
/// The fifth accessibility size.
case accessibility5
/// A Boolean value indicating whether the size is one that is associated
/// with accessibility.
public var isAccessibilitySize: Bool {
self >= .accessibility1
}
#if os(iOS) || os(tvOS)
/// Create a Dynamic Type size from its `UIContentSizeCategory` equivalent.
public init?(_ uiSizeCategory: UIContentSizeCategory) {
switch uiSizeCategory {
case .extraSmall:
self = .xSmall
case .small:
self = .small
case .medium:
self = .medium
case .large:
self = .medium
case .extraLarge:
self = .xLarge
case .extraExtraLarge:
self = .xxLarge
case .extraExtraExtraLarge:
self = .xxxLarge
case .accessibilityMedium:
self = .accessibility1
case .accessibilityLarge:
self = .accessibility2
case .accessibilityExtraLarge:
self = .accessibility3
case .accessibilityExtraExtraLarge:
self = .accessibility4
case .accessibilityExtraExtraExtraLarge:
self = .accessibility5
default:
return nil
}
}
#endif
internal init(_ sizeCategory: ContentSizeCategory) {
switch sizeCategory {
case .extraSmall:
self = .xSmall
case .small:
self = .small
case .medium:
self = .medium
case .large:
self = .large
case .extraLarge:
self = .xLarge
case .extraExtraLarge:
self = .xxLarge
case .extraExtraExtraLarge:
self = .xxxLarge
case .accessibilityMedium:
self = .accessibility1
case .accessibilityLarge:
self = .accessibility2
case .accessibilityExtraLarge:
self = .accessibility3
case .accessibilityExtraExtraLarge:
self = .accessibility4
case .accessibilityExtraExtraExtraLarge:
self = .accessibility5
default:
self = .large
}
}
var sizeCategory: ContentSizeCategory {
switch self {
case .xSmall:
return .extraSmall
case .small:
return .small
case .medium:
return .medium
case .large:
return .large
case .xLarge:
return .extraLarge
case .xxLarge:
return .extraExtraLarge
case .xxxLarge:
return .extraExtraExtraLarge
case .accessibility1:
return .accessibilityMedium
case .accessibility2:
return .accessibilityLarge
case .accessibility3:
return .accessibilityExtraLarge
case .accessibility4:
return .accessibilityExtraExtraLarge
case .accessibility5:
return .accessibilityExtraExtraExtraLarge
}
}
}
}
@available(iOS 15, tvOS 15, macOS 12, watchOS 8, *)
extension Backport.DynamicTypeSize {
var dynamicTypeSize: DynamicTypeSize {
switch self {
case .xSmall:
return .xSmall
case .small:
return .small
case .medium:
return .medium
case .large:
return .large
case .xLarge:
return .xLarge
case .xxLarge:
return .xxLarge
case .xxxLarge:
return .xxxLarge
case .accessibility1:
return .accessibility1
case .accessibility2:
return .accessibility2
case .accessibility3:
return .accessibility3
case .accessibility4:
return .accessibility4
case .accessibility5:
return .accessibility5
}
}
}
#if os(iOS) || os(tvOS)
@available(iOS, deprecated: 15)
@available(tvOS, deprecated: 15)
extension UIContentSizeCategory {
public init(_ dynamicTypeSize: Backport<Any>.DynamicTypeSize?) {
switch dynamicTypeSize {
case .xSmall:
self = .extraSmall
case .small:
self = .small
case .medium:
self = .medium
case .large:
self = .large
case .xLarge:
self = .extraLarge
case .xxLarge:
self = .extraExtraLarge
case .xxxLarge:
self = .extraExtraExtraLarge
case .accessibility1:
self = .accessibilityMedium
case .accessibility2:
self = .accessibilityLarge
case .accessibility3:
self = .accessibilityExtraLarge
case .accessibility4:
self = .accessibilityExtraExtraLarge
case .accessibility5:
self = .accessibilityExtraExtraExtraLarge
case .none:
self = .large
}
}
}
#endif

View File

@ -0,0 +1,184 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// A standard label for user interface items, consisting of an icon with a
/// title.
///
/// One of the most common and recognizable user interface components is the
/// combination of an icon and a label. This idiom appears across many kinds of
/// apps and shows up in collections, lists, menus of action items, and
/// disclosable lists, just to name a few.
///
/// You create a label, in its simplest form, by providing a title and the name
/// of an image, such as an icon from the
/// [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/)
/// collection:
///
/// Label("Lightning", systemImage: "bolt.fill")
///
/// You can also apply styles to labels in several ways. In the case of dynamic
/// changes to the view after device rotation or change to a window size you
/// might want to show only the text portion of the label using the
/// ``LabelStyle/titleOnly`` label style:
///
/// Label("Lightning", systemImage: "bolt.fill")
/// .labelStyle(.titleOnly)
///
/// Conversely, there's also an icon-only label style:
///
/// Label("Lightning", systemImage: "bolt.fill")
/// .labelStyle(.iconOnly)
///
/// Some containers might apply a different default label style, such as only
/// showing icons within toolbars on macOS and iOS. To opt in to showing both
/// the title and the icon, you can apply the ``LabelStyle/titleAndIcon`` label
/// style:
///
/// Label("Lightning", systemImage: "bolt.fill")
/// .labelStyle(.titleAndIcon)
///
/// You can also create a customized label style by modifying an existing
/// style; this example adds a red border to the default label style:
///
/// struct RedBorderedLabelStyle: LabelStyle {
/// func makeBody(configuration: Configuration) -> some View {
/// Label(configuration)
/// .border(Color.red)
/// }
/// }
///
/// For more extensive customization or to create a completely new label style,
/// you'll need to adopt the ``LabelStyle`` protocol and implement a
/// ``LabelStyleConfiguration`` for the new style.
///
/// To apply a common label style to a group of labels, apply the style
/// to the view hierarchy that contains the labels:
///
/// VStack {
/// Label("Rain", systemImage: "cloud.rain")
/// Label("Snow", systemImage: "snow")
/// Label("Sun", systemImage: "sun.max")
/// }
/// .labelStyle(.iconOnly)
///
/// It's also possible to make labels using views to compose the label's icon
/// programmatically, rather than using a pre-made image. In this example, the
/// icon portion of the label uses a filled ``Circle`` overlaid
/// with the user's initials:
///
/// Label {
/// Text(person.fullName)
/// .font(.body)
/// .foregroundColor(.primary)
/// Text(person.title)
/// .font(.subheadline)
/// .foregroundColor(.secondary)
/// } icon: {
/// Circle()
/// .fill(person.profileColor)
/// .frame(width: 44, height: 44, alignment: .center)
/// .overlay(Text(person.initials))
/// }
///
public struct Label<Title, Icon>: View where Title: View, Icon: View {
@Environment(\.self) private var environment
@Environment(\.backportLabelStyle) private var style
private var config: Backport<Any>.LabelStyleConfiguration
/// Creates a label with a custom title and icon.
public init(@ViewBuilder title: () -> Title, @ViewBuilder icon: () -> Icon) {
config = .init(title: .init(content: title()), icon: .init(content: icon()))
}
@MainActor public var body: some View {
if let style = style {
style.makeBody(configuration: config.environment(environment))
} else {
DefaultLabelStyle()
.makeBody(configuration: config.environment(environment))
}
}
}
}
@available(macOS 10.15, *)
public extension Backport.Label where Wrapped == Any, Title == Text, Icon == Image {
/// Creates a label with an icon image and a title generated from a
/// localized string.
///
/// - Parameters:
/// - titleKey: A title generated from a localized string.
/// - image: The name of the image resource to lookup.
init(_ titleKey: LocalizedStringKey, image name: String) {
self.init(title: { Text(titleKey) }, icon: { Image(name) })
}
/// Creates a label with an icon image and a title generated from a string.
///
/// - Parameters:
/// - title: A string used as the label's title.
/// - image: The name of the image resource to lookup.
init<S>(_ title: S, image name: String) where S: StringProtocol {
self.init(title: { Text(title) }, icon: { Image(name) })
}
}
@available(macOS, introduced: 11, message: "SFSymbols support was only introduced in macOS 11")
extension Backport.Label where Wrapped == Any, Title == Text, Icon == Image {
/// Creates a label with a system icon image and a title generated from a
/// localized string.
///
/// - Parameters:
/// - titleKey: A title generated from a localized string.
/// - systemImage: The name of the image resource to lookup.
public init(_ titleKey: LocalizedStringKey, systemImage name: String) {
self.init(title: { Text(titleKey) }, icon: { Image(systemName: name) })
}
/// Creates a label with a system icon image and a title generated from a
/// string.
///
/// - Parameters:
/// - title: A string used as the label's title.
/// - systemImage: The name of the image resource to lookup.
public init<S>(_ title: S, systemImage name: String) where S: StringProtocol {
self.init(title: { Text(title) }, icon: { Image(systemName: name) })
}
}
@available(macOS 10.15, *)
public extension Backport.Label
where Wrapped == Any, Title == Backport.LabelStyleConfiguration.Title, Icon == Backport.LabelStyleConfiguration.Icon
{
/// Creates a label representing the configuration of a style.
///
/// You can use this initializer within the ``LabelStyle/makeBody(configuration:)``
/// method of a ``LabelStyle`` instance to create an instance of the label
/// that's being styled. This is useful for custom label styles that only
/// wish to modify the current style, as opposed to implementing a brand new
/// style.
///
/// For example, the following style adds a red border around the label,
/// but otherwise preserves the current style:
///
/// struct RedBorderedLabelStyle: LabelStyle {
/// func makeBody(configuration: Configuration) -> some View {
/// Label(configuration)
/// .border(Color.red)
/// }
/// }
///
/// - Parameter configuration: The label style to use.
init(_ configuration: Backport.LabelStyleConfiguration) {
config = configuration
}
}

View File

@ -0,0 +1,52 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension Backport where Wrapped == Any {
/// The properties of a label.
public struct LabelStyleConfiguration {
/// A type-erased title view of a label.
public struct Title: View {
let content: AnyView
public var body: some View { content }
init<Content: View>(content: Content) {
self.content = .init(content)
}
}
@available(macOS 10.15, *)
/// A type-erased icon view of a label.
public struct Icon: View {
let content: AnyView
public var body: some View { content }
init<Content: View>(content: Content) {
self.content = .init(content)
}
}
@available(macOS 10.15, *)
/// A description of the labeled item.
public internal(set) var title: LabelStyleConfiguration.Title
@available(macOS 10.15, *)
/// A symbolic representation of the labeled item.
public internal(set) var icon: LabelStyleConfiguration.Icon
@available(macOS 10.15, *)
internal var environment: EnvironmentValues = .init()
@available(macOS 10.15, *)
func environment(_ values: EnvironmentValues) -> Self {
var config = self
config.environment = values
return config
}
}
}

View File

@ -0,0 +1,72 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
/// A type that applies a custom appearance to all labels within a view.
///
/// To configure the current label style for a view hierarchy, use the
/// ``View/labelStyle(_:)`` modifier.
public protocol BackportLabelStyle {
/// The properties of a label.
typealias Configuration = Backport<Any>.LabelStyleConfiguration
/// A view that represents the body of a label.
associatedtype Body: View
@available(macOS 10.15, *)
/// Creates a view that represents the body of a label.
///
/// The system calls this method for each ``Label`` instance in a view
/// hierarchy where this style is the current label style.
///
/// - Parameter configuration: The properties of the label.
@ViewBuilder func makeBody(configuration: Configuration) -> Body
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped: View {
public func labelStyle<S: BackportLabelStyle>(_ style: S) -> some View {
content.environment(\.backportLabelStyle, .init(style))
}
}
@available(macOS 10.15, *)
internal struct AnyLabelStyle: BackportLabelStyle {
let _makeBody: (Backport<Any>.LabelStyleConfiguration) -> AnyView
@available(macOS 10.15, *)
init<S: BackportLabelStyle>(_ style: S) {
_makeBody = { config in
AnyView(style.makeBody(configuration: config))
}
}
@available(macOS 10.15, *)
func makeBody(configuration: Configuration) -> some View {
_makeBody(configuration)
}
}
@available(macOS 10.15, *)
private struct BackportLabelStyleEnvironmentKey: EnvironmentKey {
static var defaultValue: AnyLabelStyle?
}
@available(macOS 10.15, *)
extension EnvironmentValues {
var backportLabelStyle: AnyLabelStyle? {
get { self[BackportLabelStyleEnvironmentKey.self] }
set { self[BackportLabelStyleEnvironmentKey.self] = newValue }
}
}

View File

@ -0,0 +1,44 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension Backport where Wrapped == Any {
/// The default label style in the current context.
///
/// You can also use ``LabelStyle/automatic`` to construct this style.
public struct DefaultLabelStyle: BackportLabelStyle {
public init() {}
@available(macOS 10.15, *)
/// Creates a view that represents the body of a label.
///
/// The system calls this method for each ``Label`` instance in a view
/// hierarchy where this style is the current label style.
///
/// - Parameter configuration: The properties of the label.
public func makeBody(configuration: DefaultLabelStyle.Configuration) -> some View {
HStack {
configuration.icon
configuration.title
}
}
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension BackportLabelStyle where Self == Backport<Any>.DefaultLabelStyle {
/// A label style that resolves its appearance automatically based on the
/// current context.
public static var automatic: Self { .init() }
}

View File

@ -0,0 +1,44 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension Backport where Wrapped == Any {
/// A label style that only displays the icon of the label.
///
/// You can also use ``LabelStyle/iconOnly`` to construct this style.
public struct IconOnlyLabelStyle: BackportLabelStyle {
/// Creates an icon-only label style.
public init() {}
@available(macOS 10.15, *)
/// Creates a view that represents the body of a label.
///
/// The system calls this method for each ``Label`` instance in a view
/// hierarchy where this style is the current label style.
///
/// - Parameter configuration: The properties of the label.
public func makeBody(configuration: Configuration) -> some View {
configuration.icon
}
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension BackportLabelStyle where Self == Backport<Any>.IconOnlyLabelStyle {
/// A label style that only displays the icon of the label.
///
/// The title of the label is still used for non-visual descriptions, such as
/// VoiceOver.
public static var iconOnly: Self { .init() }
}

View File

@ -0,0 +1,70 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension Backport where Wrapped == Any {
/// A label style that shows both the title and icon of the label using a
/// system-standard layout.
///
/// You can also use ``LabelStyle/titleAndIcon`` to construct this style.
public struct TitleAndIconLabelStyle: BackportLabelStyle {
/// Creates a label style that shows both the title and icon of the label
/// using a system-standard layout.
public init() {}
@available(macOS 10.15, *)
/// Creates a view that represents the body of a label.
///
/// The system calls this method for each ``Label`` instance in a view
/// hierarchy where this style is the current label style.
///
/// - Parameter configuration: The properties of the label.
public func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.icon
configuration.title
}
}
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension BackportLabelStyle where Self == Backport<Any>.TitleAndIconLabelStyle {
/// A label style that shows both the title and icon of the label using a
/// system-standard layout.
///
/// In most cases, labels show both their title and icon by default. However,
/// some containers might apply a different default label style to their
/// content, such as only showing icons within toolbars on macOS and iOS. To
/// opt in to showing both the title and the icon, you can apply the title
/// and icon label style:
///
/// Label("Lightning", systemImage: "bolt.fill")
/// .labelStyle(.titleAndIcon)
///
/// To apply the title and icon style to a group of labels, apply the style
/// to the view hierarchy that contains the labels:
///
/// VStack {
/// Label("Rain", systemImage: "cloud.rain")
/// Label("Snow", systemImage: "snow")
/// Label("Sun", systemImage: "sun.max")
/// }
/// .labelStyle(.titleAndIcon)
///
/// The relative layout of the title and icon is dependent on the context it
/// is displayed in. In most cases, however, the label is arranged
/// horizontally with the icon leading.
public static var titleAndIcon: Self { .init() }
}

View File

@ -0,0 +1,41 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension Backport where Wrapped == Any {
// A label style that only displays the title of the label.
///
/// You can also use ``LabelStyle/titleOnly`` to construct this style.
public struct TitleOnlyLabelStyle: BackportLabelStyle {
/// Creates a title-only label style.
public init() {}
@available(macOS 10.15, *)
/// Creates a view that represents the body of a label.
///
/// The system calls this method for each ``Label`` instance in a view
/// hierarchy where this style is the current label style.
///
/// - Parameter configuration: The properties of the label.
public func makeBody(configuration: Configuration) -> some View {
configuration.title
}
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
extension BackportLabelStyle where Self == Backport<Any>.TitleOnlyLabelStyle {
/// A label style that only displays the title of the label.
public static var titleOnly: Self { .init() }
}

View File

@ -0,0 +1,275 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension Backport where Wrapped == Any {
/// A container for attaching a label to a value-bearing view.
///
/// The instance's content represents a read-only or read-write value, and its
/// label identifies or describes the purpose of that value.
/// The resulting element has a layout that's consistent with other framework
/// controls and automatically adapts to its container, like a form or toolbar.
/// Some styles of labeled content also apply styling or behaviors to the value
/// content, like making ``Text`` views selectable.
///
/// The following example associates a label with a custom view and has
/// a layout that matches the label of the ``Picker``:
///
/// Form {
/// Backport.LabeledContent("Custom Value") {
/// MyCustomView(value: $value)
/// }
/// Picker("Selected Value", selection: $selection) {
/// PickerOption("Option 1", 1)
/// PickerOption("Option 2", 2)
/// }
/// }
///
/// ### Custom view labels
///
/// You can assemble labeled content with an explicit view for its label
/// using the ``init(content:label:)`` initializer. For example, you can
/// rewrite the previous labeled content example using a ``Text`` view:
///
/// LabeledContent {
/// MyCustomView(value: $value)
/// } label: {
/// Text("Custom Value")
/// }
///
/// The `label` view builder accepts any kind of view, like a ``Label``:
///
/// Backport.LabeledContent {
/// MyCustomView(value: $value)
/// } label: {
/// Label("Custom Value", systemImage: "hammer")
/// }
///
/// ### Textual labeled content
///
/// You can construct labeled content with string values or formatted values
/// to create read-only displays of textual values:
///
/// Form {
/// Section("Information") {
/// Backport.LabeledContent("Name", value: person.name)
/// }
/// if !person.pets.isEmpty {
/// Section("Pets") {
/// ForEach(pet) { pet in
/// Backport.LabeledContent(pet.species, value: pet.name)
/// }
/// }
/// }
/// }
///
/// Wherever possible, SwiftUI makes this text selectable.
///
/// ### Compositional elements
///
/// You can use labeled content as the label for other elements. For example,
/// a ``NavigationLink`` can present a summary value for the destination it
/// links to:
///
/// Form {
/// NavigationLink(value: Settings.wifiDetail) {
/// Backport.LabeledContent("Wi-Fi", value: ssidName)
/// }
/// }
///
/// In some cases, the styling of views used as the value content is
/// specialized as well. For example, while a ``Toggle`` in an inset group
/// form on macOS is styled as a switch by default, it's styled as a checkbox
/// when used as a value element within a surrounding `LabeledContent`
/// instance:
///
/// Form {
/// Backport.LabeledContent("Source Control") {
/// Toggle("Refresh local status automatically",
/// isOn: $refreshLocalStatus)
/// Toggle("Fetch and refresh server status automatically",
/// isOn: $refreshServerStatus)
/// Toggle("Add and remove files automatically",
/// isOn: $addAndRemoveFiles)
/// Toggle("Select files to commit automatically",
/// isOn: $selectFiles)
/// }
/// }
///
/// ### Controlling label visibility
///
/// A label communicates the identity or purpose of the value, which is
/// important for accessibility. However, you might want to hide the label
/// in the display, and some controls or contexts may visually hide their label
/// by default. The ``View/labels(_:)`` modifier allows controlling that
/// visibility. The following example hides both labels, producing only a
/// group of the two value views:
///
/// Group {
/// LabeledContent("Custom Value") {
/// MyCustomView(value: $value)
/// }
/// Picker("Selected Value", selection: $selection) {
/// PickerOption("Option 1", 1)
/// PickerOption("Option 2", 2)
/// }
/// }
/// .labelsHidden()
///
/// ### Styling labeled content
///
/// You can set label styles using the ``View/labeledContentStyle(_:)``
/// modifier. You can also build custom styles using ``LabeledContentStyle``.
public struct LabeledContent<Label, Content> {
@Environment(\.backportLabeledContentStyle) private var style
let config: LabeledContentStyleConfiguration
public var body: some View {
style.makeBody(configuration: config)
}
}
}
@available(macOS 10.15, *)
public extension Backport.LabeledContent
where
Wrapped == Any, Label == Backport<Any>.LabeledContentStyleConfiguration.Label,
Content == Backport<Any>.LabeledContentStyleConfiguration.Content
{
/// Creates labeled content based on a labeled content style configuration.
///
/// You can use this initializer within the
/// ``LabeledContentStyle/makeBody(configuration:)`` method of a
/// ``LabeledContentStyle`` to create a labeled content instance.
/// This is useful for custom styles that only modify the current style,
/// as opposed to implementing a brand new style.
///
/// For example, the following style adds a red border around the labeled
/// content, but otherwise preserves the current style:
///
/// struct RedBorderLabeledContentStyle: LabeledContentStyle {
/// func makeBody(configuration: Configuration) -> some View {
/// LabeledContent(configuration)
/// .border(.red)
/// }
/// }
///
/// - Parameter configuration: The properties of the labeled content
init(_ config: Backport.LabeledContentStyleConfiguration) {
self.config = config
}
}
@available(macOS 10.15, *)
public extension Backport.LabeledContent where Wrapped == Any, Label == Text, Content: View {
/// Creates a labeled view that generates its label from a localized string
/// key.
///
/// This initializer creates a ``Text`` label on your behalf, and treats the
/// localized key similar to ``Text/init(_:tableName:bundle:comment:)``. See
/// `Text` for more information about localizing strings.
///
/// - Parameters:
/// - titleKey: The key for the view's localized title, that describes
/// the purpose of the view.
/// - content: The value content being labeled.
init(_ titleKey: LocalizedStringKey, @ViewBuilder content: () -> Content) {
config = .init(
label: Text(titleKey),
content: content()
)
}
@available(macOS 10.15, *)
/// Creates a labeled view that generates its label from a string.
///
/// This initializer creates a ``Text`` label on your behalf, and treats the
/// title similar to ``Text/init(_:)-9d1g4``. See `Text` for more
/// information about localizing strings.
///
/// - Parameters:
/// - title: A string that describes the purpose of the view.
/// - content: The value content being labeled.
init<S>(_ title: S, @ViewBuilder content: () -> Content) where S: StringProtocol {
config = .init(
label: Text(title),
content: content()
)
}
}
@available(macOS 10.15, *)
extension Backport.LabeledContent: View where Wrapped == Any, Label: View, Content: View {
/// Creates a labeled view that generates its label from a localized string
/// key.
///
/// This initializer creates a ``Text`` label on your behalf, and treats the
/// localized key similar to ``Text/init(_:tableName:bundle:comment:)``. See
/// `Text` for more information about localizing strings.
///
/// - Parameters:
/// - titleKey: The key for the view's localized title, that describes
/// the purpose of the view.
/// - content: The value content being labeled.
public init(@ViewBuilder content: () -> Content, @ViewBuilder label: () -> Label) {
config = .init(
label: label(),
content: content()
)
}
}
@available(macOS 10.15, *)
public extension Backport.LabeledContent where Wrapped == Any, Label == Text, Content == Text {
/// Creates a labeled informational view.
///
/// This initializer creates a ``Text`` label on your behalf, and treats the
/// localized key similar to ``Text/init(_:tableName:bundle:comment:)``. See
/// `Text` for more information about localizing strings.
///
/// Form {
/// LabeledContent("Name", value: person.name)
/// }
///
/// In some contexts, this text will be selectable by default.
///
/// - Parameters:
/// - titleKey: The key for the view's localized title, that describes
/// the purpose of the view.
/// - value: The value being labeled.
init<S: StringProtocol>(_ titleKey: LocalizedStringKey, value: S) {
config = .init(
label: Text(titleKey),
content: Text(value)
)
}
@available(macOS 10.15, *)
/// Creates a labeled informational view.
///
/// This initializer creates a ``Text`` label on your behalf, and treats the
/// title similar to ``Text/init(_:)-9d1g4``. See `Text` for more
/// information about localizing strings.
///
/// Form {
/// ForEach(person.pet) { pet in
/// LabeledContent(pet.species, value: pet.name)
/// }
/// }
///
/// - Parameters:
/// - title: A string that describes the purpose of the view.
/// - value: The value being labeled.
init<S1: StringProtocol, S2: StringProtocol>(_ title: S1, value: S2) {
config = .init(
label: Text(title),
content: Text(value)
)
}
}

View File

@ -0,0 +1,63 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension Backport where Wrapped: View {
/// Sets a style for labeled content.
public func labeledContentStyle<S>(_ style: S) -> some View where S: BackportLabeledContentStyle {
content.environment(\.backportLabeledContentStyle, .init(style))
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
public protocol BackportLabeledContentStyle {
typealias Configuration = Backport<Any>.LabeledContentStyleConfiguration
associatedtype Body: View
@ViewBuilder func makeBody(configuration: Configuration) -> Body
}
@available(macOS 10.15, *)
internal struct AnyLabeledContentStyle: BackportLabeledContentStyle {
typealias Configuration = Backport<Any>.LabeledContentStyleConfiguration
let _makeBody: (Configuration) -> AnyView
@available(macOS 10.15, *)
init<S: BackportLabeledContentStyle>(_ style: S) {
_makeBody = { config in
AnyView(style.makeBody(configuration: config))
}
}
@available(macOS 10.15, *)
func makeBody(configuration: Configuration) -> some View {
_makeBody(configuration)
}
}
@available(macOS 10.15, *)
private struct BackportLabeledContentStyleEnvironmentKey: EnvironmentKey {
static var defaultValue: AnyLabeledContentStyle = .init(.automatic)
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension EnvironmentValues {
var backportLabeledContentStyle: AnyLabeledContentStyle {
get { self[BackportLabeledContentStyleEnvironmentKey.self] }
set { self[BackportLabeledContentStyleEnvironmentKey.self] = newValue }
}
}

View File

@ -0,0 +1,70 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension Backport where Wrapped == Any {
/// The properties of a labeled content instance.
public struct LabeledContentStyleConfiguration {
/// A type-erased label of a labeled content instance.
public struct Label: View {
@EnvironmentContains(key: "LabelsHiddenKey") private var isHidden
let view: AnyView
public var body: some View {
if isHidden {
EmptyView()
} else {
view
}
}
@available(macOS 10.15, *)
init<V: View>(_ view: V) {
self.view = .init(view)
}
}
@available(macOS 10.15, *)
/// A type-erased content of a labeled content instance.
public struct Content: View {
@EnvironmentContains(key: "LabelsHiddenKey") private var isHidden
let view: AnyView
public var body: some View {
view
.foregroundColor(isHidden ? .primary : .secondary)
.frame(maxWidth: .infinity, alignment: isHidden ? .leading : .trailing)
}
@available(macOS 10.15, *)
init<V: View>(_ view: V) {
self.view = .init(view)
}
}
@available(macOS 10.15, *)
/// The label of the labeled content instance.
public let label: Label
@available(macOS 10.15, *)
/// The content of the labeled content instance.
public let content: Content
@available(macOS 10.15, *)
internal init<L: View, C: View>(label: L, content: C) {
self.label = .init(label)
self.content = .init(content)
}
@available(macOS 10.15, *)
internal init<L: View, C: View>(@ViewBuilder content: () -> C, @ViewBuilder label: () -> L) {
self.content = .init(content())
self.label = .init(label())
}
}
}

View File

@ -0,0 +1,24 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
public extension Backport where Wrapped == Any {
struct AutomaticLabeledContentStyle: BackportLabeledContentStyle {
public func makeBody(configuration: Configuration) -> some View {
HStack(alignment: .firstTextBaseline) {
configuration.label
Spacer()
configuration.content
.multilineTextAlignment(.trailing)
}
}
}
}
@available(macOS 10.15, *)
extension BackportLabeledContentStyle where Self == Backport<Any>.AutomaticLabeledContentStyle {
static var automatic: Self { .init() }
}

View File

@ -0,0 +1,144 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(watchOS, deprecated: 9)
@available(macOS, deprecated: 13)
extension Backport where Wrapped: View {
/// Associates a destination view with a presented data type for use within
/// a navigation stack.
///
/// Add this view modifer to a view inside a ``NavigationStack`` to
/// describe the view that the stack displays when presenting
/// a particular kind of data. Use a ``NavigationLink`` to present
/// the data. For example, you can present a `ColorDetail` view for
/// each presentation of a ``Color`` instance:
///
/// NavigationStack {
/// List {
/// NavigationLink("Mint", value: Color.mint)
/// NavigationLink("Pink", value: Color.pink)
/// NavigationLink("Teal", value: Color.teal)
/// }
/// .navigationDestination(for: Color.self) { color in
/// ColorDetail(color: color)
/// }
/// .navigationTitle("Colors")
/// }
///
/// You can add more than one navigation destination modifier to the stack
/// if it needs to present more than one kind of data.
///
/// - Parameters:
/// - data: The type of data that this destination matches.
/// - destination: A view builder that defines a view to display
/// when the stack's navigation state contains a value of
/// type `data`. The closure takes one argument, which is the value
/// of the data to present.
public func navigationDestination<D: Hashable, C: View>(for _: D.Type, @ViewBuilder destination: @escaping (D) -> C)
-> some View
{
content
.environment(
\.navigationDestinations,
[
.init(type: D.self): .init { destination($0 as! D) },
]
)
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(watchOS, deprecated: 9)
@available(macOS, deprecated: 13)
extension Backport where Wrapped == Any {
public struct NavigationLink<Label, Destination>: View where Label: View, Destination: View {
@Environment(\.navigationDestinations) private var destinations
@available(macOS 10.15, *)
private let valueType: AnyMetaType
private let value: Any?
private let label: Label
private let destination: () -> Destination
@available(macOS 10.15, *)
public init<P>(value: P?, @ViewBuilder label: () -> Label) where Destination == Never {
self.value = value
valueType = .init(type: P.self)
destination = { fatalError() }
self.label = label()
}
@available(macOS 10.15, *)
public var body: some View {
SwiftUI.NavigationLink {
if let value = value {
destinations[valueType.type]?.content(value)
}
} label: {
label
}
.disabled(value == nil)
}
}
}
@available(macOS 10.15, *)
private struct NavigationDestinationsEnvironmentKey: EnvironmentKey {
static var defaultValue: [AnyMetaType: DestinationView] = [:]
}
@available(macOS 10.15, *)
private extension EnvironmentValues {
var navigationDestinations: [AnyMetaType: DestinationView] {
get { self[NavigationDestinationsEnvironmentKey.self] }
set {
var current = self[NavigationDestinationsEnvironmentKey.self]
newValue.forEach { current[$0] = $1 }
self[NavigationDestinationsEnvironmentKey.self] = current
}
}
}
@available(macOS 10.15, *)
private struct AnyMetaType {
let type: Any.Type
}
@available(macOS 10.15, *)
extension AnyMetaType: Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.type == rhs.type
}
}
@available(macOS 10.15, *)
extension AnyMetaType: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(type))
}
}
@available(macOS 10.15, *)
private extension Dictionary {
subscript(_ key: Any.Type) -> Value? where Key == AnyMetaType {
get { self[.init(type: key)] }
_modify { yield &self[.init(type: key)] }
}
}
@available(macOS 10.15, *)
private struct DestinationView: View {
let content: (Any) -> AnyView
var body: Never { fatalError() }
init<Content: View>(content: @escaping (Any) -> Content) {
self.content = { AnyView(content($0)) }
}
}

View File

@ -0,0 +1,38 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(watchOS, deprecated: 7)
@available(tvOS, deprecated: 14)
extension Backport where Wrapped: View {
@ViewBuilder
public func navigationTitle<S: StringProtocol>(_ title: S) -> some View {
#if os(macOS)
if #available(macOS 11, *) {
content.navigationTitle(title)
} else {
content
}
#else
content.navigationBarTitle(title)
#endif
}
@available(macOS 10.15, *)
@ViewBuilder
public func navigationTitle(_ titleKey: LocalizedStringKey) -> some View {
#if os(macOS)
if #available(macOS 11, *) {
content.navigationTitle(titleKey)
} else {
content
}
#else
content.navigationBarTitle(titleKey)
#endif
}
}

View File

@ -0,0 +1,55 @@
import Combine
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14.0)
@available(macOS, deprecated: 11.0)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped: View {
/// Adds a modifier for this view that fires an action when a specific
/// value changes.
///
/// `onChange` is called on the main thread. Avoid performing long-running
/// tasks on the main thread. If you need to perform a long-running task in
/// response to `value` changing, you should dispatch to a background queue.
///
/// The new value is passed into the closure.
///
/// - Parameters:
/// - value: The value to observe for changes
/// - action: A closure to run when the value changes.
/// - newValue: The new value that changed
///
/// - Returns: A view that fires an action when the specified value changes.
@ViewBuilder
public func onChange<Value: Equatable>(of value: Value, perform action: @escaping (Value) -> Void) -> some View {
content.modifier(ChangeModifier(value: value, action: action))
}
}
@available(macOS 10.15, *)
private struct ChangeModifier<Value: Equatable>: ViewModifier {
let value: Value
let action: (Value) -> Void
@available(macOS 10.15, *)
@State var oldValue: Value?
@available(macOS 10.15, *)
init(value: Value, action: @escaping (Value) -> Void) {
self.value = value
self.action = action
_oldValue = .init(initialValue: value)
}
@available(macOS 10.15, *)
func body(content: Content) -> some View {
content
.onReceive(Just(value)) { newValue in
guard newValue != oldValue else { return }
action(newValue)
oldValue = newValue
}
}
}

View File

@ -0,0 +1,177 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
#if canImport(WatchKit)
import WatchKit
#endif
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(tvOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(watchOS, deprecated: 7)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// An action that opens a URL.
///
/// Read the ``EnvironmentValues.backportOpenURL`` environment value to get an
/// instance of this structure for a given ``Environment``. Call the
/// instance to open a URL. You call the instance directly because it
/// defines a ``Backport.OpenURLAction.callAsFunction(_:)`` method that Swift
/// calls when you call the instance.
///
/// For example, you can open a web site when the user taps a button:
///
/// struct OpenURLExample: View {
/// @Environment(\.backportOpenURL) private var openURL
///
/// var body: some View {
/// Button {
/// if let url = URL(string: "https://www.example.com") {
/// openURL(url)
/// }
/// } label: {
/// Label("Get Help", systemImage: "person.fill.questionmark")
/// }
/// }
/// }
///
/// If you want to know whether the action succeeds, add a completion
/// handler that takes a Boolean value. In this case, Swift implicitly
/// calls the ``Backport.OpenURLAction.callAsFunction(_:completion:)`` method
/// instead. That method calls your completion handler after it determines
/// whether it can open the URL, but possibly before it finishes opening
/// the URL. You can add a handler to the example above so that
/// it prints the outcome to the console:
///
/// openURL(url) { accepted in
/// print(accepted ? "Success" : "Failure")
/// }
///
/// The system provides a default open URL action with behavior
/// that depends on the contents of the URL. For example, the default
/// action opens a Universal Link in the associated app if possible,
/// or in the users default web browser if not.
///
/// You can also set a custom action using the ``View.environment(_:_:)``
/// view modifier. Any views that read the action from the environment,
/// including the built-in ``Link`` view and ``Text`` views with markdown
/// links, or links in attributed strings, use your action. Initialize an
/// action by calling the ``Backport.OpenURLAction.init(handler:)`` initializer with
/// a handler that takes a URL and returns an ``Backport.OpenURLAction.Result``:
///
/// Text("Visit [Example Company](https://www.example.com) for details.")
/// .environment(\.backportOpenURL, Backport.OpenURLAction { url in
/// handleURL(url) // Define this method to take appropriate action.
/// return .handled
/// })
///
/// SwiftUI translates the value that your custom action's handler
/// returns into an appropriate Boolean result for the action call.
/// For example, a view that uses the action declared above
/// receives `true` when calling the action, because the
/// handler always returns ``Backport.OpenURLAction.Result.handled``.
public struct OpenURLAction {
@available(iOS, deprecated: 15)
@available(tvOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(watchOS, deprecated: 8)
public struct Result {
enum Value {
case handled
case discarded
case systemAction(_ url: URL?)
@available(macOS 10.15, *)
var accepted: Bool {
if case .discarded = self {
return false
} else {
return true
}
}
}
@available(macOS 10.15, *)
let value: Value
@available(macOS 10.15, *)
public static var handled: Result { .init(value: .handled) }
public static var discarded: Result { .init(value: .discarded) }
public static var systemAction: Result { .init(value: .systemAction(nil)) }
public static func systemAction(_ url: URL) -> Result { .init(value: .systemAction(url)) }
}
@available(macOS 10.15, *)
let handler: (URL) -> Result
@available(macOS 10.15, *)
public init(handler: @escaping (URL) -> Result) {
self.handler = handler
}
@available(macOS 10.15, *)
@available(watchOS, unavailable)
public func callAsFunction(_ url: URL) {
handleUrl(url)
}
@available(macOS 10.15, *)
@available(watchOS, unavailable)
public func callAsFunction(_ url: URL, completion: @escaping (_ accepted: Bool) -> Void) {
let result = handleUrl(url)
completion(result.accepted)
}
@available(macOS 10.15, *)
@discardableResult
private func handleUrl(_ url: URL) -> Result.Value {
let result = handler(url).value
switch result {
case .handled, .discarded: break
case let .systemAction(updatedUrl):
let resolved = updatedUrl ?? url
#if os(macOS)
NSWorkspace.shared.open(resolved)
#elseif os(iOS) || os(tvOS)
UIApplication.shared.open(resolved)
#else
WKExtension.shared().openSystemURL(resolved)
#endif
}
return result
}
}
}
@available(macOS 10.15, *)
private struct BackportOpenURLKey: EnvironmentKey {
static var defaultValue: Backport<Any>.OpenURLAction {
.init { url in
#if os(macOS)
return .systemAction
#elseif os(iOS) || os(tvOS)
if UIApplication.shared.canOpenURL(url) {
return .systemAction
} else {
return .discarded
}
#else
return .systemAction
#endif
}
}
}
@available(macOS 10.15, *)
public extension EnvironmentValues {
var backportOpenURL: Backport<Any>.OpenURLAction {
get { self[BackportOpenURLKey.self] }
set { self[BackportOpenURLKey.self] = newValue }
}
}

View File

@ -0,0 +1,127 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
public extension Backport where Wrapped: View {
/// Layers the views that you specify in front of this view.
///
/// Use this modifier to place one or more views in front of another view.
/// For example, you can place a group of stars on a ``RoundedRectangle``:
///
/// RoundedRectangle(cornerRadius: 8)
/// .frame(width: 200, height: 100)
/// .overlay(alignment: .topLeading) { Star(color: .red) }
/// .overlay(alignment: .topTrailing) { Star(color: .yellow) }
/// .overlay(alignment: .bottomLeading) { Star(color: .green) }
/// .overlay(alignment: .bottomTrailing) { Star(color: .blue) }
///
/// The example above assumes that you've defined a `Star` view with a
/// parameterized color:
///
/// struct Star: View {
/// var color = Color.yellow
///
/// var body: some View {
/// Image(systemName: "star.fill")
/// .foregroundStyle(color)
/// }
/// }
///
/// By setting different `alignment` values for each modifier, you make the
/// stars appear in different places on the rectangle:
///
/// ![A screenshot of a rounded rectangle with a star in each corner. The
/// star in the upper-left is red; the start in the upper-right is yellow;
/// the star in the lower-left is green; the star the lower-right is
/// blue.](View-overlay-2)
///
/// If you specify more than one view in the `content` closure, the modifier
/// collects all of the views in the closure into an implicit ``ZStack``,
/// taking them in order from back to front. For example, you can place a
/// star and a ``Circle`` on a field of ``ShapeStyle/blue``:
///
/// Color.blue
/// .frame(width: 200, height: 200)
/// .overlay {
/// Circle()
/// .frame(width: 100, height: 100)
/// Star()
/// }
///
/// Both the overlay modifier and the implicit ``ZStack`` composed from the
/// overlay content --- the circle and the star --- use a default
/// ``Alignment/center`` alignment. The star appears centered on the circle,
/// and both appear as a composite view centered in front of the square:
///
/// ![A screenshot of a star centered on a circle, which is
/// centered on a square.](View-overlay-3)
///
/// If you specify an alignment for the overlay, it applies to the implicit
/// stack rather than to the individual views in the closure. You can see
/// this if you add the ``Alignment/bottom`` alignment:
///
/// Color.blue
/// .frame(width: 200, height: 200)
/// .overlay(alignment: .bottom) {
/// Circle()
/// .frame(width: 100, height: 100)
/// Star()
/// }
///
/// The circle and the star move down as a unit to align the stack's bottom
/// edge with the bottom edge of the square, while the star remains
/// centered on the circle:
///
/// ![A screenshot of a star centered on a circle, which is on a square.
/// The circle's bottom edge is aligned with the square's bottom
/// edge.](View-overlay-3a)
///
/// To control the placement of individual items inside the `content`
/// closure, either use a different overlay modifier for each item, as the
/// earlier example of stars in the corners of a rectangle demonstrates, or
/// add an explicit ``ZStack`` inside the content closure with its own
/// alignment:
///
/// Color.blue
/// .frame(width: 200, height: 200)
/// .overlay(alignment: .bottom) {
/// ZStack(alignment: .bottom) {
/// Circle()
/// .frame(width: 100, height: 100)
/// Star()
/// }
/// }
///
/// The stack alignment ensures that the star's bottom edge aligns with the
/// circle's, while the overlay aligns the composite view with the square:
///
/// ![A screenshot of a star, a circle, and a square with all their
/// bottom edges aligned.](View-overlay-4)
///
/// You can achieve layering without an overlay modifier by putting both the
/// modified view and the overlay content into a ``ZStack``. This can
/// produce a simpler view hierarchy, but changes the layout priority that
/// SwiftUI applies to the views. Use the overlay modifier when you want the
/// modified view to dominate the layout.
///
/// If you want to specify a ``ShapeStyle`` like a ``Color`` or a
/// ``Material`` as the overlay, use
/// ``View/overlay(_:ignoresSafeAreaEdges:)`` instead. To specify a
/// ``Shape``, use ``View/overlay(_:in:fillStyle:)``.
///
/// - Parameters:
/// - alignment: The alignment that the modifier uses to position the
/// implicit ``ZStack`` that groups the foreground views. The default
/// is ``Alignment/center``.
/// - content: A ``ViewBuilder`` that you use to declare the views to
/// draw in front of this view, stacked in the order that you list them.
/// The last view that you list appears at the front of the stack.
///
/// - Returns: A view that uses the specified content as a foreground.
func overlay<Content: View>(alignment: Alignment = .center, @ViewBuilder _ content: () -> Content) -> some View {
self.content.overlay(content(), alignment: alignment)
}
}

View File

@ -0,0 +1,256 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped == Any {
public struct ProgressView<Label: View, CurrentValueLabel: View>: View {
@Environment(\.backportProgressViewStyle) private var style
let config: Backport<Any>.ProgressViewStyleConfiguration
@available(macOS 10.15, *)
public var body: some View {
Group {
if let style = style {
style.makeBody(configuration: config)
} else {
DefaultProgressViewStyle().makeBody(configuration: config)
}
}
}
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, unavailable)
extension Backport.ProgressView where Wrapped == Any, CurrentValueLabel == EmptyView {
/// Creates a progress view for showing indeterminate progress, without a
/// label.
public init() where Label == EmptyView {
self.init(config: .init(fractionCompleted: nil, preferredKind: .circular))
}
@available(macOS 10.15, *)
/// Creates a progress view for showing indeterminate progress that displays
/// a custom label.
///
/// - Parameters:
/// - label: A view builder that creates a view that describes the task
/// in progress.
public init(@ViewBuilder label: () -> Label) {
config = .init(fractionCompleted: nil, label: .init(content: label()), preferredKind: .circular)
}
@available(macOS 10.15, *)
/// Creates a progress view for showing indeterminate progress that
/// generates its label from a localized string.
///
/// This initializer creates a ``Text`` view on your behalf, and treats the
/// localized key similar to ``Text/init(_:tableName:bundle:comment:)``. See
/// ``Text`` for more information about localizing strings. To initialize a
/// indeterminate progress view with a string variable, use
/// the corresponding initializer that takes a `StringProtocol` instance.
///
/// - Parameters:
/// - titleKey: The key for the progress view's localized title that
/// describes the task in progress.
public init(_ titleKey: LocalizedStringKey) where Label == Text {
config = .init(fractionCompleted: nil, label: .init(content: Text(titleKey)), preferredKind: .circular)
}
@available(macOS 10.15, *)
/// Creates a progress view for showing indeterminate progress that
/// generates its label from a string.
///
/// - Parameters:
/// - title: A string that describes the task in progress.
///
/// This initializer creates a ``Text`` view on your behalf, and treats the
/// title similar to ``Text/init(verbatim:)``. See ``Text`` for more
/// information about localizing strings. To initialize a progress view with
/// a localized string key, use the corresponding initializer that takes a
/// `LocalizedStringKey` instance.
public init<S>(_ title: S) where Label == Text, S: StringProtocol {
config = .init(fractionCompleted: nil, label: .init(content: Text(title)), preferredKind: .circular)
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport.ProgressView where Wrapped == Any {
/// Creates a progress view for showing determinate progress.
///
/// If the value is non-`nil`, but outside the range of `0.0` through
/// `total`, the progress view pins the value to those limits, rounding to
/// the nearest possible bound. A value of `nil` represents indeterminate
/// progress, in which case the progress view ignores `total`.
///
/// - Parameters:
/// - value: The completed amount of the task to this point, in a range
/// of `0.0` to `total`, or `nil` if the progress is indeterminate.
/// - total: The full amount representing the complete scope of the
/// task, meaning the task is complete if `value` equals `total`. The
/// default value is `1.0`.
public init<V>(value: V?, total: V = 1.0)
where Label == EmptyView, CurrentValueLabel == EmptyView, V: BinaryFloatingPoint
{
if let value = value {
config = .init(fractionCompleted: Double(value) / Double(total), preferredKind: .linear, max: Double(total))
} else {
config = .init(fractionCompleted: nil, preferredKind: .linear)
}
}
@available(macOS 10.15, *)
/// Creates a progress view for showing determinate progress, with a
/// custom label.
///
/// If the value is non-`nil`, but outside the range of `0.0` through
/// `total`, the progress view pins the value to those limits, rounding to
/// the nearest possible bound. A value of `nil` represents indeterminate
/// progress, in which case the progress view ignores `total`.
///
/// - Parameters:
/// - value: The completed amount of the task to this point, in a range
/// of `0.0` to `total`, or `nil` if the progress is indeterminate.
/// - total: The full amount representing the complete scope of the
/// task, meaning the task is complete if `value` equals `total`. The
/// default value is `1.0`.
/// - label: A view builder that creates a view that describes the task
/// in progress.
public init<V>(value: V?, total: V = 1.0, @ViewBuilder label: () -> Label)
where CurrentValueLabel == EmptyView, V: BinaryFloatingPoint
{
if let value = value {
config = .init(
fractionCompleted: Double(value) / Double(total), label: .init(content: label()), preferredKind: .linear
)
} else {
config = .init(fractionCompleted: nil, label: .init(content: label()), preferredKind: .linear, max: Double(total))
}
}
@available(macOS 10.15, *)
/// Creates a progress view for showing determinate progress, with a
/// custom label.
///
/// If the value is non-`nil`, but outside the range of `0.0` through
/// `total`, the progress view pins the value to those limits, rounding to
/// the nearest possible bound. A value of `nil` represents indeterminate
/// progress, in which case the progress view ignores `total`.
///
/// - Parameters:
/// - value: The completed amount of the task to this point, in a range
/// of `0.0` to `total`, or `nil` if the progress is indeterminate.
/// - total: The full amount representing the complete scope of the
/// task, meaning the task is complete if `value` equals `total`. The
/// default value is `1.0`.
/// - label: A view builder that creates a view that describes the task
/// in progress.
/// - currentValueLabel: A view builder that creates a view that
/// describes the level of completed progress of the task.
public init<V>(
value: V?, total: V = 1.0, @ViewBuilder label: () -> Label, @ViewBuilder currentValueLabel: () -> CurrentValueLabel
) where V: BinaryFloatingPoint {
if let value = value {
config = .init(
fractionCompleted: Double(value) / Double(total), label: .init(content: label()),
currentValueLabel: .init(content: currentValueLabel()), preferredKind: .linear, max: Double(total)
)
} else {
config = .init(
fractionCompleted: nil, label: .init(content: label()), currentValueLabel: .init(content: currentValueLabel()),
preferredKind: .linear, max: Double(total)
)
}
}
@available(macOS 10.15, *)
/// Creates a progress view for showing determinate progress that generates
/// its label from a localized string.
///
/// If the value is non-`nil`, but outside the range of `0.0` through
/// `total`, the progress view pins the value to those limits, rounding to
/// the nearest possible bound. A value of `nil` represents indeterminate
/// progress, in which case the progress view ignores `total`.
///
/// This initializer creates a ``Text`` view on your behalf, and treats the
/// localized key similar to ``Text/init(_:tableName:bundle:comment:)``. See
/// ``Text`` for more information about localizing strings. To initialize a
/// determinate progress view with a string variable, use
/// the corresponding initializer that takes a `StringProtocol` instance.
///
/// - Parameters:
/// - titleKey: The key for the progress view's localized title that
/// describes the task in progress.
/// - value: The completed amount of the task to this point, in a range
/// of `0.0` to `total`, or `nil` if the progress is
/// indeterminate.
/// - total: The full amount representing the complete scope of the
/// task, meaning the task is complete if `value` equals `total`. The
/// default value is `1.0`.
public init<V>(_ titleKey: LocalizedStringKey, value: V?, total: V = 1.0)
where Label == Text, CurrentValueLabel == EmptyView, V: BinaryFloatingPoint
{
if let value = value {
config = .init(
fractionCompleted: Double(value) / Double(total), label: .init(content: Text(titleKey)), preferredKind: .linear,
max: Double(total)
)
} else {
config = .init(
fractionCompleted: nil, label: .init(content: Text(titleKey)), preferredKind: .linear, max: Double(total)
)
}
}
@available(macOS 10.15, *)
/// Creates a progress view for showing determinate progress that generates
/// its label from a string.
///
/// If the value is non-`nil`, but outside the range of `0.0` through
/// `total`, the progress view pins the value to those limits, rounding to
/// the nearest possible bound. A value of `nil` represents indeterminate
/// progress, in which case the progress view ignores `total`.
///
/// This initializer creates a ``Text`` view on your behalf, and treats the
/// title similar to ``Text/init(verbatim:)``. See ``Text`` for more
/// information about localizing strings. To initialize a determinate
/// progress view with a localized string key, use the corresponding
/// initializer that takes a `LocalizedStringKey` instance.
///
/// - Parameters:
/// - title: The string that describes the task in progress.
/// - value: The completed amount of the task to this point, in a range
/// of `0.0` to `total`, or `nil` if the progress is
/// indeterminate.
/// - total: The full amount representing the complete scope of the
/// task, meaning the task is complete if `value` equals `total`. The
/// default value is `1.0`.
public init<S, V>(_ title: S, value: V?, total: V = 1.0)
where Label == Text, CurrentValueLabel == EmptyView, S: StringProtocol, V: BinaryFloatingPoint
{
if let value = value {
config = .init(
fractionCompleted: Double(value) / Double(total), label: .init(content: Text(title)), preferredKind: .linear,
max: Double(total)
)
} else {
config = .init(
fractionCompleted: nil, label: .init(content: Text(title)), preferredKind: .linear, max: Double(total)
)
}
}
}

View File

@ -0,0 +1,73 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped == Any {
/// The properties of a progress view instance.
public struct ProgressViewStyleConfiguration {
internal enum Kind {
case circular
case linear
}
@available(macOS 10.15, *)
/// A type-erased label describing the task represented by the progress
/// view.
public struct Label: View {
let content: AnyView
public var body: some View { content }
init<Content: View>(content: Content) {
self.content = .init(content)
}
}
@available(macOS 10.15, *)
/// A type-erased label that describes the current value of a progress view.
public struct CurrentValueLabel: View {
let content: AnyView
public var body: some View { content }
init<Content: View>(content: Content) {
self.content = .init(content)
}
}
@available(macOS 10.15, *)
/// The completed fraction of the task represented by the progress view,
/// from `0.0` (not yet started) to `1.0` (fully complete), or `nil` if the
/// progress is indeterminate or relative to a date interval.
public let fractionCompleted: Double?
@available(macOS 10.15, *)
/// A view that describes the task represented by the progress view.
///
/// If `nil`, then the task is self-evident from the surrounding context,
/// and the style does not need to provide any additional description.
///
/// If the progress view is defined using a `Progress` instance, then this
/// label is equivalent to its `localizedDescription`.
public var label: Label?
@available(macOS 10.15, *)
/// A view that describes the current value of a progress view.
///
/// If `nil`, then the value of the progress view is either self-evident
/// from the surrounding context or unknown, and the style does not need to
/// provide any additional description.
///
/// If the progress view is defined using a `Progress` instance, then this
/// label is equivalent to its `localizedAdditionalDescription`.
public var currentValueLabel: CurrentValueLabel?
@available(macOS 10.15, *)
internal let preferredKind: Kind
internal var min: Double = 0
internal var max: Double = 1
}
}

View File

@ -0,0 +1,74 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
@available(macOS 10.15, *)
/// A type that applies standard interaction behavior to all progress views
/// within a view hierarchy.
///
/// To configure the current progress view style for a view hierarchy, use the
/// ``View/progressViewStyle(_:)`` modifier.
public protocol BackportProgressViewStyle {
/// A type alias for the properties of a progress view instance.
typealias Configuration = Backport<Any>.ProgressViewStyleConfiguration
/// A view representing the body of a progress view.
associatedtype Body: View
/// Creates a view representing the body of a progress view.
///
/// - Parameter configuration: The properties of the progress view being
/// created.
///
/// The view hierarchy calls this method for each progress view where this
/// style is the current progress view style.
///
/// - Parameter configuration: The properties of the progress view, such as
/// its preferred progress type.
@ViewBuilder func makeBody(configuration: Configuration) -> Body
}
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
@available(macOS 10.15, *)
extension Backport where Wrapped: View {
public func progressViewStyle<S: BackportProgressViewStyle>(_ style: S) -> some View {
content.environment(\.backportProgressViewStyle, .init(style))
}
}
@available(macOS 10.15, *)
internal struct AnyProgressViewStyle: BackportProgressViewStyle {
let _makeBody: (Backport<Any>.ProgressViewStyleConfiguration) -> AnyView
init<S: BackportProgressViewStyle>(_ style: S) {
_makeBody = { config in
AnyView(style.makeBody(configuration: config))
}
}
func makeBody(configuration: Configuration) -> some View {
_makeBody(configuration)
}
}
@available(macOS 10.15, *)
private struct BackportProgressViewStyleEnvironmentKey: EnvironmentKey {
static var defaultValue: AnyProgressViewStyle?
}
@available(macOS 10.15, *)
extension EnvironmentValues {
var backportProgressViewStyle: AnyProgressViewStyle? {
get { self[BackportProgressViewStyleEnvironmentKey.self] }
set { self[BackportProgressViewStyleEnvironmentKey.self] = newValue }
}
}

View File

@ -0,0 +1,89 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped == Any {
/// A progress view that visually indicates its progress using a circular gauge.
///
/// You can also use ``ProgressViewStyle/circular`` to construct this style.
public struct CircularProgressViewStyle: BackportProgressViewStyle {
/// Creates a circular progress view style.
public init() {}
@available(macOS 10.15, *)
/// Creates a view representing the body of a progress view.
///
/// - Parameter configuration: The properties of the progress view being
/// created.
///
/// The view hierarchy calls this method for each progress view where this
/// style is the current progress view style.
///
/// - Parameter configuration: The properties of the progress view, such as
/// its preferred progress type.
public func makeBody(configuration: Configuration) -> some View {
VStack {
#if !os(watchOS)
CircularRepresentable(configuration: configuration)
#endif
configuration.label?
.foregroundColor(.secondary)
}
}
}
}
@available(macOS 10.15, *)
public extension BackportProgressViewStyle where Self == Backport<Any>.CircularProgressViewStyle {
static var circular: Self { .init() }
}
#if os(macOS)
@available(macOS 10.15, *)
private struct CircularRepresentable: NSViewRepresentable {
let configuration: Backport<Any>.ProgressViewStyleConfiguration
@available(macOS 10.15, *)
func makeNSView(context _: Context) -> NSProgressIndicator {
.init()
}
@available(macOS 10.15, *)
func updateNSView(_ view: NSProgressIndicator, context _: Context) {
if let value = configuration.fractionCompleted {
view.doubleValue = value
view.maxValue = configuration.max
}
view.isIndeterminate = configuration.fractionCompleted == nil
view.style = .spinning
view.isDisplayedWhenStopped = true
view.startAnimation(nil)
}
}
#elseif !os(watchOS)
@available(macOS 10.15, *)
private struct CircularRepresentable: UIViewRepresentable {
let configuration: Backport<Any>.ProgressViewStyleConfiguration
@available(macOS 10.15, *)
func makeUIView(context _: Context) -> UIActivityIndicatorView {
.init(style: .medium)
}
@available(macOS 10.15, *)
func updateUIView(_ view: UIActivityIndicatorView, context _: Context) {
view.hidesWhenStopped = false
view.startAnimating()
}
}
#endif

View File

@ -0,0 +1,54 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped == Any {
/// The default progress view style in the current context of the view being
/// styled.
///
/// You can also use ``ProgressViewStyle/automatic`` to construct this style.
public struct DefaultProgressViewStyle: BackportProgressViewStyle {
/// Creates a default progress view style.
public init() {}
@available(macOS 10.15, *)
/// Creates a view representing the body of a progress view.
///
/// - Parameter configuration: The properties of the progress view being
/// created.
///
/// The view hierarchy calls this method for each progress view where this
/// style is the current progress view style.
///
/// - Parameter configuration: The properties of the progress view, such as
/// its preferred progress type.
public func makeBody(configuration: Configuration) -> some View {
switch configuration.preferredKind {
case .circular:
Backport.CircularProgressViewStyle().makeBody(configuration: configuration)
case .linear:
#if os(iOS)
if configuration.fractionCompleted == nil {
Backport.CircularProgressViewStyle().makeBody(configuration: configuration)
} else {
Backport.LinearProgressViewStyle().makeBody(configuration: configuration)
}
#else
Backport.LinearProgressViewStyle().makeBody(configuration: configuration)
#endif
}
}
}
}
@available(macOS 10.15, *)
public extension BackportProgressViewStyle where Self == Backport<Any>.DefaultProgressViewStyle {
static var automatic: Self { .init() }
}

View File

@ -0,0 +1,111 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped == Any {
/// A progress view that visually indicates its progress using a horizontal bar.
///
/// You can also use ``ProgressViewStyle/linear`` to construct this style.
public struct LinearProgressViewStyle: BackportProgressViewStyle {
/// Creates a linear progress view style.
public init() {}
/// Creates a view representing the body of a progress view.
///
/// - Parameter configuration: The properties of the progress view being
/// created.
///
/// The view hierarchy calls this method for each progress view where this
/// style is the current progress view style.
///
/// - Parameter configuration: The properties of the progress view, such as
/// its preferred progress type.
@available(macOS 10.15, *)
public func makeBody(configuration: Configuration) -> some View {
#if os(macOS)
VStack(alignment: .leading, spacing: 0) {
configuration.label
.foregroundColor(.primary)
LinearRepresentable(configuration: configuration)
configuration.currentValueLabel
.foregroundColor(.secondary)
}
.controlSize(.small)
#else
VStack(alignment: .leading, spacing: 5) {
if configuration.fractionCompleted == nil {
CircularProgressViewStyle().makeBody(configuration: configuration)
} else {
configuration.label?
.foregroundColor(.primary)
#if !os(watchOS)
LinearRepresentable(configuration: configuration)
#endif
configuration.currentValueLabel?
.foregroundColor(.secondary)
.font(.caption)
}
}
#endif
}
}
}
@available(macOS 10.15, *)
public extension BackportProgressViewStyle where Self == Backport<Any>.LinearProgressViewStyle {
static var linear: Self { .init() }
}
#if os(macOS)
@available(macOS 10.15, *)
private struct LinearRepresentable: NSViewRepresentable {
let configuration: Backport<Any>.ProgressViewStyleConfiguration
@available(macOS 10.15, *)
func makeNSView(context _: Context) -> NSProgressIndicator {
.init()
}
@available(macOS 10.15, *)
func updateNSView(_ view: NSProgressIndicator, context _: Context) {
if let value = configuration.fractionCompleted {
view.doubleValue = value
view.maxValue = configuration.max
view.display()
}
view.style = .bar
view.isIndeterminate = configuration.fractionCompleted == nil
view.isDisplayedWhenStopped = true
view.startAnimation(nil)
}
}
#elseif !os(watchOS)
@available(macOS 10.15, *)
private struct LinearRepresentable: UIViewRepresentable {
let configuration: Backport<Any>.ProgressViewStyleConfiguration
@available(macOS 10.15, *)
func makeUIView(context _: Context) -> UIProgressView {
.init(progressViewStyle: .default)
}
@available(macOS 10.15, *)
func updateUIView(_ view: UIProgressView, context _: Context) {
view.progress = Float(configuration.fractionCompleted ?? 0)
}
}
#endif

View File

@ -0,0 +1,88 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
#if os(iOS)
import QuickLook
final class PreviewController<Items>: UIViewController, UIAdaptivePresentationControllerDelegate,
QLPreviewControllerDelegate, QLPreviewControllerDataSource
where Items: RandomAccessCollection, Items.Element == URL
{
var items: Items
var selection: Binding<Items.Element?> {
didSet {
updateControllerLifecycle(
from: oldValue.wrappedValue,
to: selection.wrappedValue
)
}
}
init(selection: Binding<Items.Element?>, in items: Items) {
self.selection = selection
self.items = items
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateControllerLifecycle(from oldValue: Items.Element?, to newValue: Items.Element?) {
switch (oldValue, newValue) {
case (.none, .some):
presentController()
case (.some, .some):
updateController()
case (.some, .none):
dismissController()
case (.none, .none):
break
}
}
private func presentController() {
print("Present")
let controller = QLPreviewController(nibName: nil, bundle: nil)
controller.dataSource = self
controller.delegate = self
present(controller, animated: true)
updateController()
}
private func updateController() {
let controller = presentedViewController as? QLPreviewController
controller?.reloadData()
let index = selection.wrappedValue.flatMap { items.firstIndex(of: $0) }
controller?.currentPreviewItemIndex = items.distance(from: items.startIndex, to: index ?? items.startIndex)
}
private func dismissController() {
DispatchQueue.main.async {
self.selection.wrappedValue = nil
}
}
func numberOfPreviewItems(in _: QLPreviewController) -> Int {
items.isEmpty ? 1 : items.count
}
func previewController(_: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
if items.isEmpty {
return (selection.wrappedValue ?? URL(fileURLWithPath: "")) as NSURL
} else {
let index = items.index(items.startIndex, offsetBy: index)
return items[index] as NSURL
}
}
func previewControllerDidDismiss(_: QLPreviewController) {
dismissController()
}
}
#endif

View File

@ -0,0 +1,115 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
#if os(macOS)
import QuickLook
import QuickLookUI
@available(macOS 10.15, *)
final class PreviewController<Items>: NSViewController, QLPreviewPanelDataSource, QLPreviewPanelDelegate
where Items: RandomAccessCollection, Items.Element == URL
{
private let panel = QLPreviewPanel.shared()!
private weak var windowResponder: NSResponder?
var items: Items
var selection: Binding<Items.Element?> {
didSet {
updateControllerLifecycle(
from: oldValue.wrappedValue,
to: selection.wrappedValue
)
}
}
private func updateControllerLifecycle(from oldValue: Items.Element?, to newValue: Items.Element?) {
switch (oldValue, newValue) {
case (.none, .some):
present()
case (.some, .some):
update()
case (.some, .none):
dismiss()
case (.none, .none):
break
}
}
init(selection: Binding<Items.Element?>, in items: Items) {
self.selection = selection
self.items = items
super.init(nibName: nil, bundle: nil)
windowResponder = NSApp.mainWindow?.nextResponder
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
view = .init(frame: .zero)
}
var isVisible: Bool {
QLPreviewPanel.sharedPreviewPanelExists() && panel.isVisible
}
private func present() {
print("Present")
NSApp.mainWindow?.nextResponder = self
if isVisible {
panel.updateController()
let index = selection.wrappedValue.flatMap { items.firstIndex(of: $0) }
panel.currentPreviewItemIndex = items.distance(from: items.startIndex, to: index ?? items.startIndex)
} else {
panel.makeKeyAndOrderFront(nil)
}
}
private func update() {
present()
}
private func dismiss() {
selection.wrappedValue = nil
}
func numberOfPreviewItems(in _: QLPreviewPanel!) -> Int {
items.isEmpty ? 1 : items.count
}
func previewPanel(_: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! {
if items.isEmpty {
return selection.wrappedValue as? NSURL
} else {
let index = items.index(items.startIndex, offsetBy: index)
return items[index] as NSURL
}
}
override func acceptsPreviewPanelControl(_: QLPreviewPanel!) -> Bool {
print("Accept")
return true
}
override func beginPreviewPanelControl(_ panel: QLPreviewPanel!) {
print("Begin")
panel.dataSource = self
panel.reloadData()
}
override func endPreviewPanelControl(_ panel: QLPreviewPanel!) {
print("End")
panel.dataSource = nil
dismiss()
}
}
#endif

View File

@ -0,0 +1,101 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
#if canImport(QuickLook)
import QuickLook
#endif
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
@available(macOS 10.15, *)
extension Backport where Wrapped: View {
/// Presents a Quick Look preview of the URLs you provide.
///
/// The Quick Look preview appears when you set the binding to a non-`nil` item.
/// When you set the item back to `nil`, Quick Look dismisses the preview.
/// If the value of the selection binding isnt contained in the items collection, Quick Look treats it the same as a `nil` selection.
///
/// Quick Look updates the value of the selection binding to match the URL of the file the user is previewing.
/// Upon dismissal by the user, Quick Look automatically sets the item binding to `nil`.
///
/// - Parameters:
/// - selection: A <doc://com.apple.documentation/documentation/SwiftUI/Binding> to an element thats part of the items collection. This is the URL that you currently want to preview.
/// - items: A collection of URLs to preview.
///
/// - Returns: A view that presents the preview of the contents of the URL.
public func quickLookPreview<Items>(_ selection: Binding<Items.Element?>, in items: Items) -> some View
where Items: RandomAccessCollection, Items.Element == URL
{
#if os(iOS) || os(macOS)
content.background(QuicklookSheet(selection: selection, items: items))
#else
content
#endif
}
/// Presents a Quick Look preview of the contents of a single URL.
///
/// The Quick Look preview appears when you set the binding to a non-`nil` item.
/// When you set the item back to `nil`, Quick Look dismisses the preview.
///
/// Upon dismissal by the user, Quick Look automatically sets the item binding to `nil`.
/// Quick Look displays the preview when a non-`nil` item is set.
/// Set `item` to `nil` to dismiss the preview.
///
/// - Parameters:
/// - item: A <doc://com.apple.documentation/documentation/SwiftUI/Binding> to a URL that should be previewed.
///
/// - Returns: A view that presents the preview of the contents of the URL.
public func quickLookPreview(_ item: Binding<URL?>) -> some View {
#if os(iOS) || os(macOS)
content.background(QuicklookSheet(selection: item, items: [item.wrappedValue].compactMap { $0 }))
#else
content
#endif
}
}
#if os(macOS)
import QuickLookUI
@available(macOS 10.15, *)
private struct QuicklookSheet<Items>: NSViewControllerRepresentable
where Items: RandomAccessCollection, Items.Element == URL
{
let selection: Binding<Items.Element?>
let items: Items
func makeNSViewController(context _: Context) -> PreviewController<Items> {
.init(selection: selection, in: items)
}
func updateNSViewController(_ controller: PreviewController<Items>, context _: Context) {
controller.selection = selection
controller.items = items
}
}
#elseif os(iOS)
private struct QuicklookSheet<Items>: UIViewControllerRepresentable
where Items: RandomAccessCollection, Items.Element == URL
{
let selection: Binding<Items.Element?>
let items: Items
func makeUIViewController(context _: Context) -> PreviewController<Items> {
.init(selection: selection, in: items)
}
func updateUIViewController(_ controller: PreviewController<Items>, context _: Context) {
controller.items = items
controller.selection = selection
}
}
#endif

View File

@ -0,0 +1,193 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(tvOS, deprecated: 15)
@available(watchOS, deprecated: 8)
extension Backport where Wrapped: View {
/// Marks this view as refreshable.
///
/// Apply this modifier to a view to set the ``EnvironmentValues/refresh``
/// value in the view's environment to a ``RefreshAction`` instance that
/// uses the specified `action` as its handler. Views that detect the
/// presence of the instance can change their appearance to provide a
/// way for the user to execute the handler.
///
/// You can add refresh capability to your own views as well. For
/// information on how to do that, see ``RefreshAction``.
///
/// - Parameters:
/// - action: An asynchronous handler that SwiftUI executes when the
/// user requests a refresh. Use this handler to initiate
/// an update of model data displayed in the modified view. Use
/// `await` in front of any asynchronous calls inside the handler.
/// - Returns: A view with a new refresh action in its environment.
public func refreshable(action: @escaping @Sendable () async -> Void) -> some View {
#if os(iOS)
content
.environment(\.backportRefresh, Backport<Any>.RefreshAction(action))
.inspect { inspector in
inspector.sibling(ofType: UITableView.self)
} customize: { scrollView in
guard scrollView.refreshControl == nil else { return }
scrollView.refreshControl = RefreshControl {
await action()
}
}
#else
content
.environment(\.backportRefresh, Backport<Any>.RefreshAction(action))
#endif
}
}
#if os(iOS)
private final class RefreshControl: UIRefreshControl {
var handler: (() async -> Void)?
init(_ handler: @escaping () async -> Void) {
super.init()
self.handler = { [weak self] in
Task { [weak self] in
await handler()
self?.endRefreshing()
}
}
addTarget(self, action: #selector(update), for: .valueChanged)
}
@MainActor
override func endRefreshing() {
super.endRefreshing()
}
@objc private func update() {
Task { await handler?() }
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
#endif
@available(iOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(tvOS, deprecated: 15)
@available(watchOS, deprecated: 8)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// An action that initiates a refresh operation.
///
/// Unlike the official implementation, this backport does not affect any
/// view's like `List` to provide automatic pull-to-refresh behaviour.
///
/// You can use this to offer refresh capability in your custom views.
/// Read the ``EnvironmentValues/refresh`` environment value to get the
/// `RefreshAction` instance for a given ``Environment``. If you find
/// a non-`nil` value, change your view's appearance or behavior to offer
/// the refresh to the user, and call the instance to conduct the
/// refresh. You can call the refresh instance directly because it defines
/// a ``RefreshAction/callAsFunction()`` method that Swift calls
/// when you call the instance:
///
/// struct RefreshableView: View {
/// @Environment(\.refresh) private var refresh
///
/// var body: some View {
/// Button("Refresh") {
/// Task {
/// await refresh?()
/// }
/// }
/// .disabled(refresh == nil)
/// }
/// }
///
/// Be sure to call the handler asynchronously by preceding it
/// with `await`. Because the call is asynchronous, you can use
/// its lifetime to indicate progress to the user. For example,
/// you might reveal an indeterminate ``ProgressView`` before
/// calling the handler, and hide it when the handler completes.
///
/// If your code isn't already in an asynchronous context, create a
/// <doc://com.apple.documentation/documentation/Swift/Task> for the
/// method to run in. If you do this, consider adding a way for the
/// user to cancel the task. For more information, see
/// [Concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html)
/// in *The Swift Programming Language*.
public struct RefreshAction {
private var action: () async -> Void
internal init(_ action: @escaping () async -> Void) {
self.action = action
}
public func callAsFunction() async {
await action()
}
}
}
@available(macOS 10.15, *)
private struct RefreshEnvironmentKey: EnvironmentKey {
static let defaultValue: Backport<Any>.RefreshAction? = nil
}
@available(iOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(tvOS, deprecated: 15)
@available(watchOS, deprecated: 8)
@available(macOS 10.15, *)
extension EnvironmentValues {
/// An action that initiates a refresh operation.
///
/// Unlike the official implementation, this backport does not affect any
/// view's like `List` to provide automatic pull-to-refresh behaviour.
///
/// You can use this to offer refresh capability in your custom views.
/// Read the ``EnvironmentValues/refresh`` environment value to get the
/// `RefreshAction` instance for a given ``Environment``. If you find
/// a non-`nil` value, change your view's appearance or behavior to offer
/// the refresh to the user, and call the instance to conduct the
/// refresh. You can call the refresh instance directly because it defines
/// a ``RefreshAction/callAsFunction()`` method that Swift calls
/// when you call the instance:
///
/// struct RefreshableView: View {
/// @Environment(\.refresh) private var refresh
///
/// var body: some View {
/// Button("Refresh") {
/// Task {
/// await refresh?()
/// }
/// }
/// .disabled(refresh == nil)
/// }
/// }
///
/// Be sure to call the handler asynchronously by preceding it
/// with `await`. Because the call is asynchronous, you can use
/// its lifetime to indicate progress to the user. For example,
/// you might reveal an indeterminate ``ProgressView`` before
/// calling the handler, and hide it when the handler completes.
///
/// If your code isn't already in an asynchronous context, create a
/// <doc://com.apple.documentation/documentation/Swift/Task> for the
/// method to run in. If you do this, consider adding a way for the
/// user to cancel the task. For more information, see
/// [Concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html)
/// in *The Swift Programming Language*.
public var backportRefresh: Backport<Any>.RefreshAction? {
get { self[RefreshEnvironmentKey.self] }
set { self[RefreshEnvironmentKey.self] = newValue }
}
}

View File

@ -0,0 +1,43 @@
import StoreKit
import SwiftUI
#if os(iOS) || os(macOS)
@available(macOS 10.15, *)
public extension EnvironmentValues {
/// An instance that tells StoreKit to request an App Store rating or review from the user, if appropriate.
/// Read the requestReview environment value to get an instance of this structure for a given Environment. Call the instance to tell StoreKit to ask the user to rate or review your app, if appropriate. You call the instance directly because it defines a callAsFunction() method that Swift calls when you call the instance.
///
/// Although you normally call this instance to request a review when it makes sense in the user experience flow of your app, the App Store policy governs the actual display of the rating and review request view. Because calling this instance may not present an alert, dont call it in response to a user action, such as a button tap.
///
/// > When you call this instance while your app is in development mode, the system always displays a rating and review request view so you can test the user interface and experience. This instance has no effect when you call it in an app that you distribute using TestFlight.
@MainActor var backportRequestReview: Backport<Any>.RequestReviewAction { .init() }
}
/// An instance that tells StoreKit to request an App Store rating or review from the user, if appropriate.
/// Read the requestReview environment value to get an instance of this structure for a given Environment. Call the instance to tell StoreKit to ask the user to rate or review your app, if appropriate. You call the instance directly because it defines a callAsFunction() method that Swift calls when you call the instance.
///
/// Although you normally call this instance to request a review when it makes sense in the user experience flow of your app, the App Store policy governs the actual display of the rating and review request view. Because calling this instance may not present an alert, dont call it in response to a user action, such as a button tap.
///
/// > When you call this instance while your app is in development mode, the system always displays a rating and review request view so you can test the user interface and experience. This instance has no effect when you call it in an app that you distribute using TestFlight.
///
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(macOS, deprecated: 13)
extension Backport where Wrapped == Any {
@MainActor public struct RequestReviewAction {
public func callAsFunction() {
#if os(macOS)
SKStoreReviewController.requestReview()
#else
if #available(iOS 14, *) {
guard let scene = UIApplication.activeScene else { return }
SKStoreReviewController.requestReview(in: scene)
} else {
SKStoreReviewController.requestReview()
}
#endif
}
}
}
#endif

View File

@ -0,0 +1,58 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 15)
@available(tvOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(watchOS, deprecated: 8)
extension Backport where Wrapped == Any {
/// A container view that you can use to add hierarchy to certain collection views.
///
/// Use `Section` instances in views like ``List``, ``Picker``, and
/// ``Form`` to organize content into separate sections. Each section has
/// custom content that you provide on a per-instance basis. You can also
/// provide headers and footers for each section.
public struct Section<Parent: View, Content: View, Footer: View>: View {
@ViewBuilder let content: () -> Content
@ViewBuilder let header: () -> Parent
@ViewBuilder let footer: () -> Footer
@available(macOS 10.15, *)
public var body: some View {
SwiftUI.Section(
content: content,
header: header,
footer: footer
)
}
}
}
@available(macOS 10.15, *)
public extension Backport.Section where Wrapped == Any, Parent == Text, Footer == EmptyView {
/// Creates a section with the provided section content.
/// - Parameters:
/// - titleKey: The key for the section's localized title, which describes
/// the contents of the section.
/// - content: The section's content.
init(_ titleKey: LocalizedStringKey, @ViewBuilder content: @escaping () -> Content) {
header = { Text(titleKey) }
self.content = content
footer = { EmptyView() }
}
@available(macOS 10.15, *)
/// Creates a section with the provided section content.
/// - Parameters:
/// - title: A string that describes the contents of the section.
/// - content: The section's content.
init<S>(_ title: S, @ViewBuilder content: @escaping () -> Content) where S: StringProtocol {
header = { Text(title) }
self.content = content
footer = { EmptyView() }
}
}

View File

@ -0,0 +1,159 @@
import Combine
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 14.0)
@available(macOS, deprecated: 11.0)
@available(tvOS, deprecated: 14.0)
@available(watchOS, deprecated: 7.0)
extension Backport where Wrapped: ObservableObject {
/// A property wrapper type that instantiates an observable object.
///
/// Create a state object in a ``SwiftUI/View``, ``SwiftUI/App``, or
/// ``SwiftUI/Scene`` by applying the `@Backport.StateObject` attribute to a property
/// declaration and providing an initial value that conforms to the
/// <doc://com.apple.documentation/documentation/Combine/ObservableObject>
/// protocol:
///
/// @Backport.StateObject var model = DataModel()
///
/// SwiftUI creates a new instance of the object only once for each instance of
/// the structure that declares the object. When published properties of the
/// observable object change, SwiftUI updates the parts of any view that depend
/// on those properties:
///
/// Text(model.title) // Updates the view any time `title` changes.
///
/// You can pass the state object into a property that has the
/// ``SwiftUI/ObservedObject`` attribute. You can alternatively add the object
/// to the environment of a view hierarchy by applying the
/// ``SwiftUI/View/environmentObject(_:)`` modifier:
///
/// ContentView()
/// .environmentObject(model)
///
/// If you create an environment object as shown in the code above, you can
/// read the object inside `ContentView` or any of its descendants
/// using the ``SwiftUI/EnvironmentObject`` attribute:
///
/// @EnvironmentObject var model: DataModel
///
/// Get a ``SwiftUI/Binding`` to one of the state object's properties using the
/// `$` operator. Use a binding when you want to create a two-way connection to
/// one of the object's properties. For example, you can let a
/// ``SwiftUI/Toggle`` control a Boolean value called `isEnabled` stored in the
/// model:
///
/// Toggle("Enabled", isOn: $model.isEnabled)
@propertyWrapper public struct StateObject: DynamicProperty {
private final class Wrapper: ObservableObject {
private var subject = PassthroughSubject<Void, Never>()
@available(macOS 10.15, *)
var value: Wrapped? {
didSet {
cancellable = nil
cancellable = value?.objectWillChange
.sink { [subject] _ in subject.send() }
}
}
@available(macOS 10.15, *)
private var cancellable: AnyCancellable?
@available(macOS 10.15, *)
var objectWillChange: AnyPublisher<Void, Never> {
subject.eraseToAnyPublisher()
}
}
@available(macOS 10.15, *)
@State private var state = Wrapper()
@available(macOS 10.15, *)
@ObservedObject private var observedObject = Wrapper()
@available(macOS 10.15, *)
private var thunk: () -> Wrapped
@available(macOS 10.15, *)
/// The underlying value referenced by the state object.
///
/// The wrapped value property provides primary access to the value's data.
/// However, you don't access `wrappedValue` directly. Instead, use the
/// property variable created with the `@Backport.StateObject` attribute:
///
/// @Backport.StateObject var contact = Contact()
///
/// var body: some View {
/// Text(contact.name) // Accesses contact's wrapped value.
/// }
///
/// When you change a property of the wrapped value, you can access the new
/// value immediately. However, SwiftUI updates views displaying the value
/// asynchronously, so the user interface might not update immediately.
public var wrappedValue: Wrapped {
if let object = state.value {
return object
} else {
let object = thunk()
state.value = object
return object
}
}
@available(macOS 10.15, *)
/// A projection of the state object that creates bindings to its
/// properties.
///
/// Use the projected value to pass a binding value down a view hierarchy.
/// To get the projected value, prefix the property variable with `$`. For
/// example, you can get a binding to a model's `isEnabled` Boolean so that
/// a ``SwiftUI/Toggle`` view can control the value:
///
/// struct MyView: View {
/// @Backport.StateObject var model = DataModel()
///
/// var body: some View {
/// Toggle("Enabled", isOn: $model.isEnabled)
/// }
/// }
public var projectedValue: ObservedObject<Wrapped>.Wrapper {
ObservedObject(wrappedValue: wrappedValue).projectedValue
}
@available(macOS 10.15, *)
/// Creates a new state object with an initial wrapped value.
///
/// You dont call this initializer directly. Instead, declare a property
/// with the `@Backport.StateObject` attribute in a ``SwiftUI/View``,
/// ``SwiftUI/App``, or ``SwiftUI/Scene``, and provide an initial value:
///
/// struct MyView: View {
/// @Backport.StateObject var model = DataModel()
///
/// // ...
/// }
///
/// SwiftUI creates only one instance of the state object for each
/// container instance that you declare. In the code above, SwiftUI
/// creates `model` only the first time it initializes a particular instance
/// of `MyView`. On the other hand, each different instance of `MyView`
/// receives a distinct copy of the data model.
///
/// - Parameter thunk: An initial value for the state object.
public init(wrappedValue thunk: @autoclosure @escaping () -> Wrapped) {
self.thunk = thunk
}
@available(macOS 10.15, *)
public mutating func update() {
if state.value == nil {
state.value = thunk()
}
if observedObject.value !== state.value {
observedObject.value = state.value
}
}
}
}

View File

@ -0,0 +1,178 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 15.0)
@available(macOS, deprecated: 12.0)
@available(tvOS, deprecated: 15.0)
@available(watchOS, deprecated: 8.0)
extension Backport where Wrapped: View {
/// Adds an asynchronous task to perform when this view appears.
///
/// Use this modifier to perform an asynchronous task with a lifetime that
/// matches that of the modified view. If the task doesn't finish
/// before SwiftUI removes the view or the view changes identity, SwiftUI
/// cancels the task.
///
/// Use the `await` keyword inside the task to
/// wait for an asynchronous call to complete.
///
/// let url = URL(string: "https://example.com")!
/// @State private var message = "Loading..."
///
/// var body: some View {
/// Text(message)
/// .task {
/// do {
/// var receivedLines = [String]()
/// for try await line in url.lines {
/// receivedLines.append(line)
/// message = "Received \(receivedLines.count) lines"
/// }
/// } catch {
/// message = "Failed to load"
/// }
/// }
/// }
///
/// When each new line arrives, the body of the `for`-`await`-`in`
/// loop stores the line in an array of strings and updates the content of the
/// text view to report the latest line count.
///
/// - Parameters:
/// - priority: The task priority to use when creating the asynchronous
/// task. The default priority is `.userInitiated`
/// - action: A closure that SwiftUI calls as an asynchronous task
/// when the view appears. SwiftUI automatically cancels the task
/// if the view disappears before the action completes.
///
///
/// - Returns: A view that runs the specified action asynchronously when
/// the view appears.
@ViewBuilder
public func task(priority: TaskPriority = .userInitiated, _ action: @MainActor @escaping @Sendable () async -> Void)
-> some View
{
content.modifier(
TaskModifier(
id: 0,
priority: priority,
action: action
)
)
}
@available(macOS 10.15, *)
/// Adds a task to perform when this view appears or when a specified
/// value changes.
///
/// This method behaves like ``View/task(priority:_:)``, except that it also
/// cancels and recreates the task when a specified value changes. To detect
/// a change, the modifier tests whether a new value for the `id` parameter
/// equals the previous value. For this to work,
/// the value's type must conform to the `Equatable` protocol.
///
/// For example, if you define an equatable `Server` type that posts custom
/// notifications whenever its state changes --- for example, from _signed
/// out_ to _signed in_ --- you can use the task modifier to update
/// the contents of a ``Text`` view to reflect the state of the
/// currently selected server:
///
/// Text(status ?? "Signed Out")
/// .task(id: server) {
/// let sequence = NotificationCenter.default.notifications(
/// named: .didChangeStatus,
/// object: server)
/// for try await notification in sequence {
/// status = notification.userInfo["status"] as? String
/// }
/// }
///
/// Elsewhere, the server defines a custom `didUpdateStatus` notification:
///
/// extension NSNotification.Name {
/// static var didUpdateStatus: NSNotification.Name {
/// NSNotification.Name("didUpdateStatus")
/// }
/// }
///
/// The server then posts a notification of this type whenever its status
/// changes, like after the user signs in:
///
/// let notification = Notification(
/// name: .didUpdateStatus,
/// object: self,
/// userInfo: ["status": "Signed In"])
/// NotificationCenter.default.post(notification)
///
/// The task attached to the ``Text`` view gets and displays the status
/// value from the notification's user information dictionary. When the user
/// chooses a different server, SwiftUI cancels the task and creates a new
/// one, which then starts waiting for notifications from the new server.
///
/// - Parameters:
/// - id: The value to observe for changes. The value must conform
/// to the `Equatable` protocol.
/// - priority: The task priority to use when creating the asynchronous
/// task. The default priority is `.userInitiated`
/// - action: A closure that SwiftUI calls as an asynchronous task
/// when the view appears. SwiftUI automatically cancels the task
/// if the view disappears before the action completes. If the
/// `id` value changes, SwiftUI cancels and restarts the task.
///
/// - Returns: A view that runs the specified action asynchronously when
/// the view appears, or restarts the task with the `id` value changes.
@ViewBuilder
public func task<T: Equatable>(
id: T, priority: TaskPriority = .userInitiated, _ action: @MainActor @escaping @Sendable () async -> Void
) -> some View {
content.modifier(
TaskModifier(
id: id,
priority: priority,
action: action
)
)
}
}
@available(macOS 10.15, *)
private struct TaskModifier<ID: Equatable>: ViewModifier {
var id: ID
var priority: TaskPriority
var action: () async -> Void
@available(macOS 10.15, *)
@State private var task: Task<Void, Never>?
@available(macOS 10.15, *)
init(id: ID, priority: TaskPriority, action: @MainActor @escaping () async -> Void) {
self.id = id
self.priority = priority
self.action = action
}
@available(macOS 10.15, *)
func body(content: Content) -> some View {
content
.backport.onChange(of: id) { _ in
task?.cancel()
task = Task(priority: priority) {
await action()
}
}
.onAppear {
task?.cancel()
task = Task(priority: priority) {
await action()
}
}
.onDisappear {
task?.cancel()
task = nil
}
}
}

View File

@ -0,0 +1,34 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
public extension Backport where Wrapped == AnyTransition {
/// Creates a transition that when added to a view will animate the views insertion by moving it in from the specified edge while fading it in, and animate its removal by moving it out towards the opposite edge and fading it out.
/// - Parameter edge: the edge from which the view will be animated in.
/// - Returns: A transition that animates a view by moving and fading it.
@available(iOS, deprecated: 16.0)
@available(watchOS, deprecated: 9.0)
@available(macOS, deprecated: 13.0)
@available(tvOS, deprecated: 16.0)
func push(from edge: Edge) -> AnyTransition {
var oppositeEdge: Edge
switch edge {
case .top:
oppositeEdge = .bottom
case .leading:
oppositeEdge = .trailing
case .bottom:
oppositeEdge = .top
case .trailing:
oppositeEdge = .leading
}
return .asymmetric(
insertion: .move(edge: edge),
removal: .move(edge: oppositeEdge)
).combined(with: .opacity)
}
}

View File

@ -0,0 +1,168 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import Foundation
@available(macOS 10.15, *)
@available(iOS, deprecated: 15.0)
@available(macOS, deprecated: 12.0)
@available(tvOS, deprecated: 15.0)
@available(watchOS, deprecated: 8.0)
extension Backport where Wrapped: URLSession {
/// Start a data task with a URL using async/await.
/// - parameter url: The URL to send a request to.
/// - returns: A tuple containing the binary `Data` that was downloaded,
/// as well as a `URLResponse` representing the server's response.
/// - throws: Any error encountered while performing the data task.
public func data(from url: URL) async throws -> (Data, URLResponse) {
try await data(for: URLRequest(url: url))
}
/// Start a data task with a `URLRequest` using async/await.
/// - parameter request: The `URLRequest` that the data task should perform.
/// - returns: A tuple containing the binary `Data` that was downloaded,
/// as well as a `URLResponse` representing the server's response.
/// - throws: Any error encountered while performing the data task.
public func data(for request: URLRequest) async throws -> (Data, URLResponse) {
let sessionTask = URLSessionTaskActor()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.start(
content.dataTask(with: request) { data, response, error in
guard let data = data, let response = response else {
let error = error ?? URLError(.badServerResponse)
continuation.resume(throwing: error)
return
}
continuation.resume(returning: (data, response))
})
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
public func upload(for request: URLRequest, fromFile fileURL: URL) async throws -> (Data, URLResponse) {
let sessionTask = URLSessionTaskActor()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.start(
content.uploadTask(with: request, fromFile: fileURL) { data, response, error in
guard let data = data, let response = response else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: (data, response))
})
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
public func upload(for request: URLRequest, from bodyData: Data) async throws -> (Data, URLResponse) {
let sessionTask = URLSessionTaskActor()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.start(
content.uploadTask(with: request, from: bodyData) { data, response, error in
guard let data = data, let response = response else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: (data, response))
})
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
public func download(for request: URLRequest) async throws -> (URL, URLResponse) {
let sessionTask = URLSessionTaskActor()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.start(
content.downloadTask(with: request) { data, response, error in
guard let data = data, let response = response else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: (data, response))
})
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
public func download(from url: URL) async throws -> (URL, URLResponse) {
let sessionTask = URLSessionTaskActor()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.start(
content.downloadTask(with: url) { data, response, error in
guard let data = data, let response = response else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: (data, response))
})
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
public func download(resumeFrom resumeData: Data) async throws -> (URL, URLResponse) {
let sessionTask = URLSessionTaskActor()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.start(
content.downloadTask(withResumeData: resumeData) { data, response, error in
guard let data = data, let response = response else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: (data, response))
})
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
}
@available(macOS 10.15, *)
private actor URLSessionTaskActor {
weak var task: URLSessionTask?
func start(_ task: URLSessionTask) {
self.task = task
task.resume()
}
func cancel() {
task?.cancel()
}
}

View File

@ -0,0 +1,42 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 15)
@available(macOS, deprecated: 12)
@available(tvOS, deprecated: 15)
@available(watchOS, deprecated: 8)
extension Backport where Wrapped == Any {
public enum Visibility: Hashable, CaseIterable {
/// The element may be visible or hidden depending on the policies of the
/// component accepting the visibility configuration.
///
/// For example, some components employ different automatic behavior
/// depending on factors including the platform, the surrounding container,
/// user settings, etc.
case automatic
/// The element may be visible.
///
/// Some APIs may use this value to represent a hint or preference, rather
/// than a mandatory assertion. For example, setting list row separator
/// visibility to `visible` using the
/// ``View/listRowSeparator(_:edges:)`` modifier may not always
/// result in any visible separators, especially for list styles that do not
/// include separators as part of their design.
case visible
/// The element may be hidden.
///
/// Some APIs may use this value to represent a hint or preference, rather
/// than a mandatory assertion. For example, setting confirmation dialog
/// title visibility to `hidden` using the
/// ``View/confirmationDialog(_:isPresented:titleVisibility:actions:)-87n66``
/// modifier may not always hide the dialog title, which is required on
/// some platforms.
case hidden
}
}

View File

@ -0,0 +1,315 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension Backport where Wrapped: View {
/// Sets the available detents for the enclosing sheet.
///
/// By default, sheets support the ``PresentationDetent/large`` detent.
///
/// struct ContentView: View {
/// @State private var showSettings = false
///
/// var body: some View {
/// Button("View Settings") {
/// showSettings = true
/// }
/// .sheet(isPresented: $showSettings) {
/// SettingsView()
/// .presentationDetents([.medium, .large])
/// }
/// }
/// }
///
/// - Parameter detents: A set of supported detents for the sheet.
/// If you provide more than one detent, people can drag the sheet
/// to resize it.
@ViewBuilder
@available(iOS, introduced: 15, deprecated: 16, message: "Presentation detents are only supported in iOS 15+")
public func presentationDetents(_ detents: Set<Backport<Any>.PresentationDetent>) -> some View {
#if os(iOS)
content.background(Backport<Any>.Representable(detents: detents, selection: nil, largestUndimmed: .large))
#else
content
#endif
}
@available(macOS 10.15, *)
/// Sets the available detents for the enclosing sheet, giving you
/// programmatic control of the currently selected detent.
///
/// By default, sheets support the ``PresentationDetent/large`` detent.
///
/// struct ContentView: View {
/// @State private var showSettings = false
/// @State private var settingsDetent = PresentationDetent.medium
///
/// var body: some View {
/// Button("View Settings") {
/// showSettings = true
/// }
/// .sheet(isPresented: $showSettings) {
/// SettingsView()
/// .presentationDetents:(
/// [.medium, .large],
/// selection: $settingsDetent
/// )
/// }
/// }
/// }
///
/// - Parameters:
/// - detents: A set of supported detents for the sheet.
/// If you provide more that one detent, people can drag the sheet
/// to resize it.
/// - selection: A ``Binding`` to the currently selected detent.
/// Ensure that the value matches one of the detents that you
/// provide for the `detents` parameter.
@ViewBuilder
@available(iOS, introduced: 15, deprecated: 16, message: "Presentation detents are only supported in iOS 15+")
public func presentationDetents(
_ detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>
) -> some View {
#if os(iOS)
content.background(Backport<Any>.Representable(detents: detents, selection: selection, largestUndimmed: .large))
#else
content
#endif
}
@available(macOS 10.15, *)
/// Sets the available detents for the enclosing sheet, giving you
/// programmatic control of the currently selected detent.
///
/// By default, sheets support the ``PresentationDetent/large`` detent.
///
/// struct ContentView: View {
/// @State private var showSettings = false
/// @State private var settingsDetent = PresentationDetent.medium
///
/// var body: some View {
/// Button("View Settings") {
/// showSettings = true
/// }
/// .sheet(isPresented: $showSettings) {
/// SettingsView()
/// .presentationDetents:(
/// [.medium, .large],
/// selection: $settingsDetent
/// )
/// }
/// }
/// }
///
/// - Parameters:
/// - detents: A set of supported detents for the sheet.
/// If you provide more that one detent, people can drag the sheet
/// to resize it.
/// - selection: A ``Binding`` to the currently selected detent.
/// Ensure that the value matches one of the detents that you
/// provide for the `detents` parameter.
@ViewBuilder
@available(iOS, introduced: 15, deprecated: 16, message: "Presentation detents are only supported in iOS 15+")
public func presentationDetents(
_ detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>,
largestUndimmedDetent: Backport<Any>.PresentationDetent? = nil
) -> some View {
#if os(iOS)
content.background(
Backport<Any>.Representable(detents: detents, selection: selection, largestUndimmed: largestUndimmedDetent))
#else
content
#endif
}
}
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension Backport where Wrapped == Any {
/// A type that represents a height where a sheet naturally rests.
public struct PresentationDetent: Hashable, Comparable {
public struct Identifier: RawRepresentable, Hashable {
public var rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
@available(macOS 10.15, *)
public static var medium: Identifier {
.init(rawValue: "com.apple.UIKit.medium")
}
@available(macOS 10.15, *)
public static var large: Identifier {
.init(rawValue: "com.apple.UIKit.large")
}
}
@available(macOS 10.15, *)
public let id: Identifier
@available(macOS 10.15, *)
/// The system detent for a sheet that's approximately half the height of
/// the screen, and is inactive in compact height.
public static var medium: PresentationDetent {
.init(id: .medium)
}
@available(macOS 10.15, *)
/// The system detent for a sheet at full height.
public static var large: PresentationDetent {
.init(id: .large)
}
@available(macOS 10.15, *)
fileprivate static var none: PresentationDetent {
.init(id: .init(rawValue: ""))
}
@available(macOS 10.15, *)
public static func < (lhs: PresentationDetent, rhs: PresentationDetent) -> Bool {
switch (lhs, rhs) {
case (.large, .medium):
return false
default:
return true
}
}
}
}
#if os(iOS)
@available(iOS 15, *)
fileprivate extension Backport where Wrapped == Any {
struct Representable: UIViewControllerRepresentable {
let detents: Set<Backport<Any>.PresentationDetent>
let selection: Binding<Backport<Any>.PresentationDetent>?
let largestUndimmed: Backport<Any>.PresentationDetent?
func makeUIViewController(context _: Context) -> Backport.Representable.Controller {
Controller(detents: detents, selection: selection, largestUndimmed: largestUndimmed)
}
func updateUIViewController(_ controller: Backport.Representable.Controller, context _: Context) {
controller.update(detents: detents, selection: selection, largestUndimmed: largestUndimmed)
}
}
}
@available(macOS 10.15, *)
@available(iOS 15, *)
fileprivate extension Backport.Representable {
final class Controller: UIViewController, UISheetPresentationControllerDelegate {
var detents: Set<Backport<Any>.PresentationDetent>
var selection: Binding<Backport<Any>.PresentationDetent>?
var largestUndimmed: Backport<Any>.PresentationDetent?
weak var _delegate: UISheetPresentationControllerDelegate?
@available(macOS 10.15, *)
init(
detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>?,
largestUndimmed: Backport<Any>.PresentationDetent?
) {
self.detents = detents
self.selection = selection
self.largestUndimmed = largestUndimmed
super.init(nibName: nil, bundle: nil)
}
@available(macOS 10.15, *)
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@available(macOS 10.15, *)
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
if let controller = parent?.sheetPresentationController {
if controller.delegate !== self {
_delegate = controller.delegate
controller.delegate = self
}
}
update(detents: detents, selection: selection, largestUndimmed: largestUndimmed)
}
@available(macOS 10.15, *)
func update(
detents: Set<Backport<Any>.PresentationDetent>, selection: Binding<Backport<Any>.PresentationDetent>?,
largestUndimmed: Backport<Any>.PresentationDetent?
) {
self.detents = detents
self.selection = selection
self.largestUndimmed = largestUndimmed
if let controller = parent?.sheetPresentationController {
controller.animateChanges {
controller.detents = detents.sorted().map {
switch $0 {
case .medium:
return .medium()
default:
return .large()
}
}
controller.largestUndimmedDetentIdentifier = largestUndimmed.flatMap {
.init(rawValue: $0.id.rawValue)
}
if let selection = selection {
controller.selectedDetentIdentifier = .init(selection.wrappedValue.id.rawValue)
}
controller.prefersScrollingExpandsWhenScrolledToEdge = true
}
UIView.animate(withDuration: 0.25) {
if let undimmed = largestUndimmed {
controller.presentingViewController.view.tintAdjustmentMode =
(selection?.wrappedValue ?? .large) >= undimmed ? .automatic : .normal
} else {
controller.presentingViewController.view.tintAdjustmentMode = .automatic
}
}
}
}
@available(macOS 10.15, *)
func sheetPresentationControllerDidChangeSelectedDetentIdentifier(
_ sheetPresentationController: UISheetPresentationController
) {
guard
let selection = selection,
let id = sheetPresentationController.selectedDetentIdentifier?.rawValue,
selection.wrappedValue.id.rawValue != id
else { return }
selection.wrappedValue = .init(id: .init(rawValue: id))
}
@available(macOS 10.15, *)
override func responds(to aSelector: Selector!) -> Bool {
if super.responds(to: aSelector) { return true }
if _delegate?.responds(to: aSelector) ?? false { return true }
return false
}
@available(macOS 10.15, *)
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if super.responds(to: aSelector) { return self }
return _delegate
}
}
}
#endif

View File

@ -0,0 +1,102 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
extension Backport where Wrapped: View {
/// Sets the visibility of the drag indicator on top of a sheet.
///
/// You can show a drag indicator when it isn't apparent that a
/// sheet can resize or when the sheet can't dismiss interactively.
///
/// struct ContentView: View {
/// @State private var showSettings = false
///
/// var body: some View {
/// Button("View Settings") {
/// showSettings = true
/// }
/// .sheet(isPresented: $showSettings) {
/// SettingsView()
/// .presentationDetents:([.medium, .large])
/// .presentationDragIndicator(.visible)
/// }
/// }
/// }
///
/// - Parameter visibility: The preferred visibility of the drag indicator.
@ViewBuilder
public func presentationDragIndicator(_ visibility: Backport<Any>.Visibility) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
content.background(Backport<Any>.Representable(visibility: visibility))
} else {
content
}
#else
content
#endif
}
}
#if os(iOS)
@available(iOS 15, *)
@available(macOS 10.15, *)
fileprivate extension Backport where Wrapped == Any {
struct Representable: UIViewControllerRepresentable {
let visibility: Backport<Any>.Visibility
func makeUIViewController(context _: Context) -> Backport.Representable.Controller {
Controller(visibility: visibility)
}
func updateUIViewController(_ controller: Backport.Representable.Controller, context _: Context) {
controller.update(visibility: visibility)
}
}
}
@available(macOS 10.15, *)
@available(iOS 15, *)
fileprivate extension Backport.Representable {
final class Controller: UIViewController {
var visibility: Backport<Any>.Visibility
@available(macOS 10.15, *)
init(visibility: Backport<Any>.Visibility) {
self.visibility = visibility
super.init(nibName: nil, bundle: nil)
}
@available(macOS 10.15, *)
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@available(macOS 10.15, *)
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
update(visibility: visibility)
}
@available(macOS 10.15, *)
func update(visibility: Backport<Any>.Visibility) {
self.visibility = visibility
if let controller = parent?.sheetPresentationController {
controller.animateChanges {
controller.prefersGrabberVisible = visibility == .visible
controller.prefersScrollingExpandsWhenScrolledToEdge = true
}
}
}
}
}
#endif

View File

@ -0,0 +1,104 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
@available(macOS 10.15, *)
extension Backport where Wrapped: View {
/// Removes dimming from detents higher (and including) the provided identifier
///
/// This has two affects on dentents higher than the identifier provided:
/// 1. Touches will passthrough to the views below the sheet.
/// 2. Touches will no longer dismiss the sheet automatically when tapping outside of the sheet.
///
/// ```
/// struct ContentView: View {
/// @State private var showSettings = false
///
/// var body: some View {
/// Button("View Settings") {
/// showSettings = true
/// }
/// .sheet(isPresented: $showSettings) {
/// SettingsView()
/// .presentationDetents:([.medium, .large])
/// .presentationUndimmed(from: .medium)
/// }
/// }
/// }
/// ```
///
/// - Parameter identifier: The identifier of the largest detent that is not dimmed.
@ViewBuilder
@available(
iOS, deprecated: 13, message: "Please use backport.presentationDetents(_:selection:largestUndimmedDetent:)"
)
public func presentationUndimmed(from identifier: Backport<Any>.PresentationDetent.Identifier?) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
content.background(Backport<Any>.Representable(identifier: identifier))
} else {
content
}
#else
content
#endif
}
}
#if os(iOS)
@available(iOS 15, *)
fileprivate extension Backport where Wrapped == Any {
struct Representable: UIViewControllerRepresentable {
let identifier: Backport<Any>.PresentationDetent.Identifier?
func makeUIViewController(context _: Context) -> Backport.Representable.Controller {
Controller(identifier: identifier)
}
func updateUIViewController(_ controller: Backport.Representable.Controller, context _: Context) {
controller.update(identifier: identifier)
}
}
}
@available(iOS 15, *)
fileprivate extension Backport.Representable {
final class Controller: UIViewController {
var identifier: Backport<Any>.PresentationDetent.Identifier?
init(identifier: Backport<Any>.PresentationDetent.Identifier?) {
self.identifier = identifier
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
update(identifier: identifier)
}
func update(identifier: Backport<Any>.PresentationDetent.Identifier?) {
self.identifier = identifier
if let controller = parent?.sheetPresentationController {
controller.animateChanges {
controller.presentingViewController.view.tintAdjustmentMode = .normal
controller.largestUndimmedDetentIdentifier = identifier.flatMap {
.init(rawValue: $0.rawValue)
}
}
}
}
}
}
#endif

View File

@ -0,0 +1,256 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(macOS 10.15, *)
public extension Backport where Wrapped: View {
/// Conditionally prevents interactive dismissal of a popover or a sheet.
///
/// Users can dismiss certain kinds of presentations using built-in
/// gestures. In particular, a user can dismiss a sheet by dragging it down,
/// or a popover by clicking or tapping outside of the presented view. Use
/// the `interactiveDismissDisabled(_:)` modifier to conditionally prevent
/// this kind of dismissal. You typically do this to prevent the user from
/// dismissing a presentation before providing needed data or completing
/// a required action.
///
/// For instance, suppose you have a view that displays a licensing
/// agreement that the user must acknowledge before continuing:
///
/// struct TermsOfService: View {
/// @Binding var areTermsAccepted: Bool
/// @Environment(\.backportDismiss) private var dismiss
///
/// var body: some View {
/// Form {
/// Text("License Agreement")
/// .font(.title)
/// Text("Terms and conditions go here.")
/// Button("Accept") {
/// areTermsAccepted = true
/// dismiss()
/// }
/// }
/// }
/// }
///
/// If you present this view in a sheet, the user can dismiss it by either
/// tapping the button --- which calls ``EnvironmentValues/backportDismiss``
/// from its `action` closure --- or by dragging the sheet down. To
/// ensure that the user accepts the terms by tapping the button,
/// disable interactive dismissal, conditioned on the `areTermsAccepted`
/// property:
///
/// struct ContentView: View {
/// @State private var isSheetPresented = false
/// @State private var areTermsAccepted = false
///
/// var body: some View {
/// Button("Use Service") {
/// isSheetPresented = true
/// }
/// .sheet(isPresented: $isSheetPresented) {
/// TermsOfService()
/// .backport.interactiveDismissDisabled(!areTermsAccepted)
/// }
/// }
/// }
///
/// You can apply the modifier to any view in the sheet's view hierarchy,
/// including to the sheet's top level view, as the example demonstrates,
/// or to any child view, like the ``Form`` or the Accept ``Button``.
///
/// The modifier has no effect on programmatic dismissal, which you can
/// invoke by updating the ``Binding`` that controls the presentation, or
/// by calling the environment's ``EnvironmentValues/backportDismiss`` action.
///
/// > This modifier currently has no effect on macOS, tvOS or watchOS.
///
/// - Parameter isDisabled: A Boolean value that indicates whether to
/// prevent nonprogrammatic dismissal of the containing view hierarchy
/// when presented in a sheet or popover.
@ViewBuilder
@available(iOS, deprecated: 16)
@available(tvOS, deprecated: 16)
@available(macOS, deprecated: 13)
@available(watchOS, deprecated: 9)
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: nil))
} else {
content
}
#else
content
#endif
}
@available(macOS 10.15, *)
/// Conditionally prevents interactive dismissal of a popover or a sheet. In addition, provides fine-grained control over the dismissal
///
/// Users can dismiss certain kinds of presentations using built-in
/// gestures. In particular, a user can dismiss a sheet by dragging it down,
/// or a popover by clicking or tapping outside of the presented view. Use
/// the `interactiveDismissDisabled(_:)` modifier to conditionally prevent
/// this kind of dismissal. You typically do this to prevent the user from
/// dismissing a presentation before providing needed data or completing
/// a required action.
///
/// For instance, suppose you have a view that displays a licensing
/// agreement that the user must acknowledge before continuing:
///
/// struct TermsOfService: View {
/// @Binding var areTermsAccepted: Bool
/// @Environment(\.backportDismiss) private var dismiss
///
/// var body: some View {
/// Form {
/// Text("License Agreement")
/// .font(.title)
/// Text("Terms and conditions go here.")
/// Button("Accept") {
/// areTermsAccepted = true
/// dismiss()
/// }
/// }
/// }
/// }
///
/// If you present this view in a sheet, the user can dismiss it by either
/// tapping the button --- which calls ``EnvironmentValues/backportDismiss``
/// from its `action` closure --- or by dragging the sheet down. To
/// ensure that the user accepts the terms by tapping the button,
/// disable interactive dismissal, conditioned on the `areTermsAccepted`
/// property:
///
/// struct ContentView: View {
/// @State private var isSheetPresented = false
/// @State private var areTermsAccepted = false
///
/// var body: some View {
/// Button("Use Service") {
/// isSheetPresented = true
/// }
/// .sheet(isPresented: $isSheetPresented) {
/// TermsOfService()
/// .backport.interactiveDismissDisabled(!areTermsAccepted)
/// }
/// }
/// }
///
/// You can apply the modifier to any view in the sheet's view hierarchy,
/// including to the sheet's top level view, as the example demonstrates,
/// or to any child view, like the ``Form`` or the Accept ``Button``.
///
/// The modifier has no effect on programmatic dismissal, which you can
/// invoke by updating the ``Binding`` that controls the presentation, or
/// by calling the environment's ``EnvironmentValues/backportDismiss`` action.
///
/// > This modifier currently has no effect on macOS, tvOS or watchOS.
///
/// - Parameter isDisabled: A Boolean value that indicates whether to
/// prevent nonprogrammatic dismissal of the containing view hierarchy
/// when presented in a sheet or popover.
/// - Parameter onAttempt: A closure that will be called when an interactive dismiss attempt occurs.
/// You can use this as an opportunity to present an confirmation or prompt to the user.
@ViewBuilder
func interactiveDismissDisabled(_ isDisabled: Bool = true, onAttempt: @escaping () -> Void) -> some View {
#if os(iOS)
if #available(iOS 15, *) {
content.background(Backport<Any>.Representable(isModal: isDisabled, onAttempt: onAttempt))
} else {
content
}
#else
content
#endif
}
}
#if os(iOS)
@available(macOS 10.15, *)
fileprivate extension Backport where Wrapped == Any {
struct Representable: UIViewControllerRepresentable {
let isModal: Bool
let onAttempt: (() -> Void)?
@available(macOS 10.15, *)
func makeUIViewController(context _: Context) -> Backport.Representable.Controller {
Controller(isModal: isModal, onAttempt: onAttempt)
}
@available(macOS 10.15, *)
func updateUIViewController(_ controller: Backport.Representable.Controller, context _: Context) {
controller.update(isModal: isModal, onAttempt: onAttempt)
}
}
}
@available(macOS 10.15, *)
fileprivate extension Backport.Representable {
final class Controller: UIViewController, UIAdaptivePresentationControllerDelegate {
var isModal: Bool
var onAttempt: (() -> Void)?
weak var _delegate: UIAdaptivePresentationControllerDelegate?
@available(macOS 10.15, *)
init(isModal: Bool, onAttempt: (() -> Void)?) {
self.isModal = isModal
self.onAttempt = onAttempt
super.init(nibName: nil, bundle: nil)
}
@available(macOS 10.15, *)
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@available(macOS 10.15, *)
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
if let controller = parent?.presentationController {
if controller.delegate !== self {
_delegate = controller.delegate
controller.delegate = self
}
}
update(isModal: isModal, onAttempt: onAttempt)
}
@available(macOS 10.15, *)
func update(isModal: Bool, onAttempt: (() -> Void)?) {
self.isModal = isModal
self.onAttempt = onAttempt
parent?.isModalInPresentation = isModal
}
@available(macOS 10.15, *)
func presentationControllerDidAttemptToDismiss(_: UIPresentationController) {
onAttempt?()
}
@available(macOS 10.15, *)
func presentationControllerShouldDismiss(_: UIPresentationController) -> Bool {
parent?.isModalInPresentation == false
}
@available(macOS 10.15, *)
override func responds(to aSelector: Selector!) -> Bool {
if super.responds(to: aSelector) { return true }
if _delegate?.responds(to: aSelector) ?? false { return true }
return false
}
@available(macOS 10.15, *)
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if super.responds(to: aSelector) { return self }
return _delegate
}
}
}
#endif

View File

@ -0,0 +1,81 @@
// (c) 2022 and onwards Shaps Benkau (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import SwiftUI
@available(iOS, deprecated: 14)
@available(macOS, deprecated: 11)
@available(tvOS, deprecated: 14)
@available(watchOS, deprecated: 7)
@available(macOS 10.15, *)
extension Backport where Wrapped == Any {
/// A dynamic property that scales a numeric value.
@propertyWrapper
public struct ScaledMetric<Value>: DynamicProperty where Value: BinaryFloatingPoint {
@Environment(\.sizeCategory) var sizeCategory
private let baseValue: Value
#if os(iOS) || os(tvOS)
private let metrics: UIFontMetrics
#endif
public var wrappedValue: Value {
#if os(iOS) || os(tvOS)
let traits = UITraitCollection(traitsFrom: [
UITraitCollection(preferredContentSizeCategory: UIContentSizeCategory(sizeCategory: sizeCategory)),
])
return Value(metrics.scaledValue(for: CGFloat(baseValue), compatibleWith: traits))
#else
return baseValue
#endif
}
#if os(iOS) || os(tvOS)
/// Creates the scaled metric with an unscaled value using the default scaling.
public init(baseValue: Value, metrics: UIFontMetrics) {
self.baseValue = baseValue
self.metrics = metrics
}
/// Creates the scaled metric with an unscaled value using the default scaling.
public init(wrappedValue: Value) {
self.init(baseValue: wrappedValue, metrics: UIFontMetrics(forTextStyle: .body))
}
/// Creates the scaled metric with an unscaled value and a text style to scale relative to.
public init(wrappedValue: Value, relativeTo textStyle: UIFont.TextStyle) {
self.init(baseValue: wrappedValue, metrics: UIFontMetrics(forTextStyle: textStyle))
}
#else
/// Creates the scaled metric with an unscaled value using the default scaling.
public init(wrappedValue: Value) {
baseValue = wrappedValue
}
#endif
}
}
#if os(iOS) || os(tvOS)
fileprivate extension UIContentSizeCategory {
init(sizeCategory: ContentSizeCategory?) {
switch sizeCategory {
case .accessibilityExtraExtraExtraLarge: self = .accessibilityExtraExtraExtraLarge
case .accessibilityExtraExtraLarge: self = .accessibilityExtraExtraLarge
case .accessibilityExtraLarge: self = .accessibilityExtraLarge
case .accessibilityLarge: self = .accessibilityLarge
case .accessibilityMedium: self = .accessibilityMedium
case .extraExtraExtraLarge: self = .extraExtraExtraLarge
case .extraExtraLarge: self = .extraExtraLarge
case .extraLarge: self = .extraLarge
case .extraSmall: self = .extraSmall
case .large: self = .large
case .medium: self = .medium
case .small: self = .small
default: self = .unspecified
}
}
}
#endif

Some files were not shown because too many files have changed in this diff Show More