|
3 | 3 | // OpenSwiftUICore |
4 | 4 | // |
5 | 5 | // Audited for 6.5.4 |
6 | | -// Status: WIP |
| 6 | +// Status: Complete |
7 | 7 | // ID: 7267202B6A40C9B73733978AB256B462 (SwiftUICore) |
8 | 8 |
|
9 | 9 | public import Foundation |
10 | 10 |
|
| 11 | +// MARK: - Text + Formatter |
| 12 | + |
| 13 | +@available(OpenSwiftUI_v2_0, *) |
| 14 | +extension Text { |
| 15 | + /// Creates a text view that displays the formatted representation |
| 16 | + /// of a reference-convertible value. |
| 17 | + /// |
| 18 | + /// Use this initializer to create a text view that formats `subject` |
| 19 | + /// using `formatter`. |
| 20 | + /// - Parameters: |
| 21 | + /// - subject: A |
| 22 | + /// [ReferenceConvertible](https://developer.apple.com/documentation/foundation/referenceconvertible) |
| 23 | + /// instance compatible with `formatter`. |
| 24 | + /// - formatter: A |
| 25 | + /// [Formatter](https://developer.apple.com/documentation/foundation/formatter) |
| 26 | + /// capable of converting `subject` into a string representation. |
| 27 | + public init<Subject>( |
| 28 | + _ subject: Subject, |
| 29 | + formatter: Formatter |
| 30 | + ) where Subject: ReferenceConvertible { |
| 31 | + self.init( |
| 32 | + anyTextStorage: FormatterTextStorage( |
| 33 | + object: subject as! Subject.ReferenceType, |
| 34 | + formatter: formatter |
| 35 | + ) |
| 36 | + ) |
| 37 | + } |
| 38 | + |
| 39 | + /// Creates a text view that displays the formatted representation |
| 40 | + /// of a Foundation object. |
| 41 | + /// |
| 42 | + /// Use this initializer to create a text view that formats `subject` |
| 43 | + /// using `formatter`. |
| 44 | + /// - Parameters: |
| 45 | + /// - subject: An |
| 46 | + /// [NSObject](https://developer.apple.com/documentation/objectivec/nsobject) |
| 47 | + /// instance compatible with `formatter`. |
| 48 | + /// - formatter: A |
| 49 | + /// [Formatter](https://developer.apple.com/documentation/foundation/formatter) |
| 50 | + /// capable of converting `subject` into a string representation. |
| 51 | + public init<Subject>( |
| 52 | + _ subject: Subject, |
| 53 | + formatter: Formatter |
| 54 | + ) where Subject: NSObject { |
| 55 | + self.init( |
| 56 | + anyTextStorage: FormatterTextStorage( |
| 57 | + object: subject, |
| 58 | + formatter: formatter |
| 59 | + ) |
| 60 | + ) |
| 61 | + } |
| 62 | +} |
| 63 | + |
| 64 | +private final class FormatterTextStorage: AnyTextStorage, @unchecked Sendable { |
| 65 | + let object: NSObject |
| 66 | + let formatter: Formatter |
| 67 | + |
| 68 | + init(object: NSObject, formatter: Formatter) { |
| 69 | + self.object = object |
| 70 | + self.formatter = formatter |
| 71 | + } |
| 72 | + |
| 73 | + override func resolve<T>( |
| 74 | + into result: inout T, |
| 75 | + in environment: EnvironmentValues, |
| 76 | + with options: Text.ResolveOptions |
| 77 | + ) where T: ResolvedTextContainer { |
| 78 | + (formatter as? EnvironmentConfigurableFormatter)?.configure(in: environment) |
| 79 | + guard let string = formatter.string(for: object) else { |
| 80 | + return |
| 81 | + } |
| 82 | + result.append( |
| 83 | + string, |
| 84 | + in: environment, |
| 85 | + with: options |
| 86 | + ) |
| 87 | + } |
| 88 | + |
| 89 | + override func isEqual(to other: AnyTextStorage) -> Bool { |
| 90 | + guard let other = other as? FormatterTextStorage else { |
| 91 | + return false |
| 92 | + } |
| 93 | + return object == other.object && formatter == other.formatter |
| 94 | + } |
| 95 | + |
| 96 | + override func isStyled(options: Text.ResolveOptions) -> Bool { |
| 97 | + false |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +// MARK: - Text + FormatStyle |
| 102 | + |
11 | 103 | @available(OpenSwiftUI_v3_0, *) |
12 | 104 | extension Text { |
| 105 | + /// Creates a text view that displays the formatted representation |
| 106 | + /// of a nonstring type supported by a corresponding format style. |
| 107 | + /// |
| 108 | + /// Use this initializer to create a text view backed by a nonstring |
| 109 | + /// value, using a |
| 110 | + /// [FormatStyle](https://developer.apple.com/documentation/foundation/formatstyle) |
| 111 | + /// to convert the type to a string representation. Any changes to the value |
| 112 | + /// update the string displayed by the text view. |
| 113 | + /// |
| 114 | + /// In the following example, three ``Text`` views present a date with |
| 115 | + /// different combinations of date and time fields, by using different |
| 116 | + /// [Date.FormatStyle](https://developer.apple.com/documentation/foundation/date/formatstyle) |
| 117 | + /// options. |
| 118 | + /// |
| 119 | + /// @State private var myDate = Date() |
| 120 | + /// var body: some View { |
| 121 | + /// VStack { |
| 122 | + /// Text(myDate, format: Date.FormatStyle(date: .numeric, time: .omitted)) |
| 123 | + /// Text(myDate, format: Date.FormatStyle(date: .complete, time: .complete)) |
| 124 | + /// Text(myDate, format: Date.FormatStyle().hour(.defaultDigitsNoAMPM).minute()) |
| 125 | + /// } |
| 126 | + /// } |
| 127 | + /// |
| 128 | + ///  |
| 131 | + /// |
| 132 | + /// - Parameters: |
| 133 | + /// - input: The underlying value to display. |
| 134 | + /// - format: A format style of type `F` to convert the underlying value |
| 135 | + /// of type `F.FormatInput` to a string representation. |
13 | 136 | public init<F>( |
14 | 137 | _ input: F.FormatInput, |
15 | 138 | format: F |
16 | 139 | ) where F: FormatStyle, F.FormatInput: Equatable, F.FormatOutput == String { |
17 | | - _openSwiftUIUnimplementedFailure() |
| 140 | + self.init(anyTextStorage: FormatStyleStorage(input: input, format: format)) |
18 | 141 | } |
19 | 142 | } |
20 | 143 |
|
21 | 144 | @available(OpenSwiftUI_v6_0, *) |
22 | 145 | extension Text { |
| 146 | + /// Creates a text view that displays the formatted representation |
| 147 | + /// of a nonstring type supported by a corresponding format style. |
| 148 | + /// |
| 149 | + /// Use this initializer to create a text view backed by a nonstring |
| 150 | + /// value, using a |
| 151 | + /// [FormatStyle](https://developer.apple.com/documentation/foundation/formatstyle) |
| 152 | + /// to convert the type to an attributed string representation. Any changes to the value |
| 153 | + /// update the string displayed by the text view. |
| 154 | + /// |
| 155 | + /// In the following example, three ``Text`` views present a date with |
| 156 | + /// different combinations of date and time fields, by using different |
| 157 | + /// [Date.FormatStyle](https://developer.apple.com/documentation/foundation/date/formatstyle) |
| 158 | + /// options. |
| 159 | + /// |
| 160 | + /// @State private var myDate = Date() |
| 161 | + /// var body: some View { |
| 162 | + /// VStack { |
| 163 | + /// Text(myDate, format: Date.FormatStyle(date: .numeric, time: .omitted).attributedStyle) |
| 164 | + /// Text(myDate, format: Date.FormatStyle(date: .complete, time: .complete).attributedStyle) |
| 165 | + /// Text(myDate, format: Date.FormatStyle().hour(.defaultDigitsNoAMPM).minute().attributedStyle) |
| 166 | + /// } |
| 167 | + /// } |
| 168 | + /// |
| 169 | + ///  |
| 172 | + /// |
| 173 | + /// - Parameters: |
| 174 | + /// - input: The underlying value to display. |
| 175 | + /// - format: A format style of type `F` to convert the underlying value |
| 176 | + /// of type `F.FormatInput` to an attributed string representation. |
23 | 177 | public init<F>( |
24 | 178 | _ input: F.FormatInput, |
25 | 179 | format: F |
26 | 180 | ) where F: FormatStyle, F.FormatInput: Equatable, F.FormatOutput == AttributedString { |
27 | | - _openSwiftUIUnimplementedFailure() |
| 181 | + self.init(anyTextStorage: FormatStyleStorage(input: input, format: format)) |
| 182 | + } |
| 183 | +} |
| 184 | + |
| 185 | +private class FormatStyleBoxBase { |
| 186 | + func isEqual(to other: FormatStyleBoxBase) -> Bool { |
| 187 | + _openSwiftUIBaseClassAbstractMethod() |
| 188 | + } |
| 189 | + |
| 190 | + func format( |
| 191 | + in environment: EnvironmentValues, |
| 192 | + idiom: AnyInterfaceIdiom? |
| 193 | + ) -> (output: AttributedString, exact: Bool) { |
| 194 | + _openSwiftUIBaseClassAbstractMethod() |
| 195 | + } |
| 196 | +} |
| 197 | + |
| 198 | +private final class FormatStyleBox<F>: FormatStyleBoxBase where |
| 199 | + F: FormatStyle, |
| 200 | + F.FormatInput: Equatable, |
| 201 | + F.FormatOutput: AttributedStringConvertible |
| 202 | +{ |
| 203 | + let input: F.FormatInput |
| 204 | + let format: F |
| 205 | + |
| 206 | + init(input: F.FormatInput, format: F) { |
| 207 | + self.input = input |
| 208 | + self.format = format |
| 209 | + } |
| 210 | + |
| 211 | + override func isEqual(to other: FormatStyleBoxBase) -> Bool { |
| 212 | + guard let other = other as? FormatStyleBox<F> else { |
| 213 | + return false |
| 214 | + } |
| 215 | + return input == other.input && format == other.format |
| 216 | + } |
| 217 | + |
| 218 | + override func format( |
| 219 | + in environment: EnvironmentValues, |
| 220 | + idiom: AnyInterfaceIdiom? |
| 221 | + ) -> (output: AttributedString, exact: Bool) { |
| 222 | + var resolvedFormat = format.locale(environment.locale) |
| 223 | + if isLinkedOnOrAfter(.v6) { |
| 224 | + resolvedFormat = resolvedFormat |
| 225 | + .calendar(environment.calendar) |
| 226 | + .timeZone(environment.timeZone) |
| 227 | + } |
| 228 | + if let dependentFormat = resolvedFormat as? any InterfaceIdiomDependentFormatStyle { |
| 229 | + let resolvedIdiom: AnyInterfaceIdiom |
| 230 | + if let idiom { |
| 231 | + resolvedIdiom = idiom |
| 232 | + } else { |
| 233 | + Log.internalWarning("FormatStyleStorage was resolved without idiom!") |
| 234 | + resolvedIdiom = _GraphInputs.defaultInterfaceIdiom |
| 235 | + } |
| 236 | + resolvedFormat = dependentFormat.interfaceIdiom(resolvedIdiom) as! F |
| 237 | + } |
| 238 | + if let dependentFormat = resolvedFormat as? any TextAlignmentDependentFormatStyle { |
| 239 | + resolvedFormat = dependentFormat.textAlignment(environment.multilineTextAlignment) as! F |
| 240 | + } |
| 241 | + if isLinkedOnOrAfter(.v6), |
| 242 | + let dependentFormat = resolvedFormat as? any CapitalizationContextDependentFormatStyle { |
| 243 | + resolvedFormat = dependentFormat.capitalizationContext(environment.capitalizationContext.resolved) as! F |
| 244 | + } |
| 245 | + let resolved = resolvedFormat.exactSizeVariant(environment.textSizeVariant) |
| 246 | + let output = resolved.style.format(input).attributedString |
| 247 | + return (output, resolved.exact) |
| 248 | + } |
| 249 | +} |
| 250 | + |
| 251 | +private final class FormatStyleStorage: AnyTextStorage, @unchecked Sendable { |
| 252 | + let storage: FormatStyleBoxBase |
| 253 | + |
| 254 | + init<F>( |
| 255 | + input: F.FormatInput, |
| 256 | + format: F |
| 257 | + ) where |
| 258 | + F: FormatStyle, |
| 259 | + F.FormatInput: Equatable, |
| 260 | + F.FormatOutput: AttributedStringConvertible |
| 261 | + { |
| 262 | + storage = FormatStyleBox(input: input, format: format) |
| 263 | + } |
| 264 | + |
| 265 | + override func resolve<T>( |
| 266 | + into result: inout T, |
| 267 | + in environment: EnvironmentValues, |
| 268 | + with options: Text.ResolveOptions |
| 269 | + ) where T: ResolvedTextContainer { |
| 270 | + let resolved = storage.format(in: environment, idiom: result.idiom) |
| 271 | + result.append( |
| 272 | + NSAttributedString(resolved.output), |
| 273 | + in: environment, |
| 274 | + with: options, |
| 275 | + isUniqueSizeVariant: resolved.exact |
| 276 | + ) |
| 277 | + } |
| 278 | + |
| 279 | + override func isEqual(to other: AnyTextStorage) -> Bool { |
| 280 | + guard let other = other as? FormatStyleStorage else { |
| 281 | + return false |
| 282 | + } |
| 283 | + return storage.isEqual(to: other.storage) |
| 284 | + } |
| 285 | + |
| 286 | + override func isStyled(options: Text.ResolveOptions) -> Bool { |
| 287 | + false |
| 288 | + } |
| 289 | +} |
| 290 | + |
| 291 | +#if !canImport(Darwin) |
| 292 | +extension NSAttributedString { |
| 293 | + fileprivate convenience init(_ attributedString: AttributedString) { |
| 294 | + self.init(string: String(attributedString)) |
28 | 295 | } |
29 | 296 | } |
| 297 | +#endif |
0 commit comments