-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1 parent
54fd495
commit 52f22b9
Showing
19 changed files
with
2,365 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// | ||
// Copyright (c) Nathan Tannar | ||
// | ||
|
||
import SwiftUI | ||
|
||
@frozen | ||
public struct IdentifiableBox<Value, ID: Hashable>: Identifiable { | ||
public var value: Value | ||
public var keyPath: KeyPath<Value, ID> | ||
|
||
public var id: ID { value[keyPath: keyPath] } | ||
|
||
@inlinable | ||
public init(_ value: Value, id keyPath: KeyPath<Value, ID>) { | ||
self.value = value | ||
self.keyPath = keyPath | ||
} | ||
} | ||
|
47 changes: 47 additions & 0 deletions
47
Sources/Turbocharger/Sources/Extensions/Alignment+Extensions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// | ||
// Copyright (c) Nathan Tannar | ||
// | ||
|
||
import SwiftUI | ||
|
||
extension NSTextAlignment { | ||
public init( | ||
alignment: HorizontalAlignment, | ||
layoutDirection: LayoutDirection | ||
) { | ||
switch alignment { | ||
case .center: | ||
self.init(alignment: TextAlignment.center, layoutDirection: layoutDirection) | ||
case .trailing: | ||
self.init(alignment: TextAlignment.trailing, layoutDirection: layoutDirection) | ||
default: | ||
self.init(alignment: TextAlignment.leading, layoutDirection: layoutDirection) | ||
} | ||
} | ||
|
||
public init( | ||
alignment: TextAlignment, | ||
layoutDirection: LayoutDirection | ||
) { | ||
switch alignment { | ||
case .center: | ||
self = .center | ||
default: | ||
switch layoutDirection { | ||
case .rightToLeft: | ||
if alignment == .leading { | ||
self = .right | ||
} else { | ||
self = .left | ||
} | ||
default: | ||
if alignment == .leading { | ||
self = .left | ||
} else { | ||
self = .right | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
65 changes: 65 additions & 0 deletions
65
Sources/Turbocharger/Sources/Extensions/AnyShapeStyle+Extensions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
// | ||
// Copyright (c) Nathan Tannar | ||
// | ||
|
||
import SwiftUI | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
extension AnyShapeStyle { | ||
|
||
public var color: Color? { | ||
func resolve(provider: Any) -> Color? { | ||
let className = String(describing: type(of: provider)) | ||
if className.hasPrefix("ColorBox") { | ||
guard MemoryLayout<Color>.size == MemoryLayout<AnyObject>.size else { | ||
return nil | ||
} | ||
let color = unsafeBitCast(provider as AnyObject, to: Color.self) | ||
return color | ||
} else if className.hasPrefix("GradientBox") { | ||
guard | ||
let provider = Mirror(reflecting: provider).descendant("base", "color", "provider"), | ||
let resolved = resolve(provider: provider) | ||
else { | ||
return nil | ||
} | ||
return resolved | ||
} else { | ||
return nil | ||
} | ||
} | ||
|
||
guard | ||
let box = Mirror(reflecting: self).descendant("storage", "box"), | ||
let resolved = resolve(provider: box) | ||
else { | ||
return nil | ||
} | ||
return resolved | ||
} | ||
} | ||
|
||
// MARK: - Previews | ||
|
||
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) | ||
struct AnyShapeStyle_Previews: PreviewProvider { | ||
static var previews: some View { | ||
VStack { | ||
HStack { | ||
Rectangle() | ||
.fill(AnyShapeStyle(Color.red).color ?? .clear) | ||
|
||
Rectangle() | ||
.fill(Color.red) | ||
} | ||
|
||
HStack { | ||
Rectangle() | ||
.fill(AnyShapeStyle(Color.red.gradient).color ?? .clear) | ||
|
||
Rectangle() | ||
.fill(Color.red.gradient) | ||
} | ||
} | ||
} | ||
} |
127 changes: 127 additions & 0 deletions
127
Sources/Turbocharger/Sources/Extensions/AttributedString+Extensions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
// | ||
// Copyright (c) Nathan Tannar | ||
// | ||
|
||
import SwiftUI | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
extension AttributedString { | ||
|
||
#if os(iOS) || os(tvOS) || os(watchOS) | ||
public func toUIKit( | ||
in environment: EnvironmentValues = EnvironmentValues() | ||
) -> AttributedString { | ||
var result = self | ||
for run in result.runs { | ||
result[run.range].setAttributes(run.attributes.toUIKit(in: environment)) | ||
} | ||
return result | ||
} | ||
#elseif os(macOS) | ||
public func toAppKit( | ||
in environment: EnvironmentValues = EnvironmentValues() | ||
) -> AttributedString { | ||
var result = self | ||
for run in result.runs { | ||
result[run.range].setAttributes(run.attributes.toAppKit(in: environment)) | ||
} | ||
return result | ||
} | ||
#endif | ||
} | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
extension AttributeContainer { | ||
|
||
#if os(iOS) || os(tvOS) || os(watchOS) | ||
public func toUIKit( | ||
in environment: EnvironmentValues = EnvironmentValues() | ||
) -> AttributeContainer { | ||
var attributes = self | ||
if let font = attributes.swiftUI.font, attributes.uiKit.font == nil { | ||
attributes.uiKit.font = font.toUIFont() | ||
} | ||
if let foregroundColor = attributes.swiftUI.foregroundColor, attributes.uiKit.foregroundColor == nil { | ||
attributes.uiKit.foregroundColor = foregroundColor.toUIColor() | ||
} | ||
if let backgroundColor = attributes.swiftUI.backgroundColor, attributes.uiKit.backgroundColor == nil { | ||
attributes.uiKit.backgroundColor = backgroundColor.toUIColor() | ||
} | ||
if let strikethroughStyle = attributes.swiftUI.strikethroughStyle { | ||
attributes.uiKit.strikethroughStyle = NSUnderlineStyle(strikethroughStyle) | ||
let color = Mirror(reflecting: strikethroughStyle).descendant("color") as? Color | ||
if let color { | ||
attributes.uiKit.strikethroughColor = color.toUIColor() | ||
} | ||
} | ||
if let underlineStyle = attributes.swiftUI.underlineStyle { | ||
attributes.uiKit.underlineStyle = NSUnderlineStyle(underlineStyle) | ||
let color = Mirror(reflecting: underlineStyle).descendant("color") as? Color | ||
if let color { | ||
attributes.uiKit.underlineColor = color.toUIColor() | ||
} | ||
} | ||
if let kern = attributes.swiftUI.kern { | ||
attributes.uiKit.kern = kern | ||
} | ||
if let tracking = attributes.swiftUI.tracking { | ||
attributes.uiKit.tracking = tracking | ||
} | ||
if let baselineOffset = attributes.swiftUI.baselineOffset { | ||
attributes.uiKit.baselineOffset = baselineOffset | ||
} | ||
let paragraphStyle = NSMutableParagraphStyle() | ||
paragraphStyle.lineSpacing = environment.lineSpacing | ||
attributes.uiKit.paragraphStyle = paragraphStyle | ||
return attributes | ||
} | ||
#elseif os(macOS) | ||
public func toAppKit( | ||
in environment: EnvironmentValues = EnvironmentValues() | ||
) -> AttributeContainer { | ||
var attributes = self | ||
if let font = attributes.swiftUI.font, attributes.appKit.font == nil { | ||
attributes.appKit.font = font.toNSFont() | ||
} | ||
if let foregroundColor = attributes.swiftUI.foregroundColor, attributes.appKit.foregroundColor == nil { | ||
attributes.appKit.foregroundColor = foregroundColor.toNSColor() | ||
} | ||
if let backgroundColor = attributes.swiftUI.backgroundColor, attributes.appKit.backgroundColor == nil { | ||
attributes.appKit.backgroundColor = backgroundColor.toNSColor() | ||
} | ||
if let strikethroughStyle = attributes.swiftUI.strikethroughStyle { | ||
attributes.appKit.strikethroughStyle = NSUnderlineStyle(strikethroughStyle) | ||
let color = Mirror(reflecting: strikethroughStyle).descendant("color") as? Color | ||
if let color { | ||
attributes.appKit.strikethroughColor = color.toNSColor() | ||
} | ||
} | ||
if let underlineStyle = attributes.swiftUI.underlineStyle { | ||
attributes.appKit.underlineStyle = NSUnderlineStyle(underlineStyle) | ||
let color = Mirror(reflecting: underlineStyle).descendant("color") as? Color | ||
if let color { | ||
attributes.appKit.underlineColor = color.toNSColor() | ||
} | ||
} | ||
if let kern = attributes.swiftUI.kern { | ||
attributes.appKit.kern = kern | ||
} | ||
if let tracking = attributes.swiftUI.tracking { | ||
attributes.appKit.tracking = tracking | ||
} | ||
if let baselineOffset = attributes.swiftUI.baselineOffset { | ||
attributes.appKit.baselineOffset = baselineOffset | ||
} | ||
let paragraphStyle = NSMutableParagraphStyle() | ||
paragraphStyle.lineSpacing = environment.lineSpacing | ||
attributes.appKit.paragraphStyle = paragraphStyle | ||
return attributes | ||
} | ||
#endif | ||
} | ||
|
||
extension NSParagraphStyle: @unchecked Sendable { } | ||
|
||
#if os(macOS) | ||
extension NSFont: @unchecked Sendable { } | ||
#endif |
84 changes: 84 additions & 0 deletions
84
Sources/Turbocharger/Sources/Extensions/Color+Extensions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
// | ||
// Copyright (c) Nathan Tannar | ||
// | ||
|
||
import SwiftUI | ||
|
||
extension Color { | ||
|
||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) | ||
public func toCGColor() -> CGColor { | ||
if let cgColor = cgColor { | ||
return cgColor | ||
} else { | ||
return PlatformRepresentable(self).cgColor | ||
} | ||
} | ||
|
||
#if os(iOS) || os(tvOS) || os(watchOS) | ||
@available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) | ||
public func toUIColor() -> UIColor { | ||
toPlatformValue() | ||
} | ||
#endif | ||
|
||
#if os(macOS) | ||
@available(macOS 11.0, *) | ||
public func toNSColor() -> NSColor { | ||
toPlatformValue() | ||
} | ||
#endif | ||
|
||
#if os(macOS) | ||
typealias PlatformRepresentable = NSColor | ||
#elseif os(iOS) || os(tvOS) || os(watchOS) | ||
typealias PlatformRepresentable = UIColor | ||
#endif | ||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) | ||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) | ||
private func toPlatformValue() -> PlatformRepresentable { | ||
func resolve(provider: Any) -> PlatformRepresentable { | ||
let className = String(describing: type(of: provider)) | ||
switch className { | ||
case "OpacityColor": | ||
let mirror = Mirror(reflecting: provider) | ||
guard | ||
let opacity = mirror.descendant("opacity") as? Double, | ||
let base = mirror.descendant("base") | ||
else { | ||
return PlatformRepresentable(self) | ||
} | ||
let color = resolve(provider: base) | ||
return color.withAlphaComponent(opacity) | ||
|
||
case "NamedColor": | ||
let mirror = Mirror(reflecting: provider) | ||
guard | ||
let name = mirror.descendant("name") as? String | ||
else { | ||
return PlatformRepresentable(self) | ||
} | ||
let bundle = mirror.descendant("bundle") as? Bundle | ||
#if os(iOS) || os(tvOS) | ||
return UIColor { traits in | ||
UIColor(named: name, in: bundle, compatibleWith: traits) ?? UIColor(self) | ||
} | ||
#elseif os(watchOS) | ||
return UIColor(named: name) ?? UIColor(self) | ||
#else | ||
return NSColor(named: name, bundle: bundle) ?? NSColor(self) | ||
#endif | ||
default: | ||
return provider as? PlatformRepresentable ?? PlatformRepresentable(self) | ||
} | ||
} | ||
|
||
// Need to extract the UIColor since because SwiftUI's UIColor init | ||
// from a Color does not work for dynamic colors when set on UIView's | ||
guard let base = Mirror(reflecting: self).descendant("provider", "base") else { | ||
return PlatformRepresentable(self) | ||
} | ||
return resolve(provider: base) | ||
} | ||
#endif | ||
} |
35 changes: 35 additions & 0 deletions
35
Sources/Turbocharger/Sources/Extensions/EdgeInsets+Extensions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
// | ||
// Copyright (c) Nathan Tannar | ||
// | ||
|
||
import SwiftUI | ||
|
||
#if os(macOS) | ||
extension NSEdgeInsets { | ||
public init( | ||
edgeInsets: EdgeInsets, | ||
layoutDirection: LayoutDirection | ||
) { | ||
self.init( | ||
top: edgeInsets.top, | ||
left: layoutDirection == .leftToRight ? edgeInsets.leading : edgeInsets.trailing, | ||
bottom: edgeInsets.bottom, | ||
right: layoutDirection == .leftToRight ? edgeInsets.trailing : edgeInsets.leading | ||
) | ||
} | ||
} | ||
#else | ||
extension UIEdgeInsets { | ||
public init( | ||
edgeInsets: EdgeInsets, | ||
layoutDirection: LayoutDirection | ||
) { | ||
self.init( | ||
top: edgeInsets.top, | ||
left: layoutDirection == .leftToRight ? edgeInsets.leading : edgeInsets.trailing, | ||
bottom: edgeInsets.bottom, | ||
right: layoutDirection == .leftToRight ? edgeInsets.trailing : edgeInsets.leading | ||
) | ||
} | ||
} | ||
#endif |
179 changes: 179 additions & 0 deletions
179
Sources/Turbocharger/Sources/Extensions/Environment+Extensions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
// | ||
// Copyright (c) Nathan Tannar | ||
// | ||
|
||
import SwiftUI | ||
import Engine | ||
|
||
/// Accessors to internal keys ``Engine.EnvironmentKeyVisitor`` | ||
extension EnvironmentValues { | ||
|
||
/// The value for the ``.labelsHidden(_)`` modifier | ||
public var labelsHidden: Bool { | ||
self["LabelsHiddenKey", default: false] | ||
} | ||
|
||
/// The value for the ``.foregroundStyle(_)``/``.foregroundColor(_)`` modifier | ||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
public var foregroundStyle: AnyShapeStyle { | ||
self["ForegroundStyleKey", default: AnyShapeStyle(.foreground)] | ||
} | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
public var foregroundColor: Color? { | ||
foregroundStyle.color | ||
} | ||
|
||
/// The value for the ``.tint(_)`` modifier | ||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
public var tint: AnyShapeStyle { | ||
self["TintKey", default: AnyShapeStyle(.tint)] | ||
} | ||
|
||
/// The value for the ``.accentColor(_)`` modifier | ||
public var accentColor: Color { | ||
self["AccentColorKey", default: Color.accentColor] | ||
} | ||
|
||
/// The value for the ``.underlineStyle(_)`` modifier | ||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
public var underlineStyle: Text.LineStyle? { | ||
self["UnderlineStyleKey"] | ||
} | ||
|
||
/// The value for the ``.strikethrough(_)`` modifier | ||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
public var strikethroughStyle: Text.LineStyle? { | ||
self["StrikethroughStyleKey"] | ||
} | ||
|
||
/// The value for the ``.kerning(_)`` modifier | ||
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) | ||
public var kerning: CGFloat { | ||
self["DefaultKerningKey", default: 0] | ||
} | ||
|
||
/// The value for the ``.tracking(_)`` modifier | ||
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) | ||
public var tracking: CGFloat { | ||
self["DefaultTrackingKey", default: 0] | ||
} | ||
|
||
/// The value for the ``.baselineOffset(_)`` modifier | ||
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) | ||
public var baselineOffset: CGFloat { | ||
self["DefaultBaselineOffsetKey", default: 0] | ||
} | ||
|
||
/// The value for the ``.lineLimit(_, reservesSpace: Bool)`` modifier | ||
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) | ||
public var lowerLineLimit: Int? { | ||
self["DefaultBaselineOffsetKey"] | ||
} | ||
|
||
/// The value for the ``.textScale(_)`` modifier | ||
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) | ||
public var textScale: Text.Scale { | ||
self["TextScaleKey", default: Text.Scale.default] | ||
} | ||
|
||
/// The value for the ``.imageScale(_)`` modifier | ||
@available(iOS 13.0, macOS 11.0, tvOS 13.0, watchOS 6.0, *) | ||
public var imageScale: Image.Scale? { | ||
self["ImageScaleKey"] | ||
} | ||
|
||
#if os(iOS) || os(tvOS) || os(watchOS) | ||
/// The value for the ``.textInputAutocapitalization(_)`` modifier | ||
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) | ||
@available(macOS, unavailable) | ||
public var textInputAutocapitalization: TextInputAutocapitalization { | ||
self["TextInputAutocapitalizationKey", default: TextInputAutocapitalization.never] | ||
} | ||
#endif | ||
|
||
#if os(iOS) || os(tvOS) | ||
/// The value for the ``.textContentType(_)`` modifier | ||
@available(iOS 13.0, tvOS 13.0, *) | ||
@available(macOS, unavailable) | ||
@available(watchOS, unavailable) | ||
public var textContentType: UITextContentType? { | ||
self["TextContentTypeKey"] | ||
} | ||
#endif | ||
} | ||
|
||
// MARK: - Previews | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
struct EnvironmentValues_Previews: PreviewProvider { | ||
static var previews: some View { | ||
VStack { | ||
EnvironmentValuePreview(keyPath: \.labelsHidden) { | ||
TextField("Label", text: .constant("")) | ||
.fixedSize() | ||
} content: { isHidden in | ||
Text(isHidden.description) | ||
} | ||
.labelsHidden() | ||
|
||
EnvironmentValuePreview(keyPath: \.foregroundStyle) { | ||
Text("Hello, World") | ||
} content: { foregroundStyle in | ||
Circle() | ||
.fill(foregroundStyle) | ||
.fixedSize() | ||
} | ||
.foregroundStyle(.red) | ||
|
||
EnvironmentValuePreview(keyPath: \.tint) { | ||
Button("Action") { } | ||
} content: { foregroundStyle in | ||
Circle() | ||
.fill(foregroundStyle) | ||
.fixedSize() | ||
} | ||
.tint(.purple) | ||
|
||
EnvironmentValuePreview(keyPath: \.minimumScaleFactor) { | ||
Text("Hello, World") | ||
.frame(width: 50) | ||
} content: { minimumScaleFactor in | ||
Text(minimumScaleFactor.description) | ||
} | ||
.lineLimit(1) | ||
.minimumScaleFactor(0.5) | ||
} | ||
} | ||
|
||
struct EnvironmentValuePreview<Value, Source: View, Content: View>: View { | ||
|
||
var keyPath: KeyPath<EnvironmentValues, Value> | ||
var source: Source | ||
var content: (Value) -> Content | ||
|
||
init( | ||
keyPath: KeyPath<EnvironmentValues, Value>, | ||
@ViewBuilder source: () -> Source, | ||
@ViewBuilder content: @escaping (Value) -> Content | ||
) { | ||
self.keyPath = keyPath | ||
self.source = source() | ||
self.content = content | ||
} | ||
|
||
var body: some View { | ||
HStack { | ||
source | ||
|
||
Divider() | ||
.fixedSize() | ||
|
||
EnvironmentValueReader(keyPath) { value in | ||
content(value) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
455 changes: 455 additions & 0 deletions
455
Sources/Turbocharger/Sources/Extensions/Font+Extensions.swift
Large diffs are not rendered by default.
Oops, something went wrong.
150 changes: 150 additions & 0 deletions
150
Sources/Turbocharger/Sources/Extensions/Image+Extensions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
// | ||
// Copyright (c) Nathan Tannar | ||
// | ||
|
||
import SwiftUI | ||
|
||
extension Image { | ||
|
||
#if os(macOS) | ||
typealias PlatformRepresentable = NSImage | ||
#elseif os(iOS) || os(tvOS) || os(watchOS) | ||
typealias PlatformRepresentable = UIImage | ||
#endif | ||
|
||
#if os(iOS) || os(tvOS) || os(watchOS) | ||
public func toUIImage( | ||
in environment: EnvironmentValues = EnvironmentValues() | ||
) -> UIImage? { | ||
toPlatformValue(in: environment) | ||
} | ||
#endif | ||
|
||
#if os(macOS) | ||
public func toNSImage( | ||
in environment: EnvironmentValues = EnvironmentValues() | ||
) -> NSImage? { | ||
toPlatformValue(in: environment) | ||
} | ||
#endif | ||
|
||
fileprivate func toPlatformValue( | ||
in environment: EnvironmentValues | ||
) -> PlatformRepresentable? { | ||
ImageProvider(for: self)?.resolved(in: environment) | ||
} | ||
} | ||
|
||
private enum ImageProvider { | ||
|
||
case system(String) | ||
case named(String, Bundle?) | ||
case cg(CGImage, CGFloat, Image.Orientation) | ||
case image(Image.PlatformRepresentable) | ||
|
||
init?(for image: Image) { | ||
guard let base = Mirror(reflecting: image).descendant("provider", "base") else { | ||
return nil | ||
} | ||
|
||
let className = String(describing: type(of: base)) | ||
let mirror = Mirror(reflecting: base) | ||
switch className { | ||
case "NamedImageProvider": | ||
guard let name = mirror.descendant("name") as? String else { | ||
return nil | ||
} | ||
if let location = mirror.descendant("location") { | ||
if String(describing: location) == "system" { | ||
self = .system(name) | ||
} else { | ||
let bundle = mirror.descendant("location", "bundle") | ||
self = .named(name, bundle as? Bundle) | ||
} | ||
} else { | ||
self = .named(name, nil) | ||
} | ||
|
||
case "\(Image.PlatformRepresentable.self)": | ||
guard let image = base as? Image.PlatformRepresentable else { | ||
return nil | ||
} | ||
self = .image(image) | ||
|
||
case "CGImageProvider": | ||
guard | ||
let image = mirror.descendant("image"), | ||
let scale = mirror.descendant("scale") as? CGFloat, | ||
let orientation = mirror.descendant("orientation") as? Image.Orientation | ||
else { | ||
return nil | ||
} | ||
self = .cg(image as! CGImage, scale, orientation) | ||
|
||
default: | ||
return nil | ||
} | ||
} | ||
|
||
func resolved(in environment: EnvironmentValues) -> Image.PlatformRepresentable? { | ||
switch self { | ||
case .system(let name): | ||
#if os(iOS) || os(tvOS) || os(watchOS) | ||
let scale: UIImage.SymbolScale = { | ||
guard let scale = environment.imageScale else { return .unspecified } | ||
switch scale { | ||
case .small: return .small | ||
case .medium: return .medium | ||
case .large: return .large | ||
@unknown default: | ||
return .unspecified | ||
} | ||
}() | ||
let config = environment.font?.toUIFont().map { | ||
UIImage.SymbolConfiguration( | ||
font: $0, | ||
scale: scale | ||
) | ||
} ?? UIImage.SymbolConfiguration(scale: scale) | ||
return UIImage( | ||
systemName: name, | ||
withConfiguration: config | ||
) | ||
#elseif os(macOS) | ||
if #available(macOS 11.0, *) { | ||
return NSImage(systemSymbolName: name, accessibilityDescription: nil) | ||
} | ||
return nil | ||
#endif | ||
case let .named(name, bundle): | ||
#if os(iOS) || os(tvOS) || os(watchOS) | ||
return UIImage(named: name, in: bundle, with: nil) | ||
#elseif os(macOS) | ||
if #available(macOS 14.0, *), let bundle { | ||
return NSImage(resource: ImageResource(name: name, bundle: bundle)) | ||
} | ||
return NSImage(named: name) | ||
#endif | ||
case let .image(image): | ||
return image | ||
case let .cg(image, scale, orientation): | ||
#if os(iOS) || os(tvOS) || os(watchOS) | ||
let orientation: UIImage.Orientation = { | ||
switch orientation { | ||
case .down: return .down | ||
case .downMirrored: return .downMirrored | ||
case .left: return .left | ||
case .leftMirrored: return .leftMirrored | ||
case .right: return .right | ||
case .rightMirrored: return .rightMirrored | ||
case .up: return .up | ||
case .upMirrored: return .upMirrored | ||
} | ||
}() | ||
return UIImage(cgImage: image, scale: scale, orientation: orientation) | ||
#elseif os(macOS) | ||
return NSImage(cgImage: image, size: .zero) | ||
#endif | ||
} | ||
} | ||
} |
224 changes: 224 additions & 0 deletions
224
Sources/Turbocharger/Sources/Extensions/Text+Extensions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
// | ||
// Copyright (c) Nathan Tannar | ||
// | ||
|
||
import SwiftUI | ||
|
||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) | ||
extension Text { | ||
|
||
/// Transforms the `Text` to a `String`, using the environment to resolve localized | ||
/// string keys if necessary. | ||
@inlinable | ||
public func resolve(in environment: EnvironmentValues) -> String { | ||
_resolveText(in: environment) | ||
} | ||
|
||
/// Transforms the `Text` to a `AttributedString`, using the environment to resolve localized | ||
/// string keys if necessary. | ||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
public func resolveAttributed(in environment: EnvironmentValues) -> AttributedString { | ||
guard MemoryLayout<Text>.size == MemoryLayout<Text.Layout>.size else { | ||
return AttributedString(resolve(in: environment)) | ||
} | ||
return _resolveAttributed(in: environment) | ||
} | ||
} | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
extension Text { | ||
private enum Storage { | ||
case verbatim(String) | ||
case anyTextStorage(AnyObject) | ||
} | ||
|
||
private enum Modifier { | ||
case color(Color?) | ||
case font(Font?) | ||
case italic | ||
case weight(Font.Weight?) | ||
case kerning(CGFloat) | ||
case tracking(CGFloat) | ||
case baseline(CGFloat) | ||
case rounded | ||
case anyTextModifier(AnyObject) | ||
} | ||
|
||
private struct Environment { | ||
var font: Font? | ||
var fontWeight: Font.Weight? | ||
var fontWidth: CGFloat? | ||
var foregroundColor: Color? | ||
var underlineStyle: Text.LineStyle? | ||
var strikethroughStyle: Text.LineStyle? | ||
var kerning: CGFloat? | ||
var tracking: CGFloat? | ||
var baselineOffset: CGFloat? | ||
var isItalic: Bool = false | ||
var isBold: Bool = false | ||
var isMonospaced: Bool = false | ||
var environment: EnvironmentValues | ||
|
||
init(environment: EnvironmentValues) { | ||
self.environment = environment | ||
} | ||
|
||
var attributes: AttributeContainer { | ||
var attributes = AttributeContainer() | ||
attributes.swiftUI.font = { | ||
var font = font ?? environment.font | ||
if let fontWeight { | ||
font = font?.weight(fontWeight) | ||
} | ||
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *), let fontWidth { | ||
font = font?.width(.init(fontWidth)) | ||
} | ||
if isItalic { | ||
font = font?.italic() | ||
} | ||
if isBold { | ||
font = font?.bold() | ||
} | ||
if isMonospaced { | ||
font = font?.monospaced() | ||
} | ||
return font | ||
}() | ||
attributes.swiftUI.foregroundColor = foregroundColor ?? environment.foregroundColor | ||
attributes.swiftUI.underlineStyle = underlineStyle ?? environment.underlineStyle | ||
attributes.swiftUI.strikethroughStyle = strikethroughStyle ?? environment.strikethroughStyle | ||
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { | ||
attributes.kern = kerning ?? environment.kerning | ||
attributes.tracking = tracking ?? environment.tracking | ||
attributes.baselineOffset = baselineOffset ?? environment.baselineOffset | ||
} else { | ||
attributes.swiftUI.kern = kerning | ||
attributes.swiftUI.tracking = tracking | ||
attributes.swiftUI.baselineOffset = baselineOffset | ||
} | ||
return attributes | ||
} | ||
} | ||
|
||
private struct Layout { | ||
var storage: Storage | ||
var modifiers: [Modifier] | ||
} | ||
|
||
private var layout: Text.Layout { | ||
unsafeBitCast(self, to: Text.Layout.self) | ||
} | ||
|
||
func _resolveAttributed(in environment: EnvironmentValues) -> AttributedString { | ||
let environment = Environment(environment: environment) | ||
return _resolveAttributed(in: environment) | ||
} | ||
|
||
private func _resolveAttributed(in environment: Environment) -> AttributedString { | ||
var environment = environment | ||
for modifier in layout.modifiers.reversed() { | ||
switch modifier { | ||
case .color(let color): | ||
environment.foregroundColor = color | ||
case .font(let font): | ||
environment.font = font | ||
case .italic: | ||
environment.isItalic = true | ||
case .weight(let weight): | ||
environment.fontWeight = weight | ||
case .kerning(let kerning): | ||
environment.kerning = kerning | ||
case .tracking(let tracking): | ||
environment.tracking = tracking | ||
case .baseline(let baseline): | ||
environment.baselineOffset = baseline | ||
case .rounded: | ||
break | ||
case .anyTextModifier(let modifier): | ||
let mirror = Mirror(reflecting: modifier) | ||
let className = String(describing: type(of: modifier)) | ||
switch className { | ||
case "TextWidthModifier": | ||
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *), | ||
let width = mirror.descendant("width") as? CGFloat | ||
{ | ||
environment.fontWidth = width | ||
} | ||
case "TextDesignModifier": | ||
if let design = mirror.descendant("design") as? Font.Design { | ||
environment.isMonospaced = design == .monospaced | ||
} | ||
case "UnderlineTextModifier": | ||
if let lineStyle = mirror.descendant("lineStyle") as? Text.LineStyle { | ||
environment.underlineStyle = lineStyle | ||
} | ||
case "BoldTextModifier": | ||
let isActive = (mirror.descendant("isActive") as? Bool) ?? true | ||
environment.isBold = isActive | ||
default: | ||
break | ||
} | ||
} | ||
} | ||
switch layout.storage { | ||
case .verbatim(let text): | ||
return AttributedString( | ||
text, | ||
attributes: environment.attributes | ||
) | ||
case .anyTextStorage(let storage): | ||
return resolve(storage: storage, environment: environment) | ||
} | ||
} | ||
|
||
private func resolve( | ||
storage: Any, | ||
environment: Environment | ||
) -> AttributedString { | ||
let className = String(describing: type(of: storage)) | ||
switch className { | ||
case "ConcatenatedTextStorage": | ||
let mirror = Mirror(reflecting: storage) | ||
guard | ||
let first = mirror.descendant("first") as? Text, | ||
let second = mirror.descendant("second") as? Text | ||
else { | ||
fallthrough | ||
} | ||
return first._resolveAttributed(in: environment) + second._resolveAttributed(in: environment) | ||
|
||
case "AttachmentTextStorage": | ||
guard let image = Mirror(reflecting: storage).descendant("image") as? Image else { | ||
fallthrough | ||
} | ||
var attributedString = AttributedString(stringLiteral: " ") | ||
#if os(iOS) || os(tvOS) || os(watchOS) | ||
if let image = image.toUIImage() { | ||
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *), | ||
let baselineOffset = environment.baselineOffset, | ||
baselineOffset != 0 | ||
{ | ||
attributedString.attachment = NSTextAttachment( | ||
image: image.withBaselineOffset(fromBottom: baselineOffset) | ||
) | ||
} else { | ||
attributedString.attachment = NSTextAttachment( | ||
image: image | ||
) | ||
} | ||
} | ||
#endif | ||
return attributedString | ||
|
||
default: | ||
return AttributedString( | ||
resolve(in: environment.environment), | ||
attributes: environment.attributes | ||
) | ||
} | ||
} | ||
} | ||
|
||
#if os(iOS) || os(tvOS) || os(watchOS) | ||
extension NSTextAttachment: @unchecked Sendable { } | ||
#endif |
File renamed without changes.
Large diffs are not rendered by default.
Oops, something went wrong.
136 changes: 136 additions & 0 deletions
136
Sources/Turbocharger/Sources/View/CollectionViewRepresentable.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
// | ||
// Copyright (c) Nathan Tannar | ||
// | ||
|
||
import SwiftUI | ||
import Engine | ||
|
||
@available(iOS 14.0, *) | ||
@available(macOS, unavailable) | ||
@available(tvOS, unavailable) | ||
@available(watchOS, unavailable) | ||
public protocol CollectionViewLayout: DynamicProperty { | ||
|
||
#if os(iOS) | ||
associatedtype UICollectionViewType: UICollectionView | ||
|
||
func makeUICollectionView( | ||
context: Context, | ||
options: CollectionViewLayoutOptions | ||
) -> UICollectionViewType | ||
|
||
func updateUICollectionView( | ||
_ collectionView: UICollectionViewType, | ||
context: Context | ||
) | ||
|
||
func updateUICollectionViewCell( | ||
_ collectionView: UICollectionViewType, | ||
cell: UICollectionViewCell, | ||
kind: HostingConfigurationKind, | ||
indexPath: IndexPath | ||
) | ||
#endif | ||
|
||
typealias Context = CollectionViewLayoutContext | ||
|
||
} | ||
|
||
#if os(iOS) | ||
@available(iOS 14.0, *) | ||
extension CollectionViewLayout { | ||
public func updateUICollectionViewCell( | ||
_ collectionView: UICollectionViewType, | ||
cell: UICollectionViewCell, | ||
kind: HostingConfigurationKind, | ||
indexPath: IndexPath | ||
) { } | ||
} | ||
#endif | ||
|
||
@available(iOS 14.0, *) | ||
@available(macOS, unavailable) | ||
@available(tvOS, unavailable) | ||
@available(watchOS, unavailable) | ||
public struct CollectionViewLayoutContext { | ||
public var environment: EnvironmentValues | ||
public var transaction: Transaction | ||
} | ||
|
||
@available(iOS 14.0, *) | ||
@available(macOS, unavailable) | ||
@available(tvOS, unavailable) | ||
@available(watchOS, unavailable) | ||
public struct CollectionViewLayoutOptions: OptionSet { | ||
public var rawValue: UInt8 | ||
public init(rawValue: UInt8) { | ||
self.rawValue = rawValue | ||
} | ||
|
||
/// The `UICollectionViewLayout` should include a header | ||
public static let header = CollectionViewLayoutOptions(rawValue: 1 << 0) | ||
|
||
/// The `UICollectionViewLayout` should include a footer | ||
public static let footer = CollectionViewLayoutOptions(rawValue: 1 << 1) | ||
} | ||
|
||
@available(iOS 14.0, *) | ||
@available(macOS, unavailable) | ||
@available(tvOS, unavailable) | ||
@available(watchOS, unavailable) | ||
struct CollectionViewLayoutOptionsKey: EnvironmentKey { | ||
static let defaultValue = CollectionViewLayoutOptions() | ||
} | ||
|
||
@available(iOS 14.0, *) | ||
@available(macOS, unavailable) | ||
@available(tvOS, unavailable) | ||
@available(watchOS, unavailable) | ||
extension EnvironmentValues { | ||
public var collectionViewLayoutOptions: CollectionViewLayoutOptions { | ||
get { self[CollectionViewLayoutOptionsKey.self] } | ||
set { self[CollectionViewLayoutOptionsKey.self] = newValue } | ||
} | ||
} | ||
|
||
@available(iOS 14.0, *) | ||
@available(macOS, unavailable) | ||
@available(tvOS, unavailable) | ||
@available(watchOS, unavailable) | ||
@frozen | ||
public struct CollectionViewListLayout: CollectionViewLayout { | ||
|
||
@inlinable | ||
public init() { } | ||
|
||
#if os(iOS) | ||
public func makeUICollectionView( | ||
context: Context, | ||
options: CollectionViewLayoutOptions | ||
) -> UICollectionView { | ||
var configuration = UICollectionLayoutListConfiguration(appearance: .plain) | ||
configuration.headerMode = options.contains(.header) ? .supplementary : .none | ||
configuration.footerMode = options.contains(.footer) ? .supplementary : .none | ||
configuration.showsSeparators = false | ||
configuration.backgroundColor = .clear | ||
let layout = UICollectionViewCompositionalLayout.list(using: configuration) | ||
let uiCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) | ||
uiCollectionView.clipsToBounds = false | ||
uiCollectionView.keyboardDismissMode = .interactive | ||
return uiCollectionView | ||
} | ||
|
||
public func updateUICollectionView( | ||
_ collectionView: UICollectionView, | ||
context: Context | ||
) { } | ||
#endif | ||
} | ||
|
||
@available(iOS 14.0, *) | ||
@available(macOS, unavailable) | ||
@available(tvOS, unavailable) | ||
@available(watchOS, unavailable) | ||
extension CollectionViewLayout where Self == CollectionViewListLayout { | ||
public static var list: CollectionViewListLayout { .init() } | ||
} |
57 changes: 57 additions & 0 deletions
57
Sources/Turbocharger/Sources/View/EnvironmentValueReader.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// | ||
// Copyright (c) Nathan Tannar | ||
// | ||
|
||
import SwiftUI | ||
|
||
/// A view that reads an environment value to derive its content | ||
@frozen | ||
public struct EnvironmentValueReader<Value, Content: View>: View { | ||
|
||
@usableFromInline | ||
var value: Environment<Value> | ||
|
||
@usableFromInline | ||
var content: (Value) -> Content | ||
|
||
@inlinable | ||
public init( | ||
_ keyPath: KeyPath<EnvironmentValues, Value>, | ||
@ViewBuilder content: @escaping (Value) -> Content | ||
) { | ||
self.value = Environment(keyPath) | ||
self.content = content | ||
} | ||
|
||
public var body: some View { | ||
content(value.wrappedValue) | ||
} | ||
} | ||
|
||
// MARK: - Preview | ||
|
||
fileprivate extension EnvironmentValues { | ||
var testFlag: Bool { | ||
get { self[EnvironmentValueReader_Preview.TestFlagKey.self] } | ||
set { self[EnvironmentValueReader_Preview.TestFlagKey.self] = newValue } | ||
} | ||
} | ||
|
||
struct EnvironmentValueReader_Preview: PreviewProvider { | ||
enum TestFlagKey: EnvironmentKey { | ||
static let defaultValue: Bool = false | ||
} | ||
|
||
static var previews: some View { | ||
VStack { | ||
EnvironmentValueReader(\.testFlag) { flag in | ||
Text(flag.description) | ||
} | ||
|
||
EnvironmentValueReader(\.testFlag) { flag in | ||
Text(flag.description) | ||
} | ||
.environment(\.testFlag, true) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters