├── .spi.yml ├── .gitignore ├── Sources └── SwiftUIBackports │ ├── Shared │ ├── PhotosPicker │ │ ├── Core │ │ │ ├── PHObject+Identifiable.swift │ │ │ ├── MediaResults.swift │ │ │ └── PHFetchOptions.swift │ │ ├── PickerResult.swift │ │ ├── PhotosPickerSelectionBehavior.swift │ │ ├── Fetch │ │ │ ├── FetchAsset.swift │ │ │ ├── FetchAssetCollection.swift │ │ │ └── FetchCollectionList.swift │ │ ├── UI │ │ │ └── PhotosPickerView.swift │ │ ├── PhotosPickerItem.swift │ │ └── PickerFilter.swift │ ├── Container │ │ ├── VariadicView.swift │ │ ├── Subview.swift │ │ ├── ForEach+Subviews.swift │ │ └── Group+Subviews.swift │ ├── ShareLink │ │ ├── SharePreview.swift │ │ ├── ShareLink.swift │ │ ├── Single Item │ │ │ ├── Item+Preview.swift │ │ │ ├── Item+Label+Preview.swift │ │ │ ├── Item.swift │ │ │ └── Item+Label.swift │ │ ├── Multiple Items │ │ │ ├── Items+Preview.swift │ │ │ ├── Items+Label+Preview.swift │ │ │ ├── Items.swift │ │ │ └── Items+Label.swift │ │ ├── DefaultShareLinkLabel.swift │ │ └── Transferable.swift │ ├── Label │ │ ├── LabelConfiguration.swift │ │ ├── Styles │ │ │ ├── TitleOnlyLabelStyle.swift │ │ │ ├── IconOnlyLabelStyle.swift │ │ │ ├── DefaultLabelStyle.swift │ │ │ └── TitleAndIconLabelStyle.swift │ │ └── LabelStyle.swift │ ├── ImageRenderer │ │ ├── ProposedViewSize.swift │ │ └── Renderer.swift │ ├── Toolbar │ │ ├── ToolbarBackground+Environment.swift │ │ ├── ToolbarBackground.swift │ │ └── ToolbarBackgroundModifier.swift │ ├── LabeledContent │ │ ├── Styles │ │ │ └── AutomaticLabeledContentStyle.swift │ │ ├── LabeledContentStyleConfiguration.swift │ │ └── LabeledContentStyle.swift │ ├── Transition │ │ └── PushTransition.swift │ ├── Navigation │ │ └── NavigationTitle.swift │ ├── Visibility │ │ └── Visibility.swift │ ├── ProgressView │ │ ├── Styles │ │ │ ├── DefaultProgressViewStyle.swift │ │ │ ├── CircularProgressViewStyle.swift │ │ │ └── LinearProgressViewStyle.swift │ │ ├── ProgressViewConfiguration.swift │ │ └── ProgressViewStyle.swift │ ├── Section │ │ └── Section.swift │ ├── DynamicType │ │ └── DynamicType+Environment.swift │ ├── SensoryFeedback │ │ └── SensoryFeedback+ViewModifier.swift │ ├── Quicklook │ │ ├── Quicklook+iOS.swift │ │ ├── Quicklook+macOS.swift │ │ └── Quicklook.swift │ ├── System Overlays │ │ └── SystemOverlays.swift │ ├── RequestReview │ │ └── RequestReview.swift │ ├── OpenURL │ │ └── Safari.swift │ └── GeometryChange │ │ └── GeometryChange.swift │ ├── Internal │ ├── UIScene.swift │ ├── String+LocalizationKey.swift │ ├── Environment.swift │ ├── OwningController.swift │ ├── NSItemProvider+Async.swift │ ├── SafeArea.swift │ ├── Platforms.swift │ └── VisualEffects │ │ ├── VisualEffect+macOS.swift │ │ └── VisualEffect+iOS.swift │ ├── Backport.swift │ ├── Resources │ └── PrivacyInfo.xcprivacy │ ├── iOS │ ├── UIHostingConfiguration │ │ ├── UIContentConfiguration.swift │ │ ├── ProposedInsets.swift │ │ ├── ProposedSize.swift │ │ └── Cells │ │ │ ├── UITableViewCell.swift │ │ │ └── UICollectionViewCell.swift │ ├── ScrollView │ │ ├── ScrollIndicatorVisibility.swift │ │ ├── ScrollKeyboardDismiss.swift │ │ ├── ScrollEnabled.swift │ │ ├── Scroll+Environment.swift │ │ ├── ScrollDismissesKeyboardMode.swift │ │ └── ScrollIndicators.swift │ ├── Presentation │ │ ├── CornerRadius.swift │ │ ├── DragIndicator.swift │ │ ├── InteractiveDetent.swift │ │ └── ContentInteraction.swift │ ├── TextEditor │ │ └── TextEditor+Support.swift │ ├── ScaledMetric │ │ └── ScaledMetric.swift │ └── FocusState │ │ ├── FocusState.swift │ │ └── ViewFocused.swift │ └── Deprecations │ ├── Presenatation+Deprecations.swift │ ├── FittingScrollView+Deprecations.swift │ └── FittingGeometryReader+Deprecations.swift ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ ├── SwiftUIBackports.xcscheme │ └── Backports.xcscheme ├── Package.resolved ├── Package.swift └── LICENSE.md /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1.8.2 2 | builder: 3 | configs: 4 | - platform: ios 5 | documentation_targets: [SwiftUIBackports] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/PhotosPicker/Core/PHObject+Identifiable.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import SwiftUI 3 | import PhotosUI 4 | import SwiftBackports 5 | 6 | extension PHObject: @retroactive Identifiable { 7 | public var id: String { localIdentifier } 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Internal/UIScene.swift: -------------------------------------------------------------------------------- 1 | import SwiftBackports 2 | 3 | #if os(iOS) 4 | import UIKit 5 | 6 | internal extension UIApplication { 7 | static var activeScene: UIWindowScene? { 8 | shared.connectedScenes 9 | .first { $0.activationState == .foregroundActive } 10 | as? UIWindowScene 11 | } 12 | } 13 | #endif 14 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Internal/String+LocalizationKey.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | extension String { 5 | internal init?(_ stringKey: LocalizedStringKey) { 6 | guard let key = Mirror(reflecting: stringKey).children 7 | .first(where: { $0.label == "key" })?.value as? String else { 8 | return nil 9 | } 10 | 11 | self = NSLocalizedString(key, comment: "") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Backport.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | @_exported import SwiftBackports 3 | 4 | @MainActor 5 | public extension View { 6 | /// Wraps a SwiftUI `View` that can be extended to provide backport functionality. 7 | var backport: Backport { 8 | .init(self) 9 | } 10 | } 11 | 12 | @MainActor 13 | public extension AnyTransition { 14 | /// Wraps an `AnyTransition` that can be extended to provide backport functionality. 15 | static var backport: Backport { 16 | .init(.identity) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryUserDefaults 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | CA92.1 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Container/VariadicView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension View { 4 | @MainActor 5 | func variadic(@ViewBuilder _ transform: @escaping (_VariadicView.Children) -> R) -> some View { 6 | _VariadicView.Tree(Helper(transform: transform)) { self } 7 | } 8 | } 9 | 10 | @MainActor 11 | struct Helper: _VariadicView.MultiViewRoot { 12 | var transform: (_VariadicView.Children) -> R 13 | 14 | func body(children: _VariadicView.Children) -> some View { 15 | transform(children) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-docc-plugin", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-docc-plugin", 7 | "state" : { 8 | "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6", 9 | "version" : "1.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swiftbackports", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/shaps80/SwiftBackports", 16 | "state" : { 17 | "revision" : "ddca6a237c1ba2291d5a3cc47ec8480ce6e9f805", 18 | "version" : "1.0.3" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Container/Subview.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Backport { 4 | @MainActor 5 | @preconcurrency public struct Subview: View, Identifiable { 6 | public struct ID: Hashable, @unchecked Sendable { 7 | var wrapped: AnyHashable 8 | } 9 | 10 | public let id: ID 11 | private let content: _VariadicView.Children.Element 12 | 13 | internal init(_ content: _VariadicView.Children.Element) { 14 | self.id = .init(wrapped: content.id) 15 | self.content = content 16 | } 17 | 18 | public var body: some View { 19 | content 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/PhotosPicker/PickerResult.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import SwiftUI 3 | import PhotosUI 4 | import SwiftBackports 5 | 6 | @available(iOS, introduced: 13, deprecated: 16) 7 | public extension Backport where Wrapped == Any { 8 | /// A user selected asset from `PHPickerViewController`. 9 | struct PHPickerResult: Equatable, Hashable { 10 | /// Representations of the selected asset. 11 | public let itemProvider: NSItemProvider 12 | 13 | /// The local identifier of the selected asset. 14 | public let assetIdentifier: String? 15 | 16 | internal init(assetIdentifier: String?, itemProvider: NSItemProvider) { 17 | self.assetIdentifier = assetIdentifier 18 | self.itemProvider = itemProvider 19 | } 20 | } 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ShareLink/SharePreview.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | public struct SharePreview { 5 | let title: String 6 | var icon: Icon? 7 | var image: Image? 8 | 9 | private init() { fatalError() } 10 | } 11 | 12 | public extension SharePreview { 13 | init(_ title: S) where Image == Never, Icon == Never { 14 | self.title = title.description 15 | } 16 | 17 | init(_ title: S, icon: Icon) where Icon: View, Image == Never { 18 | self.title = title.description 19 | self.icon = icon 20 | } 21 | 22 | init(_ title: S, image: Image, icon: Icon) where Image: View, Icon: View { 23 | self.title = title.description 24 | self.image = image 25 | self.icon = icon 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Internal/Environment.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | /* 5 | The following code is for debugging purposes only! 6 | */ 7 | 8 | #if DEBUG 9 | extension EnvironmentValues: @retroactive CustomDebugStringConvertible { 10 | public var debugDescription: String { 11 | "\(self)" 12 | .trimmingCharacters(in: .init(["[", "]"])) 13 | .replacingOccurrences(of: "EnvironmentPropertyKey", with: "") 14 | .replacingOccurrences(of: ", ", with: "\n") 15 | } 16 | } 17 | 18 | struct EnvironmentOutputModifier: ViewModifier { 19 | @Environment(\.self) private var environment 20 | 21 | func body(content: Content) -> some View { 22 | content 23 | } 24 | } 25 | extension View { 26 | func printEnvironment() -> some View { 27 | modifier(EnvironmentOutputModifier()) 28 | } 29 | } 30 | #endif 31 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/UIHostingConfiguration/UIContentConfiguration.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | #if os(iOS) || os(tvOS) 4 | /// The requirements for an object that provides the configuration for a content view. 5 | /// 6 | /// This protocol provides a blueprint for a content configuration object, which encompasses 7 | /// default styling and content for a content view. The content configuration encapsulates 8 | /// all of the supported properties and behaviors for content view customization. 9 | /// You use the configuration to create the content view. 10 | @available(iOS, deprecated: 16) 11 | @available(tvOS, deprecated: 16) 12 | @available(macOS, unavailable) 13 | @available(watchOS, unavailable) 14 | public protocol BackportUIContentConfiguration { 15 | /// Initializes and returns a new instance of the content view using this configuration. 16 | func makeContentView() -> UIView 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Label/LabelConfiguration.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 14) 5 | @available(macOS, deprecated: 11) 6 | @available(tvOS, deprecated: 14) 7 | @available(watchOS, deprecated: 7) 8 | extension Backport where Wrapped == Any { 9 | 10 | /// The properties of a label. 11 | public struct LabelStyleConfiguration { 12 | 13 | /// A description of the labeled item. 14 | public internal(set) var title: AnyView 15 | 16 | /// A symbolic representation of the labeled item. 17 | public internal(set) var icon: AnyView 18 | 19 | internal var environment: EnvironmentValues = .init() 20 | 21 | func environment(_ values: EnvironmentValues) -> Self { 22 | var config = self 23 | config.environment = values 24 | return config 25 | } 26 | 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ImageRenderer/ProposedViewSize.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | public struct ProposedViewSize: Equatable, Sendable { 5 | public var width: CGFloat? 6 | public var height: CGFloat? 7 | 8 | public static let zero = Self(width: 0, height: 0) 9 | public static let infinity = Self(width: .infinity, height: .infinity) 10 | public static let unspecified = Self(width: nil, height: nil) 11 | 12 | public init(_ size: CGSize) { 13 | self.width = size.width 14 | self.height = size.height 15 | } 16 | 17 | public init(width: CGFloat?, height: CGFloat?) { 18 | self.width = width 19 | self.height = height 20 | } 21 | 22 | public func replacingUnspecifiedDimensions(by size: CGSize) -> CGSize { 23 | .init( 24 | width: width ?? size.width, 25 | height: height ?? size.height 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Internal/OwningController.swift: -------------------------------------------------------------------------------- 1 | import SwiftBackports 2 | 3 | #if os(iOS) 4 | import UIKit 5 | 6 | public extension UIView { 7 | 8 | var parentController: UIViewController? { 9 | if let responder = self.next as? UIViewController { 10 | return responder 11 | } else if let responder = self.next as? UIView { 12 | return responder.parentController 13 | } else { 14 | return nil 15 | } 16 | } 17 | 18 | } 19 | #endif 20 | 21 | #if os(macOS) 22 | import AppKit 23 | 24 | public extension NSView { 25 | 26 | var parentController: NSViewController? { 27 | if let responder = self.nextResponder as? NSViewController { 28 | return responder 29 | } else if let responder = self.nextResponder as? NSView { 30 | return responder.parentController 31 | } else { 32 | return nil 33 | } 34 | } 35 | 36 | } 37 | #endif 38 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftUIBackports", 8 | platforms: [ 9 | .iOS(.v13), 10 | .tvOS(.v13), 11 | .watchOS(.v6), 12 | .macOS(.v10_15), 13 | ], 14 | products: [ 15 | .library( 16 | name: "SwiftUIBackports", 17 | targets: ["SwiftUIBackports"] 18 | ), 19 | ], 20 | dependencies: [ 21 | .package(url: "https://github.com/shaps80/SwiftBackports", from: "26.0.1"), 22 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") 23 | ], 24 | targets: [ 25 | .target( 26 | name: "SwiftUIBackports", 27 | dependencies: ["SwiftBackports"], 28 | resources: [.process("Resources/PrivacyInfo.xcprivacy")] 29 | ) 30 | ], 31 | swiftLanguageVersions: [.v5] 32 | ) 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Shaps Benkau 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ShareLink/ShareLink.swift: -------------------------------------------------------------------------------- 1 | import SwiftBackports 2 | 3 | #if os(macOS) || os(iOS) 4 | import SwiftUI 5 | #if canImport(LinkPresentation) 6 | import LinkPresentation 7 | #endif 8 | 9 | @available(iOS, deprecated: 16) 10 | @available(macOS, deprecated: 13) 11 | @available(watchOS, deprecated: 9) 12 | @available(tvOS, unavailable) 13 | public extension Backport where Wrapped == Any { 14 | struct ShareLink: View where Data: RandomAccessCollection, Data.Element: Shareable, Label: View { 15 | @State private var activity: ActivityItem? 16 | 17 | let label: Label 18 | let data: Data 19 | let subject: String? 20 | let message: String? 21 | let preview: (Data.Element) -> SharePreview 22 | 23 | public var body: some View { 24 | Button { 25 | activity = ActivityItem(data: data) 26 | } label: { 27 | label 28 | } 29 | .shareSheet(item: $activity) 30 | } 31 | } 32 | } 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Toolbar/ToolbarBackground+Environment.swift: -------------------------------------------------------------------------------- 1 | import SwiftBackports 2 | 3 | #if os(iOS) 4 | import SwiftUI 5 | 6 | struct ToolbarViews { 7 | var navigationBar: AnyView? 8 | var bottomBar: AnyView? 9 | var tabBar: AnyView? 10 | } 11 | 12 | struct ToolbarVisibility { 13 | var navigationBar: Backport.Visibility? 14 | var bottomBar: Backport.Visibility? 15 | var tabBar: Backport.Visibility? 16 | } 17 | 18 | private struct ToolbarViewsKey: EnvironmentKey { 19 | static var defaultValue: ToolbarViews = .init() 20 | } 21 | 22 | private struct ToolbarVisibilityKey: EnvironmentKey { 23 | static var defaultValue: ToolbarVisibility = .init() 24 | } 25 | 26 | internal extension EnvironmentValues { 27 | var toolbarViews: ToolbarViews { 28 | get { self[ToolbarViewsKey.self] } 29 | set { self[ToolbarViewsKey.self] = newValue } 30 | } 31 | } 32 | 33 | internal extension EnvironmentValues { 34 | var toolbarVisibility: ToolbarVisibility { 35 | get { self[ToolbarVisibilityKey.self] } 36 | set { self[ToolbarVisibilityKey.self] = newValue } 37 | } 38 | } 39 | #endif 40 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item+Preview.swift: -------------------------------------------------------------------------------- 1 | //import SwiftUI 2 | //import SwiftBackports 3 | // 4 | //@available(iOS, deprecated: 16) 5 | //@available(macOS, deprecated: 13) 6 | //@available(watchOS, deprecated: 9) 7 | //@available(tvOS, unavailable) 8 | //public extension Backport.ShareLink where Wrapped == Any { 9 | // init(item: I, subject: String? = nil, message: String? = nil, preview: SharePreview) 10 | // where Data == CollectionOfOne, Label == DefaultShareLinkLabel { 11 | // self.label = .init() 12 | // self.data = .init(item) 13 | // self.subject = subject 14 | // self.message = message 15 | // self.preview = { _ in preview } 16 | // } 17 | // 18 | // init(item: I, subject: String? = nil, message: String? = nil, preview: SharePreview, @ViewBuilder label: () -> Label) 19 | // where Data == CollectionOfOne { 20 | // self.label = label() 21 | // self.data = .init(item) 22 | // self.subject = subject 23 | // self.message = message 24 | // self.preview = { _ in preview } 25 | // } 26 | //} 27 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items+Preview.swift: -------------------------------------------------------------------------------- 1 | //import SwiftUI 2 | //import SwiftBackports 3 | // 4 | //@available(iOS, deprecated: 16) 5 | //@available(macOS, deprecated: 13) 6 | //@available(watchOS, deprecated: 9) 7 | //@available(tvOS, unavailable) 8 | //public extension Backport.ShareLink where Wrapped == Any { 9 | // init(items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview) 10 | // where Data.Element: Shareable, Label == DefaultShareLinkLabel { 11 | // self.label = .init() 12 | // self.data = items 13 | // self.subject = subject 14 | // self.message = message 15 | // self.preview = preview 16 | // } 17 | // 18 | // init(items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview, @ViewBuilder label: () -> Label) 19 | // where Data.Element: Shareable { 20 | // self.label = label() 21 | // self.data = items 22 | // self.subject = subject 23 | // self.message = message 24 | // self.preview = preview 25 | // } 26 | //} 27 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/LabeledContent/Styles/AutomaticLabeledContentStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | extension Backport where Wrapped == Any { 5 | 6 | public struct AutomaticLabeledContentStyle: BackportLabeledContentStyle { 7 | private struct Content: View { 8 | let configuration: Configuration 9 | 10 | var body: some View { 11 | if configuration.labelHidden { 12 | configuration.content 13 | } else { 14 | HStack(alignment: .firstTextBaseline) { 15 | configuration.label 16 | Spacer() 17 | configuration.content 18 | .foregroundColor(.secondary) 19 | } 20 | } 21 | } 22 | } 23 | 24 | public func makeBody(configuration: Configuration) -> some View { 25 | Content(configuration: configuration) 26 | } 27 | } 28 | 29 | } 30 | 31 | extension BackportLabeledContentStyle where Self == Backport.AutomaticLabeledContentStyle { 32 | static var automatic: Self { .init() } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Internal/NSItemProvider+Async.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CoreServices 3 | import SwiftBackports 4 | 5 | public extension NSItemProvider { 6 | func loadObject(of type: T.Type) async throws -> T where T: _ObjectiveCBridgeable, T._ObjectiveCType: NSItemProviderReading { 7 | try await withCheckedThrowingContinuation { continuation in 8 | _ = loadObject(ofClass: T.self) { (value: _ObjectiveCBridgeable?, error: Error?) in 9 | switch (value, error) { 10 | case let (.some(value as T), nil): 11 | continuation.resume(returning: value) 12 | case let (_, .some(error)): 13 | continuation.resume(throwing: error) 14 | return 15 | default: 16 | return 17 | } 18 | } 19 | } 20 | } 21 | } 22 | 23 | extension NSData: @retroactive NSItemProviderReading { 24 | public static var readableTypeIdentifiersForItemProvider: [String] { [String(kUTTypeData)] } 25 | public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self { NSData(data: data) as! Self } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPickerSelectionBehavior.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import SwiftUI 3 | import PhotosUI 4 | import SwiftBackports 5 | 6 | @available(iOS, deprecated: 16) 7 | public extension Backport where Wrapped == Any { 8 | // Available when SwiftUI is imported with PhotosUI 9 | /// A value that determines how the Photos picker handles user selection. 10 | enum PhotosPickerSelectionBehavior: Equatable, Hashable { 11 | /// Uses the default selection behavior. 12 | case `default` 13 | /// Uses the selection order made by the user. Selected items are numbered. 14 | case ordered 15 | 16 | @available(iOS 15, *) 17 | init(behaviour: PHPickerConfiguration.Selection) { 18 | switch behaviour { 19 | case .`default`: self = .`default` 20 | case .ordered: self = .ordered 21 | default: self = .`default` 22 | } 23 | } 24 | 25 | @available(iOS 15, *) 26 | var behaviour: PHPickerConfiguration.Selection { 27 | switch self { 28 | case .ordered: return .ordered 29 | default: return .`default` 30 | } 31 | } 32 | } 33 | } 34 | #endif 35 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/UIHostingConfiguration/ProposedInsets.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// Provides optional inset values. `nil` is interpreted as: use system default 4 | internal struct ProposedInsets: Equatable { 5 | 6 | /// The proposed leading margin measured in points. 7 | /// 8 | /// A value of `nil` tells the system to use a default value 9 | public var leading: CGFloat? 10 | 11 | /// The proposed trailing margin measured in points. 12 | /// 13 | /// A value of `nil` tells the system to use a default value 14 | public var trailing: CGFloat? 15 | 16 | /// The proposed top margin measured in points. 17 | /// 18 | /// A value of `nil` tells the system to use a default value 19 | public var top: CGFloat? 20 | 21 | /// The proposed bottom margin measured in points. 22 | /// 23 | /// A value of `nil` tells the system to use a default value 24 | public var bottom: CGFloat? 25 | 26 | /// An insets proposal with all dimensions left unspecified. 27 | public static var unspecified: ProposedInsets { .init() } 28 | 29 | /// An insets proposal that contains zero for all dimensions. 30 | public static var zero: ProposedInsets { .init(leading: 0, trailing: 0, top: 0, bottom: 0) } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ShareLink/DefaultShareLinkLabel.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | public struct DefaultShareLinkLabel: View { 5 | let text: Text 6 | private static let shareIcon = "square.and.arrow.up" 7 | 8 | init() { 9 | text = .init("Share") 10 | } 11 | 12 | init(_ title: S) { 13 | text = .init(title) 14 | } 15 | 16 | init(_ titleKey: LocalizedStringKey) { 17 | text = .init(titleKey) 18 | } 19 | 20 | init(_ title: Text) { 21 | text = title 22 | } 23 | 24 | public var body: some View { 25 | if #available(iOS 14, macOS 11, watchOS 7, tvOS 14, *) { 26 | Label { 27 | text 28 | } icon: { 29 | Image(systemName: Self.shareIcon) 30 | } 31 | } else { 32 | Backport.Label { 33 | text 34 | } icon: { 35 | #if os(macOS) 36 | // no icon on earlier macOS versions 37 | if #available(macOS 11, *) { 38 | Image(systemName: Self.shareIcon) 39 | } 40 | #else 41 | Image(systemName: Self.shareIcon) 42 | #endif 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Transition/PushTransition.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | public extension Backport where Wrapped == AnyTransition { 5 | /// Creates a transition that when added to a view will animate the view’s 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. 6 | /// - Parameter edge: the edge from which the view will be animated in. 7 | /// - Returns: A transition that animates a view by moving and fading it. 8 | @available(iOS, deprecated: 16.0) 9 | @available(watchOS, deprecated: 9.0) 10 | @available(macOS, deprecated: 13.0) 11 | @available(tvOS, deprecated: 16.0) 12 | func push(from edge: Edge) -> AnyTransition { 13 | var oppositeEdge: Edge 14 | switch edge { 15 | case .top: 16 | oppositeEdge = .bottom 17 | case .leading: 18 | oppositeEdge = .trailing 19 | case .bottom: 20 | oppositeEdge = .top 21 | case .trailing: 22 | oppositeEdge = .leading 23 | } 24 | 25 | return .asymmetric( 26 | insertion: .move(edge: edge), 27 | removal: .move(edge: oppositeEdge) 28 | ).combined(with: .opacity) 29 | } 30 | } 31 | 32 | 33 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Navigation/NavigationTitle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 14) 5 | @available(watchOS, deprecated: 7) 6 | @available(tvOS, deprecated: 14) 7 | @MainActor 8 | public extension Backport where Wrapped: View { 9 | 10 | @ViewBuilder 11 | func navigationTitle(_ title: S) -> some View { 12 | #if os(macOS) 13 | if #available(macOS 11, *) { 14 | wrapped.navigationTitle(title) 15 | } else { 16 | wrapped 17 | } 18 | #else 19 | wrapped.navigationBarTitle(title) 20 | #endif 21 | } 22 | 23 | @ViewBuilder 24 | func navigationTitle(_ titleKey: LocalizedStringKey) -> some View { 25 | #if os(macOS) 26 | if #available(macOS 11, *) { 27 | wrapped.navigationTitle(titleKey) 28 | } else { 29 | wrapped 30 | } 31 | #else 32 | wrapped.navigationBarTitle(titleKey) 33 | #endif 34 | } 35 | 36 | @ViewBuilder 37 | func navigationTitle(_ title: Text) -> some View { 38 | #if os(macOS) 39 | if #available(macOS 11, *) { 40 | wrapped.navigationTitle(title) 41 | } else { 42 | wrapped 43 | } 44 | #else 45 | wrapped.navigationBarTitle(title) 46 | #endif 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Label/Styles/TitleOnlyLabelStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 14) 5 | @available(macOS, deprecated: 11) 6 | @available(tvOS, deprecated: 14) 7 | @available(watchOS, deprecated: 7) 8 | extension Backport where Wrapped == Any { 9 | 10 | // A label style that only displays the title of the label. 11 | /// 12 | /// You can also use ``LabelStyle/titleOnly`` to construct this style. 13 | public struct TitleOnlyLabelStyle: BackportLabelStyle { 14 | 15 | /// Creates a title-only label style. 16 | public init() { } 17 | 18 | /// Creates a view that represents the body of a label. 19 | /// 20 | /// The system calls this method for each ``Label`` instance in a view 21 | /// hierarchy where this style is the current label style. 22 | /// 23 | /// - Parameter configuration: The properties of the label. 24 | public func makeBody(configuration: Configuration) -> some View { 25 | configuration.title 26 | } 27 | 28 | } 29 | 30 | 31 | } 32 | 33 | @available(iOS, deprecated: 14) 34 | @available(macOS, deprecated: 11) 35 | @available(tvOS, deprecated: 14) 36 | @available(watchOS, deprecated: 7) 37 | extension BackportLabelStyle where Self == Backport.TitleOnlyLabelStyle { 38 | 39 | /// A label style that only displays the title of the label. 40 | public static var titleOnly: Self { .init() } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/LabeledContent/LabeledContentStyleConfiguration.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 16) 5 | @available(tvOS, deprecated: 16) 6 | @available(macOS, deprecated: 13) 7 | @available(watchOS, deprecated: 9) 8 | extension Backport where Wrapped == Any { 9 | 10 | /// The properties of a labeled content instance. 11 | public struct LabeledContentStyleConfiguration { 12 | private struct Label: View { 13 | let isHidden: Bool 14 | let content: Content 15 | 16 | public var body: some View { 17 | if !isHidden { 18 | content 19 | } 20 | } 21 | } 22 | 23 | var labelHidden: Bool = false 24 | 25 | private let _label: AnyView 26 | 27 | /// The label of the labeled content instance. 28 | public var label: some View { 29 | Label(isHidden: labelHidden, content: _label) 30 | } 31 | 32 | /// The content of the labeled content instance. 33 | public let content: AnyView 34 | 35 | internal init(label: L, content: C) { 36 | _label = .init(label) 37 | self.content = .init(content) 38 | } 39 | 40 | func labelHidden(_ hidden: Bool) -> Self { 41 | var copy = self 42 | copy.labelHidden = hidden 43 | return copy 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/PhotosPicker/Core/MediaResults.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import PhotosUI 3 | import SwiftBackports 4 | 5 | /// Represents a `PHFetchResult` that can be used as a `RandomAccessCollection` in a SwiftUI view such as `List`, `ForEach`, etc... 6 | internal struct MediaResults: RandomAccessCollection where Result: PHObject { 7 | 8 | /// Represents the underlying results 9 | public private(set) var result: PHFetchResult 10 | 11 | /// Instantiates a new instance with the specified result 12 | public init(_ result: PHFetchResult) { 13 | self.result = result 14 | } 15 | 16 | public var startIndex: Int { 0 } 17 | public var endIndex: Int { result.count } 18 | public subscript(position: Int) -> Result { result.object(at: position) } 19 | 20 | } 21 | 22 | /// An observer used to observe changes on a `PHFetchResult` 23 | internal final class ResultsObserver: NSObject, ObservableObject, PHPhotoLibraryChangeObserver where Result: PHObject { 24 | 25 | @Published 26 | internal var result: PHFetchResult 27 | 28 | deinit { 29 | PHPhotoLibrary.shared().unregisterChangeObserver(self) 30 | } 31 | 32 | init(result: PHFetchResult) { 33 | self.result = result 34 | super.init() 35 | PHPhotoLibrary.shared().register(self) 36 | } 37 | 38 | func photoLibraryDidChange(_ changeInstance: PHChange) { 39 | result = changeInstance.changeDetails(for: result)?.fetchResultAfterChanges ?? result 40 | } 41 | 42 | } 43 | #endif 44 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Label/Styles/IconOnlyLabelStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 14) 5 | @available(macOS, deprecated: 11) 6 | @available(tvOS, deprecated: 14) 7 | @available(watchOS, deprecated: 7) 8 | extension Backport where Wrapped == Any { 9 | 10 | /// A label style that only displays the icon of the label. 11 | /// 12 | /// You can also use ``LabelStyle/iconOnly`` to construct this style. 13 | public struct IconOnlyLabelStyle: BackportLabelStyle { 14 | 15 | /// Creates an icon-only label style. 16 | public init() { } 17 | 18 | /// Creates a view that represents the body of a label. 19 | /// 20 | /// The system calls this method for each ``Label`` instance in a view 21 | /// hierarchy where this style is the current label style. 22 | /// 23 | /// - Parameter configuration: The properties of the label. 24 | public func makeBody(configuration: Configuration) -> some View { 25 | configuration.icon 26 | } 27 | 28 | } 29 | 30 | } 31 | 32 | @available(iOS, deprecated: 14) 33 | @available(macOS, deprecated: 11) 34 | @available(tvOS, deprecated: 14) 35 | @available(watchOS, deprecated: 7) 36 | extension BackportLabelStyle where Self == Backport.IconOnlyLabelStyle { 37 | 38 | /// A label style that only displays the icon of the label. 39 | /// 40 | /// The title of the label is still used for non-visual descriptions, such as 41 | /// VoiceOver. 42 | public static var iconOnly: Self { .init() } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchAsset.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import Photos 3 | import SwiftUI 4 | import SwiftBackports 5 | 6 | /// Fetches a single asset 7 | @propertyWrapper 8 | struct FetchAsset: DynamicProperty { 9 | 10 | @ObservedObject 11 | internal private(set) var observer: AssetObserver 12 | 13 | /// Represents the fetched asset 14 | public var wrappedValue: MediaAsset { 15 | MediaAsset(asset: observer.asset) 16 | } 17 | 18 | } 19 | 20 | internal extension FetchAsset { 21 | 22 | /// Instantiates the fetch with an existing `PHAsset` 23 | /// - Parameter asset: The asset 24 | init(_ asset: PHAsset) { 25 | let observer = AssetObserver(asset: asset) 26 | self.init(observer: observer) 27 | } 28 | 29 | } 30 | 31 | /// Represents the result of a `FetchAsset` request. 32 | struct MediaAsset { 33 | 34 | public private(set) var asset: PHAsset? 35 | 36 | public init(asset: PHAsset?) { 37 | self.asset = asset 38 | } 39 | 40 | } 41 | 42 | internal final class AssetObserver: NSObject, ObservableObject, PHPhotoLibraryChangeObserver { 43 | 44 | @Published 45 | internal var asset: PHAsset? 46 | 47 | deinit { 48 | PHPhotoLibrary.shared().unregisterChangeObserver(self) 49 | } 50 | 51 | init(asset: PHAsset) { 52 | self.asset = asset 53 | super.init() 54 | PHPhotoLibrary.shared().register(self) 55 | } 56 | 57 | func photoLibraryDidChange(_ changeInstance: PHChange) { 58 | guard let asset = asset else { return } 59 | self.asset = changeInstance.changeDetails(for: asset)?.objectAfterChanges 60 | } 61 | 62 | } 63 | #endif 64 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item+Label+Preview.swift: -------------------------------------------------------------------------------- 1 | //import SwiftUI 2 | //import SwiftBackports 3 | // 4 | //@available(iOS, deprecated: 16) 5 | //@available(macOS, deprecated: 13) 6 | //@available(watchOS, deprecated: 9) 7 | //@available(tvOS, unavailable) 8 | //public extension Backport.ShareLink where Wrapped == Any { 9 | // init(_ title: S, item: I, subject: String? = nil, message: String? = nil, preview: SharePreview) 10 | // where Data == CollectionOfOne, Label == DefaultShareLinkLabel { 11 | // self.label = .init(title) 12 | // self.data = .init(item) 13 | // self.subject = subject 14 | // self.message = message 15 | // self.preview = { _ in preview } 16 | // } 17 | // 18 | // init(_ titleKey: LocalizedStringKey, item: I, subject: String? = nil, message: String? = nil, preview: SharePreview) 19 | // where Data == CollectionOfOne, Label == DefaultShareLinkLabel { 20 | // self.label = .init(titleKey) 21 | // self.data = .init(item) 22 | // self.subject = subject 23 | // self.message = message 24 | // self.preview = { _ in preview } 25 | // } 26 | // 27 | // init(_ title: Text, item: I, subject: String? = nil, message: String? = nil, preview: SharePreview) 28 | // where Data == CollectionOfOne, Label == DefaultShareLinkLabel { 29 | // self.label = .init(title) 30 | // self.data = .init(item) 31 | // self.subject = subject 32 | // self.message = message 33 | // self.preview = { _ in preview } 34 | // } 35 | //} 36 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Deprecations/Presenatation+Deprecations.swift: -------------------------------------------------------------------------------- 1 | import SwiftBackports 2 | import SwiftUI 3 | 4 | @available(tvOS, deprecated: 13) 5 | @available(macOS, deprecated: 10.15) 6 | @available(watchOS, deprecated: 6) 7 | public extension View { 8 | 9 | /// Sets whether this presentation should act as a `modal`, preventing interactive dismissals 10 | /// - Parameter isModal: If `true` the user will not be able to interactively dismiss 11 | @ViewBuilder 12 | @available(iOS, deprecated: 13, renamed: "backport.interactiveDismissDisabled(_:)") 13 | func presentation(isModal: Bool) -> some View { 14 | #if os(iOS) 15 | if #available(iOS 15, *) { 16 | backport.interactiveDismissDisabled(isModal) 17 | } else { 18 | self 19 | } 20 | #else 21 | self 22 | #endif 23 | } 24 | 25 | /// Provides fine-grained control over the dismissal. 26 | /// - Parameters: 27 | /// - isModal: If `true`, the user will not be able to interactively dismiss 28 | /// - onAttempt: A closure that will be called when an interactive dismiss attempt occurs. 29 | /// You can use this as an opportunity to present an ActionSheet to prompt the user. 30 | @ViewBuilder 31 | @available(iOS, deprecated: 13, renamed: "backport.interactiveDismissDisabled(_:onAttempt:)") 32 | func presentation(isModal: Bool = true, _ onAttempt: @escaping () -> Void) -> some View { 33 | #if os(iOS) 34 | if #available(iOS 15, *) { 35 | backport.interactiveDismissDisabled(isModal, onAttempt: onAttempt) 36 | } else { 37 | self 38 | } 39 | #else 40 | self 41 | #endif 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items+Label+Preview.swift: -------------------------------------------------------------------------------- 1 | //import SwiftUI 2 | //import SwiftBackports 3 | // 4 | //@available(iOS, deprecated: 16) 5 | //@available(macOS, deprecated: 13) 6 | //@available(watchOS, deprecated: 9) 7 | //@available(tvOS, unavailable) 8 | //public extension Backport.ShareLink where Wrapped == Any { 9 | // init(_ title: S, items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview) 10 | // where Data.Element: Shareable, Label == DefaultShareLinkLabel 11 | // { 12 | // self.label = .init(title) 13 | // self.data = items 14 | // self.subject = subject 15 | // self.message = message 16 | // self.preview = preview 17 | // } 18 | // 19 | // init(_ titleKey: LocalizedStringKey, items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview) 20 | // where Data.Element: Shareable, Label == DefaultShareLinkLabel 21 | // { 22 | // self.label = .init(titleKey) 23 | // self.data = items 24 | // self.subject = subject 25 | // self.message = message 26 | // self.preview = preview 27 | // } 28 | // 29 | // init(_ title: Text, items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview) 30 | // where Data.Element: Shareable, Label == DefaultShareLinkLabel 31 | // { 32 | // self.label = .init(title) 33 | // self.data = items 34 | // self.subject = subject 35 | // self.message = message 36 | // self.preview = preview 37 | // } 38 | //} 39 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Deprecations/FittingScrollView+Deprecations.swift: -------------------------------------------------------------------------------- 1 | import SwiftBackports 2 | import SwiftUI 3 | 4 | @available(iOS, unavailable, message: "This has been moved to SwiftUIPlus and renamed to VScrollStack. You should move to the new package which automatically includes all backports as well 👍", renamed: "VScrollStack") 5 | @available(macOS, unavailable, message: "This has been moved to SwiftUIPlus and renamed to VScrollStack. You should move to the new package which automatically includes all backports as well 👍", renamed: "VScrollStack") 6 | @available(tvOS, unavailable, message: "This has been moved to SwiftUIPlus and renamed to VScrollStack. You should move to the new package which automatically includes all backports as well 👍", renamed: "VScrollStack") 7 | @available(watchOS, unavailable, message: "This has been moved to SwiftUIPlus and renamed to VScrollStack. You should move to the new package which automatically includes all backports as well 👍", renamed: "VScrollStack") 8 | public struct FittingScrollView: View { 9 | private let content: Content 10 | private let showsIndicators: Bool 11 | 12 | public init(showsIndicators: Bool = true, @ViewBuilder content: () -> Content) { 13 | self.showsIndicators = showsIndicators 14 | self.content = content() 15 | } 16 | 17 | public var body: some View { 18 | GeometryReader { geo in 19 | SwiftUI.ScrollView(showsIndicators: showsIndicators) { 20 | VStack(spacing: 10) { 21 | content 22 | } 23 | .frame( 24 | maxWidth: geo.size.width, 25 | minHeight: geo.size.height 26 | ) 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Internal/SafeArea.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | #if os(iOS) || os(tvOS) 5 | /* 6 | Since UICollectionView is not designed to support SwiftUI out of the box, 7 | we need to use a little trick to get the SwiftUI View's to ignore safeArea 8 | insets, otherwise our cell's will not always layout correctly. 9 | */ 10 | internal extension UIHostingController { 11 | convenience init(rootView: Content, ignoreSafeArea: Bool) { 12 | self.init(rootView: rootView) 13 | 14 | if ignoreSafeArea { 15 | disableSafeArea() 16 | } 17 | } 18 | 19 | func disableSafeArea() { 20 | guard let viewClass = object_getClass(view) else { return } 21 | 22 | let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea") 23 | if let viewSubclass = NSClassFromString(viewSubclassName) { 24 | object_setClass(view, viewSubclass) 25 | } 26 | else { 27 | guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return } 28 | guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return } 29 | 30 | if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) { 31 | let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in 32 | return .zero 33 | } 34 | class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method)) 35 | } 36 | 37 | objc_registerClassPair(viewSubclass) 38 | object_setClass(view, viewSubclass) 39 | } 40 | } 41 | } 42 | #endif 43 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Visibility/Visibility.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 15) 5 | @available(macOS, deprecated: 12) 6 | @available(tvOS, deprecated: 15) 7 | @available(watchOS, deprecated: 8) 8 | extension Backport where Wrapped == Any { 9 | 10 | public enum Visibility: Hashable, CaseIterable { 11 | /// The element may be visible or hidden depending on the policies of the 12 | /// component accepting the visibility configuration. 13 | /// 14 | /// For example, some components employ different automatic behavior 15 | /// depending on factors including the platform, the surrounding container, 16 | /// user settings, etc. 17 | case automatic 18 | 19 | /// The element may be visible. 20 | /// 21 | /// Some APIs may use this value to represent a hint or preference, rather 22 | /// than a mandatory assertion. For example, setting list row separator 23 | /// visibility to `visible` using the 24 | /// ``View/listRowSeparator(_:edges:)`` modifier may not always 25 | /// result in any visible separators, especially for list styles that do not 26 | /// include separators as part of their design. 27 | case visible 28 | 29 | /// The element may be hidden. 30 | /// 31 | /// Some APIs may use this value to represent a hint or preference, rather 32 | /// than a mandatory assertion. For example, setting confirmation dialog 33 | /// title visibility to `hidden` using the 34 | /// ``View/confirmationDialog(_:isPresented:titleVisibility:actions:)-87n66`` 35 | /// modifier may not always hide the dialog title, which is required on 36 | /// some platforms. 37 | case hidden 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Internal/Platforms.swift: -------------------------------------------------------------------------------- 1 | import SwiftBackports 2 | 3 | #if os(iOS) 4 | 5 | import UIKit 6 | 7 | public typealias PlatformImage = UIImage 8 | public typealias PlatformScreen = UIScreen 9 | 10 | internal typealias PlatformView = UIView 11 | internal typealias PlatformScrollView = UIScrollView 12 | internal typealias PlatformViewController = UIViewController 13 | 14 | extension UIScreen { 15 | @nonobjc 16 | public static var mainScreen: UIScreen { .main } 17 | } 18 | 19 | extension UIImage { 20 | public var png: Data? { pngData() } 21 | public func jpg(quality: CGFloat) -> Data? { jpegData(compressionQuality: quality) } 22 | } 23 | 24 | extension CGContext { 25 | internal static var current: CGContext? { 26 | UIGraphicsGetCurrentContext() 27 | } 28 | } 29 | 30 | 31 | #elseif os(macOS) 32 | 33 | import AppKit 34 | 35 | public typealias PlatformImage = NSImage 36 | public typealias PlatformScreen = NSScreen 37 | 38 | internal typealias PlatformView = NSView 39 | internal typealias PlatformScrollView = NSScrollView 40 | internal typealias PlatformViewController = NSViewController 41 | 42 | extension NSScreen { 43 | public static var mainScreen: NSScreen { NSScreen.main! } 44 | public var scale: CGFloat { backingScaleFactor } 45 | } 46 | 47 | extension NSImage { 48 | public var png: Data? { 49 | return NSBitmapImageRep(data: tiffRepresentation!)?.representation(using: .png, properties: [:]) 50 | } 51 | 52 | public func jpg(quality: CGFloat) -> Data? { 53 | return NSBitmapImageRep(data: tiffRepresentation!)?.representation(using: .jpeg, properties: [.compressionFactor: quality]) 54 | } 55 | } 56 | 57 | extension CGContext { 58 | internal static var current: CGContext? { 59 | NSGraphicsContext.current?.cgContext 60 | } 61 | } 62 | #endif 63 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) || os(iOS) 2 | import SwiftUI 3 | import SwiftBackports 4 | 5 | @available(iOS, deprecated: 16) 6 | @available(macOS, deprecated: 13) 7 | @available(watchOS, deprecated: 9) 8 | @available(tvOS, unavailable) 9 | public extension Backport.ShareLink where Wrapped == Any { 10 | init(items: Data, subject: String? = nil, message: String? = nil) 11 | where PreviewImage == Never, PreviewIcon == Never, Data.Element == String, Label == DefaultShareLinkLabel { 12 | self.label = .init() 13 | self.data = items 14 | self.subject = subject 15 | self.message = message 16 | self.preview = { .init($0) } 17 | } 18 | 19 | init(items: Data, subject: String? = nil, message: String? = nil) 20 | where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL, Label == DefaultShareLinkLabel { 21 | self.label = .init() 22 | self.data = items 23 | self.subject = subject 24 | self.message = message 25 | self.preview = { .init($0.absoluteString) } 26 | } 27 | 28 | init(items: Data, subject: String? = nil, message: String? = nil, @ViewBuilder label: () -> Label) 29 | where PreviewImage == Never, PreviewIcon == Never, Data.Element == String { 30 | self.label = label() 31 | self.data = items 32 | self.subject = subject 33 | self.message = message 34 | self.preview = { .init($0) } 35 | } 36 | 37 | init(items: Data, subject: String? = nil, message: String? = nil, @ViewBuilder label: () -> Label) 38 | where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL { 39 | self.label = label() 40 | self.data = items 41 | self.subject = subject 42 | self.message = message 43 | self.preview = { .init($0.absoluteString) } 44 | } 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Container/ForEach+Subviews.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Backport { 4 | public struct ForEach: View { 5 | private var content: AnyView 6 | 7 | public init(subviewOf view: V, @ViewBuilder content: @escaping (Subview) -> Content) where Data == ForEachSubviewCollection, ID == Subview.ID, Content: View, V: View { 8 | self.content = .init( 9 | view.variadic { children in 10 | ForEachSubviewCollection(children: children, content: content) 11 | } 12 | ) 13 | } 14 | 15 | public var body: some View { content } 16 | } 17 | } 18 | 19 | extension Backport { 20 | public struct ForEachSubviewCollection: View, RandomAccessCollection, Sendable where Content: View { 21 | public typealias SubSequence = Slice> 22 | public typealias Iterator = IndexingIterator> 23 | public typealias Indices = Range 24 | public typealias Index = Int 25 | public typealias Element = Subview 26 | 27 | public var startIndex: Int { children.startIndex } 28 | public var endIndex: Int { children.endIndex } 29 | 30 | public func index(before i: Int) -> Int { 31 | children.index(before: i) 32 | } 33 | 34 | public func index(after i: Int) -> Int { 35 | children.index(after: i) 36 | } 37 | 38 | public subscript(index: Int) -> Subview { 39 | .init(children[index]) 40 | } 41 | 42 | fileprivate let children: _VariadicView.Children 43 | let content: (Subview) -> Content 44 | 45 | public var body: some View { 46 | SwiftUI.ForEach(children, id: \.id) { 47 | content(Subview($0)) 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) || os(iOS) 2 | import SwiftUI 3 | import SwiftBackports 4 | 5 | @available(iOS, deprecated: 16) 6 | @available(macOS, deprecated: 13) 7 | @available(watchOS, deprecated: 9) 8 | @available(tvOS, unavailable) 9 | public extension Backport.ShareLink where Wrapped == Any { 10 | init(item: String, subject: String? = nil, message: String? = nil) 11 | where Data == CollectionOfOne, PreviewImage == Never, PreviewIcon == Never, Label == DefaultShareLinkLabel { 12 | self.label = .init() 13 | self.data = .init(item) 14 | self.subject = subject 15 | self.message = message 16 | self.preview = { .init($0) } 17 | } 18 | 19 | init(item: URL, subject: String? = nil, message: String? = nil) 20 | where Data == CollectionOfOne, PreviewImage == Never, PreviewIcon == Never, Label == DefaultShareLinkLabel { 21 | self.label = .init() 22 | self.data = .init(item) 23 | self.subject = subject 24 | self.message = message 25 | self.preview = { .init($0.absoluteString) } 26 | } 27 | 28 | init(item: String, subject: String? = nil, message: String? = nil, @ViewBuilder label: () -> Label) 29 | where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne { 30 | self.label = label() 31 | self.data = .init(item) 32 | self.subject = subject 33 | self.message = message 34 | self.preview = { .init($0) } 35 | } 36 | 37 | init(item: URL, subject: String? = nil, message: String? = nil, @ViewBuilder label: () -> Label) 38 | where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne { 39 | self.label = label() 40 | self.data = .init(item) 41 | self.subject = subject 42 | self.message = message 43 | self.preview = { .init($0.absoluteString) } 44 | } 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/LabeledContent/LabeledContentStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 16) 5 | @available(tvOS, deprecated: 16) 6 | @available(macOS, deprecated: 13) 7 | @available(watchOS, deprecated: 9) 8 | extension Backport where Wrapped: View { 9 | /// Sets a style for labeled content. 10 | public func labeledContentStyle(_ style: S) -> some View where S: BackportLabeledContentStyle { 11 | wrapped.environment(\.backportLabeledContentStyle, .init(style)) 12 | } 13 | } 14 | 15 | @available(iOS, deprecated: 16) 16 | @available(tvOS, deprecated: 16) 17 | @available(macOS, deprecated: 13) 18 | @available(watchOS, deprecated: 9) 19 | public protocol BackportLabeledContentStyle { 20 | typealias Configuration = Backport.LabeledContentStyleConfiguration 21 | associatedtype Body: View 22 | @ViewBuilder func makeBody(configuration: Configuration) -> Body 23 | } 24 | 25 | internal struct AnyLabeledContentStyle: BackportLabeledContentStyle { 26 | typealias Configuration = Backport.LabeledContentStyleConfiguration 27 | let _makeBody: (Configuration) -> AnyView 28 | 29 | init(_ style: S) { 30 | _makeBody = { config in 31 | AnyView(style.makeBody(configuration: config)) 32 | } 33 | } 34 | 35 | func makeBody(configuration: Configuration) -> some View { 36 | _makeBody(configuration) 37 | } 38 | } 39 | 40 | private struct BackportLabeledContentStyleEnvironmentKey: EnvironmentKey { 41 | static var defaultValue: AnyLabeledContentStyle = .init(.automatic) 42 | } 43 | 44 | @available(iOS, deprecated: 16) 45 | @available(tvOS, deprecated: 16) 46 | @available(macOS, deprecated: 13) 47 | @available(watchOS, deprecated: 9) 48 | internal extension EnvironmentValues { 49 | var backportLabeledContentStyle: AnyLabeledContentStyle { 50 | get { self[BackportLabeledContentStyleEnvironmentKey.self] } 51 | set { self[BackportLabeledContentStyleEnvironmentKey.self] = newValue } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/ScrollView/ScrollIndicatorVisibility.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 16) 5 | @available(tvOS, deprecated: 16) 6 | @available(macOS, deprecated: 13) 7 | @available(watchOS, deprecated: 9) 8 | extension Backport where Wrapped == Any { 9 | 10 | /// The visibility of scroll indicators of a UI element. 11 | /// 12 | /// Pass a value of this type to the ``View.backport.scrollIndicators(_:axes:)`` method 13 | /// to specify the preferred scroll indicator visibility of a view hierarchy. 14 | public struct ScrollIndicatorVisibility: Hashable, CustomStringConvertible { 15 | internal enum IndicatorVisibility: Hashable { 16 | case automatic 17 | case visible 18 | case hidden 19 | } 20 | 21 | let visibility: Backport.Visibility 22 | 23 | var scrollViewVisible: Bool { 24 | visibility != .hidden 25 | } 26 | 27 | public var description: String { 28 | String(describing: visibility) 29 | } 30 | 31 | /// Scroll indicator visibility depends on the 32 | /// policies of the component accepting the visibility configuration. 33 | public static var automatic: ScrollIndicatorVisibility { 34 | .init(visibility: .automatic) 35 | } 36 | 37 | /// Show the scroll indicators. 38 | /// 39 | /// The actual visibility of the indicators depends on platform 40 | /// conventions like auto-hiding behaviors in iOS or user preference 41 | /// behaviors in macOS. 42 | public static var visible: ScrollIndicatorVisibility { 43 | .init(visibility: .visible) 44 | } 45 | 46 | /// Hide the scroll indicators. 47 | /// 48 | /// By default, scroll views in macOS show indicators when a 49 | /// mouse is connected. Use ``never`` to indicate 50 | /// a stronger preference that can override this behavior. 51 | public static var hidden: ScrollIndicatorVisibility { 52 | .init(visibility: .hidden) 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Deprecations/FittingGeometryReader+Deprecations.swift: -------------------------------------------------------------------------------- 1 | import SwiftBackports 2 | import SwiftUI 3 | 4 | @available(iOS, unavailable, message: "This has been moved to SwiftUIPlus. You should move to the new package which automatically includes all backports as well 👍") 5 | @available(macOS, unavailable, message: "This has been moved to SwiftUIPlus. You should move to the new package which automatically includes all backports as well 👍") 6 | @available(tvOS, unavailable, message: "This has been moved to SwiftUIPlus. You should move to the new package which automatically includes all backports as well 👍") 7 | @available(watchOS, unavailable, message: "This has been moved to SwiftUIPlus. You should move to the new package which automatically includes all backports as well 👍") 8 | public struct FittingGeometryReader: View where Content: View { 9 | 10 | @State private var height: CGFloat = 10 // must be non-zero 11 | private var content: (GeometryProxy) -> Content 12 | 13 | public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content) { 14 | self.content = content 15 | } 16 | 17 | public var body: some View { 18 | GeometryReader { geo in 19 | content(geo) 20 | .fixedSize(horizontal: false, vertical: true) 21 | .modifier(SizeModifier()) 22 | .onPreferenceChange(SizePreferenceKey.self) { 23 | height = $0.height 24 | } 25 | } 26 | .frame(height: height) 27 | } 28 | 29 | } 30 | 31 | private struct SizePreferenceKey: PreferenceKey { 32 | static var defaultValue: CGSize = .zero 33 | static func reduce(value: inout CGSize, nextValue: () -> CGSize) { 34 | value = nextValue() 35 | } 36 | } 37 | 38 | private struct SizeModifier: ViewModifier { 39 | func body(content: Content) -> some View { 40 | content.overlay( 41 | GeometryReader { geo in 42 | Color.clear.preference( 43 | key: SizePreferenceKey.self, 44 | value: geo.size 45 | ) 46 | } 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ProgressView/Styles/DefaultProgressViewStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 14) 5 | @available(macOS, deprecated: 11) 6 | @available(tvOS, deprecated: 14.0) 7 | @available(watchOS, deprecated: 7.0) 8 | extension Backport where Wrapped == Any { 9 | 10 | /// The default progress view style in the current context of the view being 11 | /// styled. 12 | /// 13 | /// You can also use ``ProgressViewStyle/automatic`` to construct this style. 14 | public struct DefaultProgressViewStyle: BackportProgressViewStyle { 15 | 16 | /// Creates a default progress view style. 17 | public init() { } 18 | 19 | /// Creates a view representing the body of a progress view. 20 | /// 21 | /// - Parameter configuration: The properties of the progress view being 22 | /// created. 23 | /// 24 | /// The view hierarchy calls this method for each progress view where this 25 | /// style is the current progress view style. 26 | /// 27 | /// - Parameter configuration: The properties of the progress view, such as 28 | /// its preferred progress type. 29 | public func makeBody(configuration: Configuration) -> some View { 30 | switch configuration.preferredKind { 31 | case .circular: 32 | Backport.CircularProgressViewStyle().makeBody(configuration: configuration) 33 | case .linear: 34 | #if os(iOS) 35 | if configuration.fractionCompleted == nil { 36 | Backport.CircularProgressViewStyle().makeBody(configuration: configuration) 37 | } else { 38 | Backport.LinearProgressViewStyle().makeBody(configuration: configuration) 39 | } 40 | #else 41 | Backport.LinearProgressViewStyle().makeBody(configuration: configuration) 42 | #endif 43 | } 44 | } 45 | } 46 | 47 | } 48 | 49 | public extension BackportProgressViewStyle where Self == Backport.DefaultProgressViewStyle { 50 | static var automatic: Self { .init() } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Label/Styles/DefaultLabelStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 14) 5 | @available(macOS, deprecated: 11) 6 | @available(tvOS, deprecated: 14) 7 | @available(watchOS, deprecated: 7) 8 | extension Backport where Wrapped == Any { 9 | 10 | /// The default label style in the current context. 11 | /// 12 | /// You can also use ``LabelStyle/automatic`` to construct this style. 13 | public struct DefaultLabelStyle: BackportLabelStyle { 14 | private struct Label: View { 15 | let configuration: Configuration 16 | @State private var isToolbarElement: Bool = false 17 | 18 | var body: some View { 19 | if isToolbarElement { 20 | IconOnlyLabelStyle().makeBody(configuration: configuration) 21 | } else { 22 | TitleAndIconLabelStyle().makeBody(configuration: configuration) 23 | #if os(iOS) 24 | .ancestor(forType: UINavigationBar.self) { _ in 25 | isToolbarElement = true 26 | } 27 | #endif 28 | } 29 | } 30 | } 31 | 32 | public init() { } 33 | 34 | /// Creates a view that represents the body of a label. 35 | /// 36 | /// The system calls this method for each ``Label`` instance in a view 37 | /// hierarchy where this style is the current label style. 38 | /// 39 | /// - Parameter configuration: The properties of the label. 40 | public func makeBody(configuration: Configuration) -> some View { 41 | Label(configuration: configuration) 42 | } 43 | 44 | } 45 | 46 | } 47 | 48 | @available(iOS, deprecated: 14) 49 | @available(macOS, deprecated: 11) 50 | @available(tvOS, deprecated: 14) 51 | @available(watchOS, deprecated: 7) 52 | extension BackportLabelStyle where Self == Backport.DefaultLabelStyle { 53 | 54 | /// A label style that resolves its appearance automatically based on the 55 | /// current context. 56 | public static var automatic: Self { .init() } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Section/Section.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 15) 5 | @available(tvOS, deprecated: 15) 6 | @available(macOS, deprecated: 12) 7 | @available(watchOS, deprecated: 8) 8 | extension Backport where Wrapped == Any { 9 | 10 | /// A container view that you can use to add hierarchy to certain collection views. 11 | /// 12 | /// Use `Section` instances in views like ``List``, ``Picker``, and 13 | /// ``Form`` to organize content into separate sections. Each section has 14 | /// custom content that you provide on a per-instance basis. You can also 15 | /// provide headers and footers for each section. 16 | public struct Section: View { 17 | @ViewBuilder let content: () -> Content 18 | @ViewBuilder let header: () -> Parent 19 | @ViewBuilder let footer: () -> Footer 20 | 21 | public var body: some View { 22 | SwiftUI.Section( 23 | content: content, 24 | header: header, 25 | footer: footer 26 | ) 27 | } 28 | } 29 | 30 | } 31 | 32 | extension Backport.Section where Wrapped == Any, Parent == Text, Footer == EmptyView { 33 | 34 | /// Creates a section with the provided section content. 35 | /// - Parameters: 36 | /// - titleKey: The key for the section's localized title, which describes 37 | /// the contents of the section. 38 | /// - content: The section's content. 39 | public init(_ titleKey: LocalizedStringKey, @ViewBuilder content: @escaping () -> Content) { 40 | self.header = { Text(titleKey) } 41 | self.content = content 42 | self.footer = { EmptyView() } 43 | } 44 | 45 | /// Creates a section with the provided section content. 46 | /// - Parameters: 47 | /// - title: A string that describes the contents of the section. 48 | /// - content: The section's content. 49 | public init(_ title: S, @ViewBuilder content: @escaping () -> Content) where S: StringProtocol { 50 | self.header = { Text(title) } 51 | self.content = content 52 | self.footer = { EmptyView() } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/UIHostingConfiguration/ProposedSize.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A proposal for the size 4 | /// 5 | /// * The ``zero`` proposal; the size responds with its minimum size. 6 | /// * The ``infinity`` proposal; the size responds with its maximum size. 7 | /// * The ``unspecified`` proposal; the size responds with its system default size. 8 | internal struct ProposedSize: Equatable, Sendable { 9 | 10 | /// The proposed horizontal size measured in points. 11 | /// 12 | /// A value of `nil` represents an unspecified width proposal. 13 | public var width: CGFloat? 14 | 15 | /// The proposed vertical size measured in points. 16 | /// 17 | /// A value of `nil` represents an unspecified height proposal. 18 | public var height: CGFloat? 19 | 20 | /// A size proposal that contains zero in both dimensions. 21 | public static var zero: ProposedSize { .init(width: 0, height: 0) } 22 | 23 | /// The proposed size with both dimensions left unspecified. 24 | /// 25 | /// Both dimensions contain `nil` in this size proposal. 26 | public static var unspecified: ProposedSize { .init(width: nil, height: nil) } 27 | 28 | /// A size proposal that contains infinity in both dimensions. 29 | /// 30 | /// Both dimensions contain .infinity in this size proposal. 31 | public static var infinity: ProposedSize { .init(width: .infinity, height: .infinity) } 32 | 33 | /// Creates a new proposed size using the specified width and height. 34 | /// 35 | /// - Parameters: 36 | /// - width: A proposed width in points. Use a value of `nil` to indicate 37 | /// that the width is unspecified for this proposal. 38 | /// - height: A proposed height in points. Use a value of `nil` to 39 | /// indicate that the height is unspecified for this proposal. 40 | @inlinable public init(width: CGFloat?, height: CGFloat?) { 41 | self.width = width 42 | self.height = height 43 | } 44 | 45 | /// Creates a new proposed size from a specified size. 46 | /// 47 | /// - Parameter size: A proposed size with dimensions measured in points. 48 | @inlinable public init(_ size: CGSize) { 49 | self.width = size.width 50 | self.height = size.height 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/DynamicType/DynamicType+Environment.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | private struct BackportDynamicTypeKey: EnvironmentKey { 5 | static var defaultValue: Backport.DynamicTypeSize = .large 6 | } 7 | 8 | @available(iOS, deprecated: 15) 9 | @available(tvOS, deprecated: 15) 10 | @available(macOS, deprecated: 12) 11 | @available(watchOS, deprecated: 8) 12 | public extension EnvironmentValues { 13 | 14 | /// Sets the Dynamic Type size within the view to the given value. 15 | /// 16 | /// As an example, you can set a Dynamic Type size in `ContentView` to be 17 | /// ``DynamicTypeSize/xLarge`` (this can be useful in previews to see your 18 | /// content at a different size) like this: 19 | /// 20 | /// ContentView() 21 | /// .backport.dynamicTypeSize(.xLarge) 22 | /// 23 | /// If a Dynamic Type size range is applied after setting a value, 24 | /// the value is limited by that range: 25 | /// 26 | /// ContentView() // Dynamic Type size will be .large 27 | /// .backport.dynamicTypeSize(...DynamicTypeSize.large) 28 | /// .backport.dynamicTypeSize(.xLarge) 29 | /// 30 | /// When limiting the Dynamic Type size, consider if adding a 31 | /// large content view with ``View/accessibilityShowsLargeContentViewer()`` 32 | /// would be appropriate. 33 | /// 34 | /// - Parameter size: The size to set for this view. 35 | /// 36 | /// - Returns: A view that sets the Dynamic Type size to the specified 37 | /// `size`. 38 | var backportDynamicTypeSize: Backport.DynamicTypeSize { 39 | get { .init(self[keyPath: \.sizeCategory]) } 40 | set { self[keyPath: \.sizeCategory] = newValue.sizeCategory } 41 | } 42 | } 43 | 44 | private struct DynamicTypeRangeKey: EnvironmentKey { 45 | static var defaultValue: Range.DynamicTypeSize> { 46 | .init(uncheckedBounds: (lower: .xSmall, upper: .accessibility5)) 47 | } 48 | } 49 | 50 | internal extension EnvironmentValues { 51 | var dynamicTypeRange: Range.DynamicTypeSize> { 52 | get { self[DynamicTypeRangeKey.self] } 53 | set { 54 | let current = self[DynamicTypeRangeKey.self] 55 | self[DynamicTypeRangeKey.self] = current.clamped(to: newValue) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/SensoryFeedback/SensoryFeedback+ViewModifier.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | import CoreHaptics 4 | 5 | internal struct SensoryFeedbackModifier: ViewModifier { 6 | var trigger: T 7 | var feedback: (_ oldValue: T, _ newValue: T) -> Backport.SensoryFeedback? 8 | var condition: (_ oldValue: T, _ newValue: T) -> Bool 9 | 10 | func body(content: Content) -> some View { 11 | content 12 | #if os(iOS) 13 | .backport.onChange(of: trigger) { oldValue, newValue in 14 | guard condition(oldValue, newValue) else { return } 15 | guard let feedback = feedback(oldValue, newValue) else { return } 16 | 17 | switch feedback.kind { 18 | case .success: 19 | let generator = UINotificationFeedbackGenerator() 20 | generator.prepare() 21 | generator.notificationOccurred(.success) 22 | case .warning: 23 | let generator = UINotificationFeedbackGenerator() 24 | generator.prepare() 25 | generator.notificationOccurred(.warning) 26 | case .error: 27 | let generator = UINotificationFeedbackGenerator() 28 | generator.prepare() 29 | generator.notificationOccurred(.error) 30 | case .selection: 31 | let generator = UISelectionFeedbackGenerator() 32 | generator.prepare() 33 | generator.selectionChanged() 34 | case .impact(let impact): 35 | let style: UIImpactFeedbackGenerator.FeedbackStyle 36 | 37 | switch impact { 38 | case .light: style = .light 39 | case .medium: style = .medium 40 | case .heavy: style = .heavy 41 | case .soft: style = .soft 42 | case .solid: style = .medium 43 | case .rigid: style = .rigid 44 | } 45 | 46 | let generator = UIImpactFeedbackGenerator(style: style) 47 | generator.prepare() 48 | generator.impactOccurred(intensity: feedback.intensity) 49 | } 50 | } 51 | #endif 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Toolbar/ToolbarBackground.swift: -------------------------------------------------------------------------------- 1 | import SwiftBackports 2 | 3 | #if os(iOS) 4 | import SwiftUI 5 | 6 | public extension Backport { 7 | struct ToolbarPlacement: Hashable { 8 | enum Placement: Hashable { 9 | var id: Self { self } 10 | case bottomBar 11 | case navigationbar 12 | case tabBar 13 | } 14 | 15 | let placement: Placement 16 | 17 | /// The bottom toolbar of an app. 18 | @available(macOS, unavailable) 19 | public static var bottomBar: ToolbarPlacement { 20 | .init(placement: .bottomBar) 21 | } 22 | 23 | /// The navigation bar of an app. 24 | @available(macOS, unavailable) 25 | public static var navigationBar: ToolbarPlacement { 26 | .init(placement: .navigationbar) 27 | } 28 | 29 | /// The tab bar of an app. 30 | @available(macOS, unavailable) 31 | public static var tabBar: ToolbarPlacement { 32 | .init(placement: .tabBar) 33 | } 34 | } 35 | } 36 | 37 | //@available(iOS, deprecated: 16) 38 | //@available(macOS, deprecated: 13) 39 | //@available(tvOS, unavailable) 40 | //@available(watchOS, unavailable) 41 | //public extension Backport where Wrapped: View { 42 | // func toolbarBackground(_ visibility: Backport.Visibility, for bars: Backport.ToolbarPlacement...) -> some View { 43 | // content 44 | // .modifier(ToolbarBackgroundModifier()) 45 | // .environment(\.toolbarVisibility, .init( 46 | // navigationBar: bars.contains(.navigationBar) ? visibility : nil, 47 | // bottomBar: bars.contains(.bottomBar) ? visibility : nil, 48 | // tabBar: bars.contains(.tabBar) ? visibility : nil) 49 | // ) 50 | // } 51 | // 52 | // func toolbarBackground(_ style: S, for bars: Backport.ToolbarPlacement...) -> some View where S: ShapeStyle & View { 53 | // content 54 | // .modifier(ToolbarBackgroundModifier()) 55 | // .environment(\.toolbarViews, .init( 56 | // navigationBar: bars.contains(.navigationBar) ? .init(style) : nil, 57 | // bottomBar: bars.contains(.bottomBar) ? .init(style) : nil, 58 | // tabBar: bars.contains(.tabBar) ? .init(style) : nil) 59 | // ) 60 | // } 61 | //} 62 | #endif 63 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Internal/VisualEffects/VisualEffect+macOS.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | #if os(macOS) 4 | internal struct VisualEffectBlur: View { 5 | private var material: NSVisualEffectView.Material 6 | private var blendingMode: NSVisualEffectView.BlendingMode 7 | private var state: NSVisualEffectView.State 8 | 9 | public init( 10 | material: NSVisualEffectView.Material = .headerView, 11 | blendingMode: NSVisualEffectView.BlendingMode = .withinWindow, 12 | state: NSVisualEffectView.State = .followsWindowActiveState 13 | ) { 14 | self.material = material 15 | self.blendingMode = blendingMode 16 | self.state = state 17 | } 18 | 19 | public var body: some View { 20 | Representable( 21 | material: material, 22 | blendingMode: blendingMode, 23 | state: state 24 | ).accessibility(hidden: true) 25 | } 26 | } 27 | 28 | // MARK: - Representable 29 | private extension VisualEffectBlur { 30 | struct Representable: NSViewRepresentable { 31 | var material: NSVisualEffectView.Material 32 | var blendingMode: NSVisualEffectView.BlendingMode 33 | var state: NSVisualEffectView.State 34 | 35 | func makeNSView(context: Context) -> NSVisualEffectView { 36 | context.coordinator.visualEffectView 37 | } 38 | 39 | func updateNSView(_ view: NSVisualEffectView, context: Context) { 40 | context.coordinator.update(material: material) 41 | context.coordinator.update(blendingMode: blendingMode) 42 | context.coordinator.update(state: state) 43 | } 44 | 45 | func makeCoordinator() -> Coordinator { 46 | Coordinator() 47 | } 48 | 49 | } 50 | 51 | class Coordinator { 52 | let visualEffectView = NSVisualEffectView() 53 | 54 | init() { 55 | visualEffectView.blendingMode = .withinWindow 56 | } 57 | 58 | func update(material: NSVisualEffectView.Material) { 59 | visualEffectView.material = material 60 | } 61 | 62 | func update(blendingMode: NSVisualEffectView.BlendingMode) { 63 | visualEffectView.blendingMode = blendingMode 64 | } 65 | 66 | func update(state: NSVisualEffectView.State) { 67 | visualEffectView.state = state 68 | } 69 | } 70 | } 71 | #endif 72 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ImageRenderer/Renderer.swift: -------------------------------------------------------------------------------- 1 | import SwiftBackports 2 | 3 | #if os(macOS) || os(iOS) 4 | import SwiftUI 5 | 6 | public extension Backport { 7 | final class ImageRenderer: ObservableObject where Content: View { 8 | public var content: Content 9 | public var label: String? 10 | public var proposedSize: ProposedViewSize = .unspecified 11 | public var scale: CGFloat = PlatformScreen.mainScreen.scale 12 | public var isOpaque: Bool = false 13 | public var colorMode: ColorRenderingMode = .nonLinear 14 | 15 | public init(content: Content) { 16 | self.content = content 17 | } 18 | } 19 | } 20 | 21 | public extension Backport.ImageRenderer { 22 | var cgImage: CGImage? { 23 | #if os(macOS) 24 | nsImage?.cgImage(forProposedRect: nil, context: .current, hints: nil) 25 | #else 26 | uiImage?.cgImage 27 | #endif 28 | } 29 | 30 | #if os(macOS) 31 | 32 | var nsImage: NSImage? { 33 | NSHostingController(rootView: content).view.snapshot 34 | } 35 | 36 | #else 37 | 38 | var uiImage: UIImage? { 39 | let controller = UIHostingController(rootView: content) 40 | let size = controller.view.intrinsicContentSize 41 | controller.view.bounds = CGRect(origin: .zero, size: size) 42 | controller.view.backgroundColor = .clear 43 | 44 | let format = UIGraphicsImageRendererFormat(for: controller.traitCollection) 45 | format.opaque = isOpaque 46 | format.scale = scale 47 | 48 | let renderer = UIGraphicsImageRenderer(size: size, format: format) 49 | 50 | let image = renderer.image { context in 51 | controller.view.drawHierarchy(in: context.format.bounds, afterScreenUpdates: true) 52 | } 53 | 54 | image.accessibilityLabel = label 55 | objectWillChange.send() 56 | 57 | return image 58 | } 59 | 60 | #endif 61 | } 62 | 63 | #if os(iOS) 64 | extension ColorRenderingMode { 65 | var range: UIGraphicsImageRendererFormat.Range { 66 | switch self { 67 | case .extendedLinear: return .extended 68 | case .linear: return .standard 69 | default: return .automatic 70 | } 71 | } 72 | } 73 | #endif 74 | 75 | #if os(macOS) 76 | private extension NSView { 77 | var snapshot: NSImage? { 78 | return NSImage(data: dataWithPDF(inside: bounds)) 79 | } 80 | } 81 | #endif 82 | #endif 83 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/ScrollView/ScrollKeyboardDismiss.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 16) 5 | @available(tvOS, deprecated: 16) 6 | @available(macOS, deprecated: 13) 7 | @available(watchOS, deprecated: 9) 8 | @MainActor 9 | extension Backport where Wrapped: View { 10 | 11 | /// Configures the behavior in which scrollable content interacts with 12 | /// the software keyboard. 13 | /// 14 | /// You use this modifier to customize how scrollable content interacts 15 | /// with the software keyboard. For example, you can specify a value of 16 | /// ``ScrollDismissesKeyboardMode/immediately`` to indicate that you 17 | /// would like scrollable content to immediately dismiss the keyboard if 18 | /// present when a scroll drag gesture begins. 19 | /// 20 | /// @State var text = "" 21 | /// 22 | /// ScrollView { 23 | /// TextField("Prompt", text: $text) 24 | /// ForEach(0 ..< 50) { index in 25 | /// Text("\(index)") 26 | /// .padding() 27 | /// } 28 | /// } 29 | /// .scrollDismissesKeyboard(.immediately) 30 | /// 31 | /// You can also use this modifier to customize the keyboard dismissal 32 | /// behavior for other kinds of scrollable views, like a ``List`` or a 33 | /// ``TextEditor``. 34 | /// 35 | /// By default, a ``TextEditor`` is interactive while other kinds 36 | /// of scrollable content always dismiss the keyboard on a scroll 37 | /// when linked against iOS 16 or later. Pass a value of 38 | /// ``ScrollDismissesKeyboardMode/never`` to indicate that scrollable 39 | /// content should never automatically dismiss the keyboard. 40 | /// 41 | /// - Parameter mode: The keyboard dismissal mode that scrollable content 42 | /// uses. 43 | /// 44 | /// - Returns: A view that uses the specified keyboard dismissal mode. 45 | public func scrollDismissesKeyboard(_ mode: Backport.ScrollDismissesKeyboardMode) -> some View { 46 | wrapped 47 | .environment(\.backportScrollDismissesKeyboardMode, mode) 48 | #if os(iOS) 49 | .sibling(forType: UIScrollView.self) { proxy in 50 | let scrollView = proxy.instance 51 | guard scrollView.keyboardDismissMode != mode.scrollViewDismissMode else { return } 52 | scrollView.keyboardDismissMode = mode.scrollViewDismissMode 53 | } 54 | #endif 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/ScrollView/ScrollEnabled.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 16) 5 | @available(tvOS, deprecated: 16) 6 | @available(macOS, deprecated: 13) 7 | @available(watchOS, deprecated: 9) 8 | @MainActor 9 | extension Backport where Wrapped: View { 10 | 11 | /// Disables or enables scrolling in scrollable views. 12 | /// 13 | /// Use this modifier to control whether a ``ScrollView`` can scroll: 14 | /// 15 | /// @State private var isScrollDisabled = false 16 | /// 17 | /// var body: some View { 18 | /// ScrollView { 19 | /// VStack { 20 | /// Toggle("Disable", isOn: $isScrollDisabled) 21 | /// MyContent() 22 | /// } 23 | /// } 24 | /// .backport.scrollDisabled(isScrollDisabled) 25 | /// } 26 | /// 27 | /// SwiftUI passes the disabled property through the environment, which 28 | /// means you can use this modifier to disable scrolling for all scroll 29 | /// views within a view hierarchy. In the following example, the modifier 30 | /// affects both scroll views: 31 | /// 32 | /// ScrollView { 33 | /// ForEach(rows) { row in 34 | /// ScrollView(.horizontal) { 35 | /// RowContent(row) 36 | /// } 37 | /// } 38 | /// } 39 | /// .backport.scrollDisabled(true) 40 | /// 41 | /// You can also use this modifier to disable scrolling for other kinds 42 | /// of scrollable views, like a ``List`` or a ``TextEditor``. 43 | /// 44 | /// - Parameter disabled: A Boolean that indicates whether scrolling is 45 | /// disabled. 46 | public func scrollDisabled(_ disabled: Bool) -> some View { 47 | wrapped 48 | .environment(\.backportIsScrollEnabled, !disabled) 49 | #if os(iOS) 50 | .any(forType: UIScrollView.self) { proxy in 51 | let scrollView = proxy.instance 52 | scrollView.isScrollEnabled = !disabled 53 | scrollView.alwaysBounceVertical = !disabled 54 | scrollView.alwaysBounceHorizontal = !disabled 55 | } 56 | #endif 57 | #if os(macOS) 58 | .any(forType: NSScrollView.self) { proxy in 59 | let scrollView = proxy.instance 60 | scrollView.hasHorizontalScroller = !disabled 61 | scrollView.hasVerticalScroller = !disabled 62 | } 63 | #endif 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/Presentation/CornerRadius.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 16.4) 5 | @available(tvOS, deprecated: 16.4) 6 | @available(macOS, deprecated: 13.3) 7 | @available(watchOS, deprecated: 9.4) 8 | @MainActor 9 | public extension Backport where Wrapped: View { 10 | @ViewBuilder 11 | func presentationCornerRadius(_ cornerRadius: CGFloat?) -> some View { 12 | #if os(iOS) 13 | if #available(iOS 15, *) { 14 | wrapped.background(Backport.Representable(cornerRadius: cornerRadius)) 15 | } else { 16 | wrapped 17 | } 18 | #else 19 | wrapped 20 | #endif 21 | } 22 | } 23 | 24 | #if os(iOS) 25 | @available(iOS 15, *) 26 | private extension Backport where Wrapped == Any { 27 | struct Representable: UIViewControllerRepresentable { 28 | let cornerRadius: CGFloat? 29 | 30 | func makeUIViewController(context: Context) -> Backport.Representable.Controller { 31 | Controller(cornerRadius: cornerRadius) 32 | } 33 | 34 | func updateUIViewController(_ controller: Backport.Representable.Controller, context: Context) { 35 | controller.update(cornerRadius: cornerRadius) 36 | } 37 | } 38 | } 39 | 40 | @available(iOS 15, *) 41 | private extension Backport.Representable { 42 | final class Controller: UIViewController, UISheetPresentationControllerDelegate { 43 | var cornerRadius: CGFloat? 44 | 45 | init(cornerRadius: CGFloat?) { 46 | self.cornerRadius = cornerRadius 47 | super.init(nibName: nil, bundle: nil) 48 | } 49 | 50 | required init?(coder: NSCoder) { 51 | fatalError("init(coder:) has not been implemented") 52 | } 53 | 54 | override func willMove(toParent parent: UIViewController?) { 55 | super.willMove(toParent: parent) 56 | update(cornerRadius: cornerRadius) 57 | } 58 | 59 | override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { 60 | super.willTransition(to: newCollection, with: coordinator) 61 | update(cornerRadius: cornerRadius) 62 | } 63 | 64 | func update(cornerRadius: CGFloat?) { 65 | self.cornerRadius = cornerRadius 66 | 67 | if let controller = parent?.sheetPresentationController { 68 | controller.animateChanges { 69 | controller.preferredCornerRadius = cornerRadius 70 | } 71 | } 72 | } 73 | } 74 | } 75 | #endif 76 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/ScrollView/Scroll+Environment.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 16) 5 | @available(tvOS, deprecated: 16) 6 | @available(macOS, deprecated: 13) 7 | @available(watchOS, deprecated: 9) 8 | extension EnvironmentValues { 9 | 10 | /// The visiblity to apply to scroll indicators of any 11 | /// vertically scrollable content. 12 | public var backportVerticalScrollIndicatorVisibility: Backport.ScrollIndicatorVisibility { 13 | get { self[BackportVerticalIndicatorKey.self] } 14 | set { self[BackportVerticalIndicatorKey.self] = newValue } 15 | } 16 | 17 | /// The visibility to apply to scroll indicators of any 18 | /// horizontally scrollable content. 19 | public var backportHorizontalScrollIndicatorVisibility: Backport.ScrollIndicatorVisibility { 20 | get { self[BackportHorizontalIndicatorKey.self] } 21 | set { self[BackportHorizontalIndicatorKey.self] = newValue } 22 | } 23 | 24 | /// The way that scrollable content interacts with the software keyboard. 25 | /// 26 | /// The default value is ``Backport.ScrollDismissesKeyboardMode.automatic``. Use the 27 | /// ``View.backport.scrollDismissesKeyboard(_:)`` modifier to configure this 28 | /// property. 29 | public var backportScrollDismissesKeyboardMode: Backport.ScrollDismissesKeyboardMode { 30 | get { self[BackportKeyboardDismissKey.self] } 31 | set { self[BackportKeyboardDismissKey.self] = newValue } 32 | } 33 | 34 | /// A Boolean value that indicates whether any scroll views associated 35 | /// with this environment allow scrolling to occur. 36 | /// 37 | /// The default value is `true`. Use the ``View.backport.scrollDisabled(_:)`` 38 | /// modifier to configure this property. 39 | public var backportIsScrollEnabled: Bool { 40 | get { self[BackportScrollEnabledKey.self] } 41 | set { self[BackportScrollEnabledKey.self] = newValue } 42 | } 43 | 44 | } 45 | 46 | private struct BackportVerticalIndicatorKey: EnvironmentKey { 47 | static var defaultValue: Backport.ScrollIndicatorVisibility = .automatic 48 | } 49 | 50 | private struct BackportHorizontalIndicatorKey: EnvironmentKey { 51 | static var defaultValue: Backport.ScrollIndicatorVisibility = .automatic 52 | } 53 | 54 | private struct BackportKeyboardDismissKey: EnvironmentKey { 55 | static var defaultValue: Backport.ScrollDismissesKeyboardMode = .automatic 56 | } 57 | 58 | private struct BackportScrollEnabledKey: EnvironmentKey { 59 | static var defaultValue: Bool = true 60 | } 61 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/ScrollView/ScrollDismissesKeyboardMode.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 16) 5 | @available(tvOS, deprecated: 16) 6 | @available(macOS, deprecated: 13) 7 | @available(watchOS, deprecated: 9) 8 | extension Backport where Wrapped == Any { 9 | 10 | /// The ways that scrollable content can interact with the software keyboard. 11 | /// 12 | /// Use this type in a call to the ``View.backport.scrollDismissesKeyboard(_:)`` 13 | /// modifier to specify the dismissal behavior of scrollable views. 14 | public struct ScrollDismissesKeyboardMode: Hashable, CustomStringConvertible { 15 | internal enum DismissMode: Hashable { 16 | case automatic 17 | case immediately 18 | case interactively 19 | case never 20 | } 21 | 22 | let dismissMode: DismissMode 23 | 24 | #if os(iOS) 25 | var scrollViewDismissMode: UIScrollView.KeyboardDismissMode { 26 | switch dismissMode { 27 | case .automatic: return .none 28 | case .immediately: return .onDrag 29 | case .interactively: return .interactive 30 | case .never: return .none 31 | } 32 | } 33 | #endif 34 | 35 | public var description: String { 36 | String(describing: dismissMode) 37 | } 38 | 39 | /// Determine the mode automatically based on the surrounding context. 40 | /// 41 | /// By default, a ``TextEditor`` is interactive while a ``List`` 42 | /// of scrollable content always dismiss the keyboard on a scroll 43 | public static var automatic: Self { .init(dismissMode: .automatic) } 44 | 45 | /// Dismiss the keyboard as soon as scrolling starts. 46 | public static var immediately: Self { .init(dismissMode: .immediately) } 47 | 48 | /// Enable people to interactively dismiss the keyboard as part of the 49 | /// scroll operation. 50 | /// 51 | /// The software keyboard's position tracks the gesture that drives the 52 | /// scroll operation if the gesture crosses into the keyboard's area of the 53 | /// display. People can dismiss the keyboard by scrolling it off the 54 | /// display, or reverse the direction of the scroll to cancel the dismissal. 55 | public static var interactively: Self { .init(dismissMode: .interactively) } 56 | 57 | /// Never dismiss the keyboard automatically as a result of scrolling. 58 | public static var never: Self { .init(dismissMode: .never) } 59 | 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/SwiftUIBackports.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/PhotosPicker/Core/PHFetchOptions.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import Foundation 3 | import PhotosUI 4 | import SwiftBackports 5 | 6 | internal extension PHFetchOptions { 7 | 8 | /// The maximum number of objects to include in the fetch result. 9 | func fetchLimit(_ fetchLimit: Int) -> Self { 10 | self.fetchLimit = fetchLimit 11 | return self 12 | } 13 | 14 | /// The set of source types for which to include assets in the fetch result. 15 | func includeAssetSourceTypes(_ sourceTypes: PHAssetSourceType) -> Self { 16 | self.includeAssetSourceTypes = sourceTypes 17 | return self 18 | } 19 | 20 | /// Determines whether the fetch result includes all assets from burst photo sequences. 21 | func includeHiddenAssets(_ includeHiddenAssets: Bool) -> Self { 22 | self.includeHiddenAssets = includeHiddenAssets 23 | return self 24 | } 25 | 26 | /// Determines whether the fetch result includes all assets from burst photo sequences. 27 | func includeAllBurstAssets(_ includeAllBurstAssets: Bool) -> Self { 28 | self.includeAllBurstAssets = includeAllBurstAssets 29 | return self 30 | } 31 | 32 | /// Appends the specified sort to the current list of descriptors. 33 | /// - Parameters: 34 | /// - keyPath: The keyPath sort by 35 | /// - ascending: If true, the results will be in ascending order 36 | func sort(by keyPath: KeyPath, ascending: Bool = true) -> Self { 37 | var descriptors = sortDescriptors ?? [] 38 | descriptors.append(NSSortDescriptor(keyPath: keyPath, ascending: ascending)) 39 | self.sortDescriptors = descriptors 40 | return self 41 | } 42 | 43 | /// Appends the specified predicate to the current list of predicates. 44 | /// - Parameter predicate: The predicate to append 45 | func filter(_ predicate: NSPredicate) -> Self { 46 | let predicates = [predicate, self.predicate].compactMap { $0 } 47 | self.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) 48 | return self 49 | } 50 | 51 | /// Replaces the sort descriptors. 52 | /// - Parameter sortDescriptors: The descriptors to sort results 53 | func sortDescriptors(_ sortDescriptors: [NSSortDescriptor]) -> Self { 54 | self.sortDescriptors = sortDescriptors 55 | return self 56 | } 57 | 58 | /// Replaces the predicate. 59 | /// - Parameter predicate: The predicate to filter results 60 | func predicate(_ predicate: NSPredicate) -> Self { 61 | self.predicate = predicate 62 | return self 63 | } 64 | 65 | } 66 | #endif 67 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/Backports.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 44 | 45 | 51 | 52 | 58 | 59 | 60 | 61 | 63 | 64 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ProgressView/ProgressViewConfiguration.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 14) 5 | @available(macOS, deprecated: 11) 6 | @available(tvOS, deprecated: 14.0) 7 | @available(watchOS, deprecated: 7.0) 8 | extension Backport where Wrapped == Any { 9 | /// The properties of a progress view instance. 10 | public struct ProgressViewStyleConfiguration { 11 | 12 | internal enum Kind { 13 | case circular 14 | case linear 15 | } 16 | 17 | /// A type-erased label describing the task represented by the progress 18 | /// view. 19 | public struct Label: View { 20 | let content: AnyView 21 | public var body: some View { content } 22 | init(content: Content) { 23 | self.content = .init(content) 24 | } 25 | } 26 | 27 | /// A type-erased label that describes the current value of a progress view. 28 | public struct CurrentValueLabel: View { 29 | let content: AnyView 30 | public var body: some View { content } 31 | init(content: Content) { 32 | self.content = .init(content) 33 | } 34 | } 35 | 36 | /// The completed fraction of the task represented by the progress view, 37 | /// from `0.0` (not yet started) to `1.0` (fully complete), or `nil` if the 38 | /// progress is indeterminate or relative to a date interval. 39 | public let fractionCompleted: Double? 40 | 41 | /// A view that describes the task represented by the progress view. 42 | /// 43 | /// If `nil`, then the task is self-evident from the surrounding context, 44 | /// and the style does not need to provide any additional description. 45 | /// 46 | /// If the progress view is defined using a `Progress` instance, then this 47 | /// label is equivalent to its `localizedDescription`. 48 | public var label: Label? = nil 49 | 50 | /// A view that describes the current value of a progress view. 51 | /// 52 | /// If `nil`, then the value of the progress view is either self-evident 53 | /// from the surrounding context or unknown, and the style does not need to 54 | /// provide any additional description. 55 | /// 56 | /// If the progress view is defined using a `Progress` instance, then this 57 | /// label is equivalent to its `localizedAdditionalDescription`. 58 | public var currentValueLabel: CurrentValueLabel? = nil 59 | 60 | internal let preferredKind: Kind 61 | internal var min: Double = 0 62 | internal var max: Double = 1 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item+Label.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) || os(iOS) 2 | import SwiftUI 3 | import SwiftBackports 4 | 5 | @available(iOS, deprecated: 16) 6 | @available(macOS, deprecated: 13) 7 | @available(watchOS, deprecated: 9) 8 | @available(tvOS, unavailable) 9 | public extension Backport.ShareLink where Wrapped == Any { 10 | init(_ title: S, item: String, subject: String? = nil, message: String? = nil) 11 | where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { 12 | self.label = Text(title) 13 | self.data = .init(item) 14 | self.subject = subject 15 | self.message = message 16 | self.preview = { .init($0) } 17 | } 18 | 19 | init(_ titleKey: LocalizedStringKey, item: String, subject: String? = nil, message: String? = nil) 20 | where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { 21 | self.label = Text(titleKey) 22 | self.data = .init(item) 23 | self.subject = subject 24 | self.message = message 25 | self.preview = { .init($0) } 26 | } 27 | 28 | init(_ title: Text, item: String, subject: String? = nil, message: String? = nil) 29 | where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { 30 | self.label = title 31 | self.data = .init(item) 32 | self.subject = subject 33 | self.message = message 34 | self.preview = { .init($0) } 35 | } 36 | 37 | init(_ title: S, item: URL, subject: String? = nil, message: String? = nil) 38 | where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { 39 | self.label = Text(title) 40 | self.data = .init(item) 41 | self.subject = subject 42 | self.message = message 43 | self.preview = { .init($0.absoluteString) } 44 | } 45 | 46 | init(_ titleKey: LocalizedStringKey, item: URL, subject: String? = nil, message: String? = nil) 47 | where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { 48 | self.label = Text(titleKey) 49 | self.data = .init(item) 50 | self.subject = subject 51 | self.message = message 52 | self.preview = { .init($0.absoluteString) } 53 | } 54 | 55 | init(_ title: Text, item: URL, subject: String? = nil, message: String? = nil) 56 | where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { 57 | self.label = title 58 | self.data = .init(item) 59 | self.subject = subject 60 | self.message = message 61 | self.preview = { .init($0.absoluteString) } 62 | } 63 | } 64 | #endif 65 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items+Label.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) || os(iOS) 2 | import SwiftUI 3 | import SwiftBackports 4 | 5 | @available(iOS, deprecated: 16) 6 | @available(macOS, deprecated: 13) 7 | @available(watchOS, deprecated: 9) 8 | @available(tvOS, unavailable) 9 | public extension Backport.ShareLink where Wrapped == Any { 10 | init(_ title: S, items: Data, subject: String? = nil, message: String? = nil) 11 | where PreviewImage == Never, PreviewIcon == Never, Data.Element == String, Label == DefaultShareLinkLabel { 12 | self.label = .init(title) 13 | self.data = items 14 | self.subject = subject 15 | self.message = message 16 | self.preview = { .init($0) } 17 | } 18 | 19 | init(_ titleKey: LocalizedStringKey, items: Data, subject: String? = nil, message: String? = nil) 20 | where PreviewImage == Never, PreviewIcon == Never, Data.Element == String, Label == DefaultShareLinkLabel { 21 | self.label = .init(titleKey) 22 | self.data = items 23 | self.subject = subject 24 | self.message = message 25 | self.preview = { .init($0) } 26 | } 27 | 28 | init(_ title: Text, items: Data, subject: String? = nil, message: String? = nil) 29 | where PreviewImage == Never, PreviewIcon == Never, Data.Element == String, Label == DefaultShareLinkLabel { 30 | self.label = .init(title) 31 | self.data = items 32 | self.subject = subject 33 | self.message = message 34 | self.preview = { .init($0) } 35 | } 36 | 37 | init(_ title: S, items: Data, subject: String? = nil, message: String? = nil) 38 | where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL, Label == DefaultShareLinkLabel { 39 | self.label = .init(title) 40 | self.data = items 41 | self.subject = subject 42 | self.message = message 43 | self.preview = { .init($0.absoluteString) } 44 | } 45 | 46 | init(_ titleKey: LocalizedStringKey, items: Data, subject: String? = nil, message: String? = nil) 47 | where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL, Label == DefaultShareLinkLabel { 48 | self.label = .init(titleKey) 49 | self.data = items 50 | self.subject = subject 51 | self.message = message 52 | self.preview = { .init($0.absoluteString) } 53 | } 54 | 55 | init(_ title: Text, items: Data, subject: String? = nil, message: String? = nil) 56 | where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL, Label == DefaultShareLinkLabel { 57 | self.label = .init(title) 58 | self.data = items 59 | self.subject = subject 60 | self.message = message 61 | self.preview = { .init($0.absoluteString) } 62 | } 63 | } 64 | #endif 65 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/PhotosPicker/UI/PhotosPickerView.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import SwiftUI 3 | import PhotosUI 4 | import SwiftBackports 5 | 6 | internal struct PhotosPickerView: View { 7 | @Environment(\.backportDismiss) private var dismiss 8 | @Binding var selection: [Backport.PhotosPickerItem] 9 | 10 | let filter: Backport.PHPickerFilter? 11 | let maxSelection: Int? 12 | let selectionBehavior: Backport.PhotosPickerSelectionBehavior 13 | let encoding: Backport.PhotosPickerItem.EncodingDisambiguationPolicy 14 | let library: PHPhotoLibrary 15 | 16 | private enum Source: String, CaseIterable, Identifiable { 17 | var id: Self { self } 18 | case photos = "Photos" 19 | case albums = "Albums" 20 | } 21 | 22 | @State private var source: Source = .photos 23 | 24 | var body: some View { 25 | NavigationView { 26 | List { 27 | 28 | } 29 | .navigationBarTitle(Text("Photos"), displayMode: .inline) 30 | .backport.toolbar { 31 | Backport.ToolbarItem(placement: .primaryAction) { 32 | Button("Add") { 33 | 34 | } 35 | .font(.body.weight(.semibold)) 36 | .disabled(selection.isEmpty) 37 | .opacity(maxSelection == 1 ? 0 : 1) 38 | } 39 | 40 | Backport.ToolbarItem(placement: .cancellationAction) { 41 | Button("Cancel") { 42 | selection = [] 43 | dismiss() 44 | } 45 | } 46 | 47 | Backport.ToolbarItem(placement: .principal) { 48 | Picker("", selection: $source) { 49 | ForEach(Source.allCases) { source in 50 | Text(source.rawValue) 51 | .tag(source) 52 | } 53 | } 54 | .pickerStyle(.segmented) 55 | .fixedSize() 56 | } 57 | 58 | Backport.ToolbarItem(placement: .bottomBar) { 59 | VStack { 60 | Text(selection.isEmpty ? "Select Items" : "Selected (\(selection.count))") 61 | .font(.subheadline.weight(.semibold)) 62 | 63 | Text("Select up to \(maxSelection ?? 1) items.") 64 | .foregroundColor(.secondary) 65 | .font(.footnote) 66 | } 67 | } 68 | } 69 | } 70 | .backport.interactiveDismissDisabled() 71 | .backport.onChange(of: source) { newValue in 72 | selection = source == .albums ? [.init(itemIdentifier: "")] : [] 73 | } 74 | } 75 | } 76 | #endif 77 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Label/Styles/TitleAndIconLabelStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 14) 5 | @available(macOS, deprecated: 11) 6 | @available(tvOS, deprecated: 14) 7 | @available(watchOS, deprecated: 7) 8 | extension Backport where Wrapped == Any { 9 | 10 | /// A label style that shows both the title and icon of the label using a 11 | /// system-standard layout. 12 | /// 13 | /// You can also use ``LabelStyle/titleAndIcon`` to construct this style. 14 | public struct TitleAndIconLabelStyle: BackportLabelStyle { 15 | 16 | /// Creates a label style that shows both the title and icon of the label 17 | /// using a system-standard layout. 18 | public init() { } 19 | 20 | /// Creates a view that represents the body of a label. 21 | /// 22 | /// The system calls this method for each ``Label`` instance in a view 23 | /// hierarchy where this style is the current label style. 24 | /// 25 | /// - Parameter configuration: The properties of the label. 26 | public func makeBody(configuration: Configuration) -> some View { 27 | HStack { 28 | configuration.icon 29 | configuration.title 30 | } 31 | } 32 | 33 | } 34 | 35 | } 36 | 37 | @available(iOS, deprecated: 14) 38 | @available(macOS, deprecated: 11) 39 | @available(tvOS, deprecated: 14) 40 | @available(watchOS, deprecated: 7) 41 | extension BackportLabelStyle where Self == Backport.TitleAndIconLabelStyle { 42 | 43 | /// A label style that shows both the title and icon of the label using a 44 | /// system-standard layout. 45 | /// 46 | /// In most cases, labels show both their title and icon by default. However, 47 | /// some containers might apply a different default label style to their 48 | /// content, such as only showing icons within toolbars on macOS and iOS. To 49 | /// opt in to showing both the title and the icon, you can apply the title 50 | /// and icon label style: 51 | /// 52 | /// Label("Lightning", systemImage: "bolt.fill") 53 | /// .labelStyle(.titleAndIcon) 54 | /// 55 | /// To apply the title and icon style to a group of labels, apply the style 56 | /// to the view hierarchy that contains the labels: 57 | /// 58 | /// VStack { 59 | /// Label("Rain", systemImage: "cloud.rain") 60 | /// Label("Snow", systemImage: "snow") 61 | /// Label("Sun", systemImage: "sun.max") 62 | /// } 63 | /// .labelStyle(.titleAndIcon) 64 | /// 65 | /// The relative layout of the title and icon is dependent on the context it 66 | /// is displayed in. In most cases, however, the label is arranged 67 | /// horizontally with the icon leading. 68 | public static var titleAndIcon: Self { .init() } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Quicklook/Quicklook+iOS.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | #if os(iOS) 5 | import QuickLook 6 | 7 | final class PreviewController: UIViewController, UIAdaptivePresentationControllerDelegate, QLPreviewControllerDelegate, QLPreviewControllerDataSource where Items: RandomAccessCollection, Items.Element == URL { 8 | var items: Items 9 | 10 | var selection: Binding { 11 | didSet { 12 | updateControllerLifecycle( 13 | from: oldValue.wrappedValue, 14 | to: selection.wrappedValue 15 | ) 16 | } 17 | } 18 | 19 | init(selection: Binding, in items: Items) { 20 | self.selection = selection 21 | self.items = items 22 | super.init(nibName: nil, bundle: nil) 23 | } 24 | 25 | required init?(coder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | private func updateControllerLifecycle(from oldValue: Items.Element?, to newValue: Items.Element?) { 30 | switch (oldValue, newValue) { 31 | case (.none, .some): 32 | presentController() 33 | case (.some, .some): 34 | updateController() 35 | case (.some, .none): 36 | dismissController() 37 | case (.none, .none): 38 | break 39 | } 40 | } 41 | 42 | private func presentController() { 43 | let controller = QLPreviewController(nibName: nil, bundle: nil) 44 | controller.dataSource = self 45 | controller.delegate = self 46 | self.present(controller, animated: true) 47 | self.updateController() 48 | } 49 | 50 | private func updateController() { 51 | let controller = presentedViewController as? QLPreviewController 52 | controller?.reloadData() 53 | let index = selection.wrappedValue.flatMap { items.firstIndex(of: $0) } 54 | controller?.currentPreviewItemIndex = items.distance(from: items.startIndex, to: index ?? items.startIndex) 55 | } 56 | 57 | private func dismissController() { 58 | DispatchQueue.main.async { 59 | self.selection.wrappedValue = nil 60 | } 61 | } 62 | 63 | func numberOfPreviewItems(in controller: QLPreviewController) -> Int { 64 | items.isEmpty ? 1 : items.count 65 | } 66 | 67 | func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { 68 | if items.isEmpty { 69 | return (selection.wrappedValue ?? URL(fileURLWithPath: "")) as NSURL 70 | } else { 71 | let index = items.index(items.startIndex, offsetBy: index) 72 | return items[index] as NSURL 73 | } 74 | } 75 | 76 | func previewControllerDidDismiss(_ controller: QLPreviewController) { 77 | dismissController() 78 | } 79 | } 80 | #endif 81 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ProgressView/Styles/CircularProgressViewStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 14) 5 | @available(macOS, deprecated: 11) 6 | @available(tvOS, deprecated: 14.0) 7 | @available(watchOS, deprecated: 7.0) 8 | extension Backport where Wrapped == Any { 9 | 10 | /// A progress view that visually indicates its progress using a circular gauge. 11 | /// 12 | /// You can also use ``ProgressViewStyle/circular`` to construct this style. 13 | public struct CircularProgressViewStyle: BackportProgressViewStyle { 14 | 15 | /// Creates a circular progress view style. 16 | public init() { } 17 | 18 | /// Creates a view representing the body of a progress view. 19 | /// 20 | /// - Parameter configuration: The properties of the progress view being 21 | /// created. 22 | /// 23 | /// The view hierarchy calls this method for each progress view where this 24 | /// style is the current progress view style. 25 | /// 26 | /// - Parameter configuration: The properties of the progress view, such as 27 | /// its preferred progress type. 28 | public func makeBody(configuration: Configuration) -> some View { 29 | VStack { 30 | #if !os(watchOS) 31 | CircularRepresentable(configuration: configuration) 32 | #endif 33 | 34 | configuration.label? 35 | .foregroundColor(.secondary) 36 | } 37 | } 38 | } 39 | 40 | } 41 | 42 | public extension BackportProgressViewStyle where Self == Backport.CircularProgressViewStyle { 43 | static var circular: Self { .init() } 44 | } 45 | 46 | #if os(macOS) 47 | private struct CircularRepresentable: NSViewRepresentable { 48 | let configuration: Backport.ProgressViewStyleConfiguration 49 | 50 | func makeNSView(context: Context) -> NSProgressIndicator { 51 | .init() 52 | } 53 | 54 | func updateNSView(_ view: NSProgressIndicator, context: Context) { 55 | if let value = configuration.fractionCompleted { 56 | view.doubleValue = value 57 | view.maxValue = configuration.max 58 | } 59 | 60 | view.isIndeterminate = configuration.fractionCompleted == nil 61 | view.style = .spinning 62 | view.isDisplayedWhenStopped = true 63 | view.startAnimation(nil) 64 | } 65 | } 66 | #elseif !os(watchOS) 67 | private struct CircularRepresentable: UIViewRepresentable { 68 | let configuration: Backport.ProgressViewStyleConfiguration 69 | 70 | func makeUIView(context: Context) -> UIActivityIndicatorView { 71 | .init(style: .medium) 72 | } 73 | 74 | func updateUIView(_ view: UIActivityIndicatorView, context: Context) { 75 | view.hidesWhenStopped = false 76 | view.startAnimating() 77 | } 78 | } 79 | #endif 80 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Label/LabelStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 14, message: "Use SwiftUI.LabelStyle instead") 5 | @available(macOS, deprecated: 11, message: "Use SwiftUI.LabelStyle instead") 6 | @available(tvOS, deprecated: 14, message: "Use SwiftUI.LabelStyle instead") 7 | @available(watchOS, deprecated: 7, message: "Use SwiftUI.LabelStyle instead") 8 | /// A type that applies a custom appearance to all labels within a view. 9 | /// 10 | /// To configure the current label style for a view hierarchy, use the 11 | /// ``View/labelStyle(_:)`` modifier. 12 | public protocol BackportLabelStyle { 13 | 14 | /// The properties of a label. 15 | typealias Configuration = Backport.LabelStyleConfiguration 16 | 17 | /// A view that represents the body of a label. 18 | associatedtype Body: View 19 | 20 | /// Creates a view that represents the body of a label. 21 | /// 22 | /// The system calls this method for each ``Label`` instance in a view 23 | /// hierarchy where this style is the current label style. 24 | /// 25 | /// - Parameter configuration: The properties of the label. 26 | @ViewBuilder func makeBody(configuration: Configuration) -> Body 27 | 28 | } 29 | 30 | @available(iOS, deprecated: 14, message: "Use View.labelStyle instead") 31 | @available(macOS, deprecated: 11, message: "Use View.labelStyle instead") 32 | @available(tvOS, deprecated: 14.0, message: "Use View.labelStyle instead") 33 | @available(watchOS, deprecated: 7.0, message: "Use View.labelStyle instead") 34 | @MainActor 35 | public extension Backport where Wrapped: View { 36 | func labelStyle(_ style: S) -> some View { 37 | wrapped.environment(\.backportLabelStyle, .init(style)) 38 | } 39 | } 40 | 41 | internal struct AnyLabelStyle: BackportLabelStyle { 42 | let _makeBody: (Backport.LabelStyleConfiguration) -> AnyView 43 | 44 | init(_ style: S) { 45 | _makeBody = { config in 46 | AnyView(style.makeBody(configuration: config)) 47 | } 48 | } 49 | 50 | func makeBody(configuration: Configuration) -> some View { 51 | _makeBody(configuration) 52 | } 53 | } 54 | 55 | private struct BackportLabelStyleEnvironmentKey: EnvironmentKey { 56 | static var defaultValue: AnyLabelStyle = .init(.automatic) 57 | } 58 | 59 | @available(iOS, deprecated: 14, message: "Use View.labelStyle instead") 60 | @available(macOS, deprecated: 11, message: "Use View.labelStyle instead") 61 | @available(tvOS, deprecated: 14.0, message: "Use View.labelStyle instead") 62 | @available(watchOS, deprecated: 7.0, message: "Use View.labelStyle instead") 63 | internal extension EnvironmentValues { 64 | var backportLabelStyle: AnyLabelStyle { 65 | get { self[BackportLabelStyleEnvironmentKey.self] } 66 | set { self[BackportLabelStyleEnvironmentKey.self] = newValue } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ShareLink/Transferable.swift: -------------------------------------------------------------------------------- 1 | import SwiftBackports 2 | 3 | #if os(macOS) || os(iOS) 4 | import SwiftUI 5 | 6 | /// TEMPORARY, DO NOT RELY ON THIS! 7 | /// 8 | /// - Note: This **will be removed** in an upcoming release, regardless of semantic versioning 9 | @available(iOS, deprecated: 16, message: "This **will be removed** in an upcoming release, regardless of semantic versioning") 10 | @available(macOS, deprecated: 13, message: "This **will be removed** in an upcoming release, regardless of semantic versioning") 11 | public protocol Shareable { 12 | var pathExtension: String { get } 13 | var itemProvider: NSItemProvider? { get } 14 | } 15 | 16 | internal struct ActivityItem where Data: RandomAccessCollection, Data.Element: Shareable { 17 | internal var data: Data 18 | } 19 | 20 | extension String: Shareable { 21 | public var pathExtension: String { "txt" } 22 | public var itemProvider: NSItemProvider? { 23 | do { 24 | let url = URL(fileURLWithPath: NSTemporaryDirectory()) 25 | .appendingPathComponent("\(UUID().uuidString)") 26 | .appendingPathExtension(pathExtension) 27 | try write(to: url, atomically: true, encoding: .utf8) 28 | return .init(contentsOf: url) 29 | } catch { 30 | return nil 31 | } 32 | } 33 | } 34 | 35 | extension URL: Shareable { 36 | public var itemProvider: NSItemProvider? { 37 | .init(contentsOf: self) 38 | } 39 | } 40 | 41 | extension Image: Shareable { 42 | public var pathExtension: String { "jpg" } 43 | public var itemProvider: NSItemProvider? { 44 | do { 45 | let url = URL(fileURLWithPath: NSTemporaryDirectory()) 46 | .appendingPathComponent("\(UUID().uuidString)") 47 | .appendingPathExtension(pathExtension) 48 | let renderer = Backport.ImageRenderer(content: self) 49 | 50 | #if os(iOS) 51 | let data = renderer.uiImage?.jpegData(compressionQuality: 0.8) 52 | #else 53 | let data = renderer.nsImage?.jpg(quality: 0.8) 54 | #endif 55 | 56 | try data?.write(to: url, options: .atomic) 57 | return .init(contentsOf: url) 58 | } catch { 59 | return nil 60 | } 61 | } 62 | } 63 | 64 | extension PlatformImage: Shareable { 65 | public var pathExtension: String { "jpg" } 66 | public var itemProvider: NSItemProvider? { 67 | do { 68 | let url = URL(fileURLWithPath: NSTemporaryDirectory()) 69 | .appendingPathComponent("\(UUID().uuidString)") 70 | .appendingPathExtension(pathExtension) 71 | let data = jpg(quality: 0.8) 72 | try data?.write(to: url, options: .atomic) 73 | return .init(contentsOf: url) 74 | } catch { 75 | return nil 76 | } 77 | } 78 | } 79 | #endif 80 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/System Overlays/SystemOverlays.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 16) 5 | @available(macOS, deprecated: 13) 6 | @available(tvOS, deprecated: 16) 7 | @available(watchOS, deprecated: 9) 8 | @MainActor 9 | public extension Backport where Wrapped: View { 10 | /// Sets the preferred visibility of the non-transient system views 11 | /// overlaying the app. 12 | /// 13 | /// Use this modifier if you would like to customise the immersive 14 | /// experience of your app by hiding or showing system overlays that may 15 | /// affect user experience. The following example hides every persistent 16 | /// system overlay. 17 | /// 18 | /// struct ImmersiveView: View { 19 | /// var body: some View { 20 | /// Text("Maximum immersion") 21 | /// .persistentSystemOverlays(.hidden) 22 | /// } 23 | /// } 24 | /// 25 | /// Note that this modifier only sets a preference and, ultimately the 26 | /// system will decide if it will honour it or not. 27 | /// 28 | /// These non-transient system views include: 29 | /// - The Home indicator 30 | /// 31 | /// - Parameter visibility: A value that indicates the visibility of the 32 | /// non-transient system views overlaying the app. 33 | func persistentSystemOverlays(_ visibility: Backport.Visibility) -> some View { 34 | wrapped.preference(key: PersistentSystemOverlaysPreferenceKey.self, value: visibility) 35 | } 36 | } 37 | 38 | private struct PersistentSystemOverlaysPreferenceKey: PreferenceKey { 39 | typealias Value = Backport.Visibility 40 | static var defaultValue: Value = .automatic 41 | static func reduce(value: inout Value, nextValue: () -> Value) { 42 | value = nextValue() 43 | } 44 | } 45 | 46 | #if os(iOS) 47 | private final class Representable: UIHostingController { 48 | init(rootView: Content) { 49 | let box = WeakBox() 50 | super.init( 51 | rootView: AnyView( 52 | rootView 53 | .onPreferenceChange(PersistentSystemOverlaysPreferenceKey.self) { visibility in 54 | box.value?.persistentSystemOverlaysHidden = visibility == .hidden 55 | } 56 | ) 57 | ) 58 | box.value = self 59 | } 60 | 61 | @MainActor required dynamic init?(coder aDecoder: NSCoder) { 62 | fatalError("init(coder:) has not been implemented") 63 | } 64 | 65 | private class WeakBox { 66 | weak var value: Representable? 67 | init() {} 68 | } 69 | 70 | private var persistentSystemOverlaysHidden = false { 71 | didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() } 72 | } 73 | 74 | override var prefersHomeIndicatorAutoHidden: Bool { 75 | persistentSystemOverlaysHidden 76 | } 77 | } 78 | #endif 79 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ProgressView/ProgressViewStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 14, message: "Use SwiftUI.ProgressViewStyle instead") 5 | @available(macOS, deprecated: 11, message: "Use SwiftUI.ProgressViewStyle instead") 6 | @available(tvOS, deprecated: 14.0, message: "Use SwiftUI.ProgressViewStyle instead") 7 | @available(watchOS, deprecated: 7.0, message: "Use SwiftUI.ProgressViewStyle instead") 8 | /// A type that applies standard interaction behavior to all progress views 9 | /// within a view hierarchy. 10 | /// 11 | /// To configure the current progress view style for a view hierarchy, use the 12 | /// ``View/progressViewStyle(_:)`` modifier. 13 | public protocol BackportProgressViewStyle { 14 | /// A type alias for the properties of a progress view instance. 15 | typealias Configuration = Backport.ProgressViewStyleConfiguration 16 | 17 | /// A view representing the body of a progress view. 18 | associatedtype Body: View 19 | 20 | /// Creates a view representing the body of a progress view. 21 | /// 22 | /// - Parameter configuration: The properties of the progress view being 23 | /// created. 24 | /// 25 | /// The view hierarchy calls this method for each progress view where this 26 | /// style is the current progress view style. 27 | /// 28 | /// - Parameter configuration: The properties of the progress view, such as 29 | /// its preferred progress type. 30 | @ViewBuilder func makeBody(configuration: Configuration) -> Body 31 | } 32 | 33 | @available(iOS, deprecated: 14, message: "Use View.progressViewStyle instead") 34 | @available(macOS, deprecated: 11, message: "Use View.progressViewStyle instead") 35 | @available(tvOS, deprecated: 14.0, message: "Use View.progressViewStyle instead") 36 | @available(watchOS, deprecated: 7.0, message: "Use View.progressViewStyle instead") 37 | @MainActor 38 | public extension Backport where Wrapped: View { 39 | func progressViewStyle(_ style: S) -> some View { 40 | wrapped.environment(\.backportProgressViewStyle, .init(style)) 41 | } 42 | } 43 | 44 | internal struct AnyProgressViewStyle: BackportProgressViewStyle { 45 | let _makeBody: (Backport.ProgressViewStyleConfiguration) -> AnyView 46 | 47 | init(_ style: S) { 48 | _makeBody = { config in 49 | AnyView(style.makeBody(configuration: config)) 50 | } 51 | } 52 | 53 | func makeBody(configuration: Configuration) -> some View { 54 | _makeBody(configuration) 55 | } 56 | } 57 | 58 | private struct BackportProgressViewStyleEnvironmentKey: EnvironmentKey { 59 | static var defaultValue: AnyProgressViewStyle? = nil 60 | } 61 | 62 | internal extension EnvironmentValues { 63 | var backportProgressViewStyle: AnyProgressViewStyle? { 64 | get { self[BackportProgressViewStyleEnvironmentKey.self] } 65 | set { self[BackportProgressViewStyleEnvironmentKey.self] = newValue } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/ScrollView/ScrollIndicators.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 16) 5 | @available(tvOS, deprecated: 16) 6 | @available(macOS, deprecated: 13) 7 | @available(watchOS, deprecated: 9) 8 | @MainActor 9 | extension Backport where Wrapped: View { 10 | 11 | /// Sets the visibility of scroll indicators within this view. 12 | /// 13 | /// Use this modifier to hide or show scroll indicators on scrollable 14 | /// content in views like a ``ScrollView``, ``List``, or ``TextEditor``. 15 | /// This modifier applies the prefered visibility to any 16 | /// scrollable content within a view hierarchy. 17 | /// 18 | /// ScrollView { 19 | /// VStack(alignment: .leading) { 20 | /// ForEach(0..<100) { 21 | /// Text("Row \($0)") 22 | /// } 23 | /// } 24 | /// } 25 | /// .backport.scrollIndicators(.hidden) 26 | /// 27 | /// Use the ``Backport.ScrollIndicatorVisibility.hidden`` value to indicate that you 28 | /// prefer that views never show scroll indicators along a given axis. 29 | /// Use ``Backport.ScrollIndicatorVisibility.visible`` when you prefer that 30 | /// views show scroll indicators. Depending on platform conventions, 31 | /// visible scroll indicators might only appear while scrolling. Pass 32 | /// ``Backport.ScrollIndicatorVisibility.automatic`` to allow views to 33 | /// decide whether or not to show their indicators. 34 | /// 35 | /// - Parameters: 36 | /// - visibility: The visibility to apply to scrollable views. 37 | /// - axes: The axes of scrollable views that the visibility applies to. 38 | /// 39 | /// - Returns: A view with the specified scroll indicator visibility. 40 | public func scrollIndicators(_ visibility: Backport.ScrollIndicatorVisibility, axes: Axis.Set = [.vertical]) -> some View { 41 | wrapped 42 | .environment(\.backportHorizontalScrollIndicatorVisibility, axes.contains(.horizontal) ? visibility : .automatic) 43 | .environment(\.backportVerticalScrollIndicatorVisibility, axes.contains(.vertical) ? visibility : .automatic) 44 | #if os(iOS) 45 | .sibling(forType: UIScrollView.self) { proxy in 46 | let scrollView = proxy.instance 47 | if axes.contains(.horizontal) { 48 | scrollView.showsHorizontalScrollIndicator = visibility.scrollViewVisible 49 | scrollView.alwaysBounceHorizontal = true 50 | } else { 51 | scrollView.alwaysBounceHorizontal = false 52 | } 53 | 54 | if axes.contains(.vertical) { 55 | scrollView.showsVerticalScrollIndicator = visibility.scrollViewVisible 56 | scrollView.alwaysBounceVertical = true 57 | } else { 58 | scrollView.alwaysBounceVertical = false 59 | } 60 | } 61 | #endif 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/TextEditor/TextEditor+Support.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | #if os(iOS) 4 | @available(iOS 15, *) 5 | extension DynamicTypeSize { 6 | var uiContentSizeCategory: UIContentSizeCategory { 7 | switch self { 8 | case .xSmall: return .extraSmall 9 | case .small: return .small 10 | case .medium: return .medium 11 | case .large: return .large 12 | case .xLarge: return .extraLarge 13 | case .xxLarge: return .extraExtraLarge 14 | case .xxxLarge: return .extraExtraExtraLarge 15 | case .accessibility1: return .accessibilityMedium 16 | case .accessibility2: return .accessibilityLarge 17 | case .accessibility3: return .accessibilityExtraLarge 18 | case .accessibility4: return .accessibilityExtraExtraLarge 19 | case .accessibility5: return .accessibilityExtraExtraExtraLarge 20 | default: return .large 21 | } 22 | } 23 | } 24 | 25 | extension LayoutDirection { 26 | var uiLayoutDirection: UITraitEnvironmentLayoutDirection { 27 | switch self { 28 | case .leftToRight: return .leftToRight 29 | case .rightToLeft: return .rightToLeft 30 | default: return .leftToRight 31 | } 32 | } 33 | } 34 | 35 | extension TextAlignment { 36 | var nsTextAlignment: NSTextAlignment { 37 | switch self { 38 | case .leading: return .left 39 | case .center: return .center 40 | case .trailing: return .right 41 | } 42 | } 43 | } 44 | 45 | extension ContentSizeCategory { 46 | var uiContentSizeCategory: UIContentSizeCategory { 47 | switch self { 48 | case .extraSmall: return .extraSmall 49 | case .small: return .small 50 | case .medium: return .medium 51 | case .large: return .large 52 | case .extraLarge: return .extraLarge 53 | case .extraExtraLarge: return .extraExtraLarge 54 | case .extraExtraExtraLarge: return .extraExtraExtraLarge 55 | case .accessibilityMedium: return .accessibilityMedium 56 | case .accessibilityLarge: return .accessibilityLarge 57 | case .accessibilityExtraLarge: return .accessibilityExtraLarge 58 | case .accessibilityExtraExtraLarge: return .accessibilityExtraExtraLarge 59 | case .accessibilityExtraExtraExtraLarge: return .accessibilityExtraExtraExtraLarge 60 | default: return .large 61 | } 62 | } 63 | } 64 | 65 | extension EnvironmentValues { 66 | var uiTraitCollection: UITraitCollection { 67 | var traits: [UITraitCollection] = [ 68 | .init(legibilityWeight: legibilityWeight?.uiLegibilityWeight ?? .unspecified), 69 | .init(layoutDirection: layoutDirection.uiLayoutDirection), 70 | ] 71 | 72 | if #available(iOS 15, *) { 73 | traits.append(.init(preferredContentSizeCategory: dynamicTypeSize.uiContentSizeCategory)) 74 | } else { 75 | traits.append(.init(preferredContentSizeCategory: sizeCategory.uiContentSizeCategory)) 76 | } 77 | 78 | return UITraitCollection(traitsFrom: traits) 79 | } 80 | } 81 | #endif 82 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/RequestReview/RequestReview.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import StoreKit 3 | import SwiftBackports 4 | 5 | #if os(iOS) || os(macOS) 6 | public extension EnvironmentValues { 7 | 8 | /// An instance that tells StoreKit to request an App Store rating or review from the user, if appropriate. 9 | /// 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. 10 | /// 11 | /// 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, don’t call it in response to a user action, such as a button tap. 12 | /// 13 | /// > 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. 14 | @MainActor var backportRequestReview: Backport.RequestReviewAction { .init() } 15 | 16 | } 17 | 18 | /// An instance that tells StoreKit to request an App Store rating or review from the user, if appropriate. 19 | /// 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. 20 | /// 21 | /// 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, don’t call it in response to a user action, such as a button tap. 22 | /// 23 | /// > 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. 24 | /// 25 | @available(iOS, deprecated: 16) 26 | @available(macOS, deprecated: 13) 27 | extension Backport where Wrapped == Any { 28 | @MainActor public struct RequestReviewAction { 29 | public func callAsFunction() { 30 | #if os(macOS) 31 | SKStoreReviewController.requestReview() 32 | #else 33 | if #available(iOS 14, *) { 34 | guard let scene = UIApplication.activeScene else { return } 35 | SKStoreReviewController.requestReview(in: scene) 36 | } else { 37 | SKStoreReviewController.requestReview() 38 | } 39 | #endif 40 | } 41 | } 42 | } 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/OpenURL/Safari.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | #if canImport(SafariServices) 4 | import SafariServices 5 | #endif 6 | 7 | @MainActor 8 | @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) 9 | public extension Backport.OpenURLAction.Result { 10 | static func safari(_ url: URL) -> Self { 11 | #if os(macOS) 12 | NSWorkspace.shared.open(url) 13 | #elseif os(iOS) 14 | let scene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene 15 | let window = scene?.windows.first { $0.isKeyWindow } 16 | guard let root = window?.rootViewController else { 17 | UIApplication.shared.open(url) 18 | return .handled 19 | } 20 | 21 | let controller = SFSafariViewController(url: url) 22 | if window?.traitCollection.horizontalSizeClass == .regular { 23 | controller.modalPresentationStyle = .pageSheet 24 | } 25 | 26 | root.present(controller, animated: true) 27 | #elseif os(tvOS) 28 | UIApplication.shared.open(url) 29 | #else 30 | WKExtension.shared().openSystemURL(url) 31 | #endif 32 | return .handled 33 | } 34 | 35 | #if os(iOS) && canImport(SafariServices) 36 | static func safari(_ url: URL, configure: (inout SafariConfiguration) -> Void) -> Self { 37 | let scene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene 38 | let window = scene?.windows.first { $0.isKeyWindow } 39 | 40 | guard let root = window?.rootViewController else { 41 | UIApplication.shared.open(url) 42 | return .handled 43 | } 44 | 45 | var config = SafariConfiguration() 46 | configure(&config) 47 | 48 | let configuration = SFSafariViewController.Configuration() 49 | configuration.barCollapsingEnabled = config.barCollapsingEnabled 50 | configuration.entersReaderIfAvailable = config.prefersReader 51 | 52 | let controller = SFSafariViewController(url: url, configuration: configuration) 53 | controller.preferredControlTintColor = UIColor(config.tintColor) 54 | controller.dismissButtonStyle = config.dismissStyle.buttonStyle 55 | 56 | if window?.traitCollection.horizontalSizeClass == .regular { 57 | controller.modalPresentationStyle = .pageSheet 58 | } 59 | 60 | root.present(controller, animated: true) 61 | return .handled 62 | } 63 | 64 | struct SafariConfiguration { 65 | public enum DismissStyle { 66 | case done 67 | case close 68 | case cancel 69 | 70 | internal var buttonStyle: SFSafariViewController.DismissButtonStyle { 71 | switch self { 72 | case .cancel: return .cancel 73 | case .close: return .close 74 | case .done: return.done 75 | } 76 | } 77 | } 78 | 79 | public var prefersReader: Bool = false 80 | public var barCollapsingEnabled: Bool = true 81 | public var dismissStyle: DismissStyle = .done 82 | public var tintColor: Color = .accentColor 83 | } 84 | #endif 85 | } 86 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/ScaledMetric/ScaledMetric.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 14) 5 | @available(macOS, deprecated: 11) 6 | @available(tvOS, deprecated: 14) 7 | @available(watchOS, deprecated: 7) 8 | extension Backport where Wrapped == Any { 9 | 10 | /// A dynamic property that scales a numeric value. 11 | @propertyWrapper 12 | public struct ScaledMetric: DynamicProperty where Value: BinaryFloatingPoint { 13 | 14 | @Environment(\.sizeCategory) var sizeCategory 15 | 16 | private let baseValue: Value 17 | 18 | #if os(iOS) || os(tvOS) 19 | private let metrics: UIFontMetrics 20 | #endif 21 | 22 | public var wrappedValue: Value { 23 | #if os(iOS) || os(tvOS) 24 | let traits = UITraitCollection(traitsFrom: [ 25 | UITraitCollection(preferredContentSizeCategory: UIContentSizeCategory(sizeCategory: sizeCategory)) 26 | ]) 27 | 28 | return Value(metrics.scaledValue(for: CGFloat(baseValue), compatibleWith: traits)) 29 | #else 30 | return baseValue 31 | #endif 32 | } 33 | 34 | #if os(iOS) || os(tvOS) 35 | /// Creates the scaled metric with an unscaled value using the default scaling. 36 | public init(baseValue: Value, metrics: UIFontMetrics) { 37 | self.baseValue = baseValue 38 | self.metrics = metrics 39 | } 40 | 41 | /// Creates the scaled metric with an unscaled value using the default scaling. 42 | public init(wrappedValue: Value) { 43 | self.init(baseValue: wrappedValue, metrics: UIFontMetrics(forTextStyle: .body)) 44 | } 45 | 46 | /// Creates the scaled metric with an unscaled value and a text style to scale relative to. 47 | public init(wrappedValue: Value, relativeTo textStyle: UIFont.TextStyle) { 48 | self.init(baseValue: wrappedValue, metrics: UIFontMetrics(forTextStyle: textStyle)) 49 | } 50 | #else 51 | /// Creates the scaled metric with an unscaled value using the default scaling. 52 | public init(wrappedValue: Value) { 53 | self.baseValue = wrappedValue 54 | } 55 | #endif 56 | 57 | } 58 | 59 | } 60 | 61 | #if os(iOS) || os(tvOS) 62 | private extension UIContentSizeCategory { 63 | init(sizeCategory: ContentSizeCategory?) { 64 | switch sizeCategory { 65 | case .accessibilityExtraExtraExtraLarge: self = .accessibilityExtraExtraExtraLarge 66 | case .accessibilityExtraExtraLarge: self = .accessibilityExtraExtraLarge 67 | case .accessibilityExtraLarge: self = .accessibilityExtraLarge 68 | case .accessibilityLarge: self = .accessibilityLarge 69 | case .accessibilityMedium: self = .accessibilityMedium 70 | case .extraExtraExtraLarge: self = .extraExtraExtraLarge 71 | case .extraExtraLarge: self = .extraExtraLarge 72 | case .extraLarge: self = .extraLarge 73 | case .extraSmall: self = .extraSmall 74 | case .large: self = .large 75 | case .medium: self = .medium 76 | case .small: self = .small 77 | default: self = .unspecified 78 | } 79 | } 80 | } 81 | #endif 82 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Internal/VisualEffects/VisualEffect+iOS.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | #if os(iOS) 4 | internal struct VisualEffectBlur: View { 5 | /// Defaults to .systemMaterial 6 | var blurStyle: UIBlurEffect.Style 7 | 8 | /// Defaults to nil 9 | var vibrancyStyle: UIVibrancyEffectStyle? 10 | 11 | var content: Content 12 | 13 | public init(blurStyle: UIBlurEffect.Style = .systemMaterial, vibrancyStyle: UIVibrancyEffectStyle? = nil, @ViewBuilder content: () -> Content) { 14 | self.blurStyle = blurStyle 15 | self.vibrancyStyle = vibrancyStyle 16 | self.content = content() 17 | } 18 | 19 | public var body: some View { 20 | Representable(blurStyle: blurStyle, vibrancyStyle: vibrancyStyle, content: content) 21 | .accessibility(hidden: Content.self == EmptyView.self) 22 | } 23 | } 24 | 25 | private extension VisualEffectBlur { 26 | struct Representable: UIViewRepresentable { 27 | var blurStyle: UIBlurEffect.Style 28 | var vibrancyStyle: UIVibrancyEffectStyle? 29 | var content: Content 30 | 31 | func makeUIView(context: Context) -> UIVisualEffectView { 32 | context.coordinator.blurView 33 | } 34 | 35 | func updateUIView(_ view: UIVisualEffectView, context: Context) { 36 | context.coordinator.update(content: content, blurStyle: blurStyle, vibrancyStyle: vibrancyStyle) 37 | } 38 | 39 | func makeCoordinator() -> Coordinator { 40 | Coordinator(content: content) 41 | } 42 | } 43 | } 44 | 45 | private extension VisualEffectBlur.Representable { 46 | class Coordinator { 47 | let blurView = UIVisualEffectView() 48 | let vibrancyView = UIVisualEffectView() 49 | let hostingController: UIHostingController 50 | 51 | init(content: Content) { 52 | hostingController = UIHostingController(rootView: content) 53 | hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 54 | hostingController.view.backgroundColor = nil 55 | blurView.contentView.addSubview(vibrancyView) 56 | 57 | blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 58 | vibrancyView.contentView.addSubview(hostingController.view) 59 | vibrancyView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 60 | } 61 | 62 | func update(content: Content, blurStyle: UIBlurEffect.Style, vibrancyStyle: UIVibrancyEffectStyle?) { 63 | hostingController.rootView = content 64 | 65 | let blurEffect = UIBlurEffect(style: blurStyle) 66 | blurView.effect = blurEffect 67 | 68 | if let vibrancyStyle = vibrancyStyle { 69 | vibrancyView.effect = UIVibrancyEffect(blurEffect: blurEffect, style: vibrancyStyle) 70 | } else { 71 | vibrancyView.effect = nil 72 | } 73 | 74 | hostingController.view.setNeedsDisplay() 75 | } 76 | } 77 | } 78 | 79 | extension VisualEffectBlur where Content == EmptyView { 80 | init(blurStyle: UIBlurEffect.Style = .systemMaterial) { 81 | self.init(blurStyle: blurStyle, vibrancyStyle: nil) { 82 | EmptyView() 83 | } 84 | } 85 | } 86 | #endif 87 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Quicklook/Quicklook+macOS.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | #if os(macOS) 5 | import QuickLook 6 | import QuickLookUI 7 | 8 | final class PreviewController: NSViewController, QLPreviewPanelDataSource, QLPreviewPanelDelegate where Items: RandomAccessCollection, Items.Element == URL { 9 | private let panel = QLPreviewPanel.shared()! 10 | private weak var windowResponder: NSResponder? 11 | 12 | var items: Items 13 | 14 | var selection: Binding { 15 | didSet { 16 | updateControllerLifecycle( 17 | from: oldValue.wrappedValue, 18 | to: selection.wrappedValue 19 | ) 20 | } 21 | } 22 | 23 | private func updateControllerLifecycle(from oldValue: Items.Element?, to newValue: Items.Element?) { 24 | switch (oldValue, newValue) { 25 | case (.none, .some): 26 | present() 27 | case (.some, .some): 28 | update() 29 | case (.some, .none): 30 | dismiss() 31 | case (.none, .none): 32 | break 33 | } 34 | } 35 | 36 | init(selection: Binding, in items: Items) { 37 | self.selection = selection 38 | self.items = items 39 | super.init(nibName: nil, bundle: nil) 40 | windowResponder = NSApp.mainWindow?.nextResponder 41 | } 42 | 43 | required init?(coder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | 47 | override func loadView() { 48 | view = .init(frame: .zero) 49 | } 50 | 51 | var isVisible: Bool { 52 | QLPreviewPanel.sharedPreviewPanelExists() && panel.isVisible 53 | } 54 | 55 | private func present() { 56 | NSApp.mainWindow?.nextResponder = self 57 | 58 | if isVisible { 59 | panel.updateController() 60 | let index = selection.wrappedValue.flatMap { items.firstIndex(of: $0) } 61 | panel.currentPreviewItemIndex = items.distance(from: items.startIndex, to: index ?? items.startIndex) 62 | } else { 63 | panel.makeKeyAndOrderFront(nil) 64 | } 65 | } 66 | 67 | private func update() { 68 | present() 69 | } 70 | 71 | private func dismiss() { 72 | selection.wrappedValue = nil 73 | } 74 | 75 | func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int { 76 | items.isEmpty ? 1 : items.count 77 | } 78 | 79 | func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! { 80 | if items.isEmpty { 81 | return selection.wrappedValue as? NSURL 82 | } else { 83 | let index = items.index(items.startIndex, offsetBy: index) 84 | return items[index] as NSURL 85 | } 86 | } 87 | 88 | override func acceptsPreviewPanelControl(_ panel: QLPreviewPanel!) -> Bool { 89 | return true 90 | } 91 | 92 | override func beginPreviewPanelControl(_ panel: QLPreviewPanel!) { 93 | panel.dataSource = self 94 | panel.reloadData() 95 | } 96 | 97 | override func endPreviewPanelControl(_ panel: QLPreviewPanel!) { 98 | panel.dataSource = nil 99 | dismiss() 100 | } 101 | 102 | } 103 | 104 | #endif 105 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Container/Group+Subviews.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Backport { 4 | public struct Group: View { 5 | private var content: Content 6 | 7 | public init( 8 | subviews view: Base, 9 | @ViewBuilder transform: @escaping (SubviewsCollection) -> Result 10 | ) where Content == GroupElementsOfContent, Base: View, Result: View { 11 | content = GroupElementsOfContent(view, subviews: transform) 12 | } 13 | 14 | public var body: some View { 15 | content 16 | } 17 | } 18 | 19 | public struct GroupElementsOfContent: View, Sendable where Subviews: View, Content: View { 20 | private var content: AnyView 21 | private var subviews: (SubviewsCollection) -> Content 22 | 23 | public init(_ source: Source, @ViewBuilder subviews: @escaping (SubviewsCollection) -> Content) { 24 | self.content = .init(source.variadic { subviews(SubviewsCollection(children: $0)) }) 25 | self.subviews = subviews 26 | } 27 | 28 | public var body: some View { 29 | content 30 | } 31 | } 32 | } 33 | 34 | extension Backport { 35 | public struct SubviewsCollection: RandomAccessCollection, View, Sendable { 36 | public typealias SubSequence = SubviewsCollectionSlice 37 | public typealias Iterator = IndexingIterator 38 | public typealias Indices = Range 39 | public typealias Index = Int 40 | public typealias Element = Subview 41 | 42 | public var startIndex: Int { children.startIndex } 43 | public var endIndex: Int { children.endIndex } 44 | 45 | public func index(before i: Int) -> Int { 46 | children.index(before: i) 47 | } 48 | 49 | public func index(after i: Int) -> Int { 50 | children.index(after: i) 51 | } 52 | 53 | public subscript(position: Int) -> Subview { 54 | .init(children[position]) 55 | } 56 | 57 | public subscript(bounds: Range) -> SubviewsCollectionSlice { 58 | .init(children: children[bounds]) 59 | } 60 | 61 | fileprivate let children: _VariadicView.Children 62 | 63 | public var body: some View { 64 | SwiftUI.ForEach(children, id: \.id) { $0.id($0.id) } 65 | } 66 | } 67 | 68 | public struct SubviewsCollectionSlice: RandomAccessCollection, View, Sendable { 69 | public typealias SubSequence = SubviewsCollectionSlice 70 | public typealias Element = Subview 71 | public typealias Iterator = IndexingIterator 72 | public typealias Indices = Range 73 | public typealias Index = Int 74 | 75 | public var startIndex: Int { children.startIndex } 76 | public var endIndex: Int { children.endIndex } 77 | 78 | public subscript(position: Int) -> Subview { 79 | .init(children[position]) 80 | } 81 | 82 | public subscript(bounds: Range) -> SubviewsCollectionSlice { 83 | .init(children: children[bounds]) 84 | } 85 | 86 | fileprivate let children: _VariadicView.Children.SubSequence 87 | 88 | public var body: some View { 89 | SwiftUI.ForEach(children, id: \.id) { $0 } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/UIHostingConfiguration/Cells/UITableViewCell.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | import ObjectiveC 4 | 5 | #if os(iOS) || os(tvOS) 6 | 7 | extension UITableViewCell { 8 | 9 | private static var configuredViewAssociatedKey: Void? 10 | 11 | fileprivate var configuredView: UIView? { 12 | get { objc_getAssociatedObject(self, &Self.configuredViewAssociatedKey) as? UIView } 13 | set { objc_setAssociatedObject(self, &Self.configuredViewAssociatedKey, newValue, .OBJC_ASSOCIATION_ASSIGN) } 14 | } 15 | } 16 | 17 | @available(iOS, deprecated: 16) 18 | @available(tvOS, deprecated: 16) 19 | @available(macOS, unavailable) 20 | @available(watchOS, unavailable) 21 | extension Backport where Wrapped: UITableViewCell { 22 | 23 | /// The current content configuration of the cell. 24 | /// 25 | /// Setting a content configuration replaces the existing contentView of the 26 | /// cell with a new content view instance from the configuration. 27 | public var contentConfiguration: BackportUIContentConfiguration? { 28 | get { nil } // we can't really support anything here, so for now we'll return nil 29 | nonmutating set { 30 | wrapped.configuredView?.removeFromSuperview() 31 | 32 | guard let configuration = newValue else { return } 33 | let contentView = wrapped.contentView 34 | 35 | let configuredView = configuration.makeContentView() 36 | configuredView.translatesAutoresizingMaskIntoConstraints = false 37 | 38 | wrapped.clipsToBounds = false 39 | contentView.clipsToBounds = false 40 | contentView.preservesSuperviewLayoutMargins = false 41 | contentView.addSubview(configuredView) 42 | 43 | let insets = Mirror(reflecting: configuration) 44 | .children.first(where: { $0.label == "insets" })?.value as? ProposedInsets 45 | ?? .unspecified 46 | 47 | insets.top.flatMap { contentView.directionalLayoutMargins.top = $0 } 48 | insets.bottom.flatMap { contentView.directionalLayoutMargins.bottom = $0 } 49 | insets.leading.flatMap { contentView.directionalLayoutMargins.leading = $0 } 50 | insets.trailing.flatMap { contentView.directionalLayoutMargins.trailing = $0 } 51 | 52 | NSLayoutConstraint.activate([ 53 | configuredView.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor), 54 | configuredView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), 55 | configuredView.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor), 56 | configuredView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), 57 | ]) 58 | 59 | var background: AnyView? { 60 | Mirror(reflecting: configuration) 61 | .children.first(where: { $0.label == "background" })?.value as? AnyView 62 | } 63 | 64 | background.flatMap { 65 | let host = UIHostingController(rootView: $0, ignoreSafeArea: true) 66 | wrapped.backgroundView = host.view 67 | } 68 | 69 | background.flatMap { 70 | let host = UIHostingController(rootView: $0, ignoreSafeArea: true) 71 | wrapped.selectedBackgroundView = host.view 72 | } 73 | 74 | wrapped.configuredView = configuredView 75 | } 76 | } 77 | 78 | } 79 | #endif 80 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/UIHostingConfiguration/Cells/UICollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | import ObjectiveC 4 | 5 | #if os(iOS) || os(tvOS) 6 | 7 | extension UICollectionViewCell { 8 | 9 | private static var configuredViewAssociatedKey: Void? 10 | 11 | fileprivate var configuredView: UIView? { 12 | get { objc_getAssociatedObject(self, &Self.configuredViewAssociatedKey) as? UIView } 13 | set { objc_setAssociatedObject(self, &Self.configuredViewAssociatedKey, newValue, .OBJC_ASSOCIATION_ASSIGN) } 14 | } 15 | } 16 | 17 | @available(iOS, deprecated: 16) 18 | @available(tvOS, deprecated: 16) 19 | @available(macOS, unavailable) 20 | @available(watchOS, unavailable) 21 | extension Backport where Wrapped: UICollectionViewCell { 22 | 23 | /// The current content configuration of the cell. 24 | /// 25 | /// Setting a content configuration replaces the existing contentView of the 26 | /// cell with a new content view instance from the configuration. 27 | public var contentConfiguration: BackportUIContentConfiguration? { 28 | get { nil } // we can't really support anything here, so for now we'll return nil 29 | nonmutating set { 30 | wrapped.configuredView?.removeFromSuperview() 31 | 32 | guard let configuration = newValue else { return } 33 | let contentView = wrapped.contentView 34 | 35 | let configuredView = configuration.makeContentView() 36 | configuredView.translatesAutoresizingMaskIntoConstraints = false 37 | 38 | wrapped.clipsToBounds = false 39 | contentView.clipsToBounds = false 40 | contentView.preservesSuperviewLayoutMargins = false 41 | contentView.addSubview(configuredView) 42 | 43 | let insets = Mirror(reflecting: configuration) 44 | .children.first(where: { $0.label == "insets" })?.value as? ProposedInsets 45 | ?? .unspecified 46 | 47 | insets.top.flatMap { contentView.directionalLayoutMargins.top = $0 } 48 | insets.bottom.flatMap { contentView.directionalLayoutMargins.bottom = $0 } 49 | insets.leading.flatMap { contentView.directionalLayoutMargins.leading = $0 } 50 | insets.trailing.flatMap { contentView.directionalLayoutMargins.trailing = $0 } 51 | 52 | NSLayoutConstraint.activate([ 53 | configuredView.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor), 54 | configuredView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), 55 | configuredView.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor), 56 | configuredView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), 57 | ]) 58 | 59 | var background: AnyView? { 60 | Mirror(reflecting: configuration) 61 | .children.first(where: { $0.label == "background" })?.value as? AnyView 62 | } 63 | 64 | background.flatMap { 65 | let host = UIHostingController(rootView: $0, ignoreSafeArea: true) 66 | wrapped.backgroundView = host.view 67 | } 68 | 69 | background.flatMap { 70 | let host = UIHostingController(rootView: $0, ignoreSafeArea: true) 71 | wrapped.selectedBackgroundView = host.view 72 | } 73 | 74 | wrapped.configuredView = configuredView 75 | } 76 | } 77 | 78 | } 79 | #endif 80 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/FocusState/FocusState.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | #if os(iOS) 4 | @available(iOS, deprecated: 15) 5 | public extension Backport where Wrapped == Any { 6 | /// A property wrapper type that can read and write a value that SwiftUI updates 7 | /// as the placement of focus within the scene changes. 8 | /// 9 | /// Use this property wrapper in conjunction with ``View/backport.focused(_:equals:)`` 10 | /// and ``View/backport.focused(_:)`` to 11 | /// describe views whose appearance and contents relate to the location of 12 | /// focus in the scene. When focus enters the modified view, the wrapped value 13 | /// of this property updates to match a given prototype value. Similarly, when 14 | /// focus leaves, the wrapped value of this property resets to `nil` 15 | /// or `false`. Setting the property's value programmatically has the reverse 16 | /// effect, causing focus to move to the view associated with the 17 | /// updated value. 18 | /// 19 | /// In the following example of a simple login screen, when the user presses the 20 | /// Sign In button and one of the fields is still empty, focus moves to that 21 | /// field. Otherwise, the sign-in process proceeds. 22 | /// 23 | /// struct LoginForm { 24 | /// enum Field: Hashable { 25 | /// case username 26 | /// case password 27 | /// } 28 | /// 29 | /// @State private var username = "" 30 | /// @State private var password = "" 31 | /// @Backport.FocusState private var focusedField: Field? 32 | /// 33 | /// var body: some View { 34 | /// Form { 35 | /// TextField("Username", text: $username) 36 | /// .backport.focused($focusedField, equals: .username) 37 | /// 38 | /// SecureField("Password", text: $password) 39 | /// .backport.focused($focusedField, equals: .password) 40 | /// 41 | /// Button("Sign In") { 42 | /// if username.isEmpty { 43 | /// focusedField = .username 44 | /// } else if password.isEmpty { 45 | /// focusedField = .password 46 | /// } else { 47 | /// handleLogin(username, password) 48 | /// } 49 | /// } 50 | /// } 51 | /// } 52 | /// } 53 | /// 54 | /// To allow for cases where focus is completely absent from a view tree, the 55 | /// wrapped value must be either an optional or a Boolean. Set the focus binding 56 | /// to `false` or `nil` as appropriate to remove focus from all bound fields. 57 | /// You can also use this to remove focus from a ``TextField`` and thereby 58 | /// dismiss the keyboard. 59 | /// 60 | @propertyWrapper 61 | struct FocusState: DynamicProperty where Value: Hashable { 62 | @State private var value: Value 63 | 64 | public var projectedValue: Binding { 65 | Binding( 66 | get: { wrappedValue }, 67 | set: { wrappedValue = $0 } 68 | ) 69 | } 70 | 71 | public var wrappedValue: Value { 72 | get { value } 73 | nonmutating set { value = newValue } 74 | } 75 | 76 | public init() where Value == Bool { 77 | _value = .init(initialValue: false) 78 | } 79 | 80 | public init() where Value == T?, T: Hashable { 81 | _value = .init(initialValue: nil) 82 | } 83 | } 84 | } 85 | #endif 86 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/PhotosPicker/PhotosPickerItem.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import SwiftUI 3 | import PhotosUI 4 | import SwiftBackports 5 | 6 | @available(iOS, deprecated: 16) 7 | public extension Backport { 8 | /// An item can contain multiple representations. Each representation has a corresponding content type. 9 | struct PhotosPickerItem: Equatable, Hashable { 10 | /// A policy that decides the encoding to use given a content type, if multiple encodings are available. 11 | public struct EncodingDisambiguationPolicy: Equatable, Hashable { 12 | internal let rawValue: UInt8 13 | 14 | /// Uses the best encoding determined by the system. This may change in future releases. 15 | public static let automatic: Self = .init(rawValue: 0) 16 | 17 | /// Uses the current encoding to avoid transcoding if possible. 18 | public static let current: Self = .init(rawValue: 1) 19 | 20 | /// Uses the most compatible encoding if possible, even if transcoding is required. 21 | public static let compatible: Self = .init(rawValue: 2) 22 | 23 | @available(iOS 14, *) 24 | internal var mode: PHPickerConfiguration.AssetRepresentationMode { 25 | switch self { 26 | case .current: return .current 27 | case .compatible: return .compatible 28 | default: return .automatic 29 | } 30 | } 31 | } 32 | 33 | /// The local identifier of the item. It will be `nil` if the Photos picker is created without a photo library. 34 | public let itemIdentifier: String? 35 | 36 | /// All supported content types of the item, in order of most preferred to least preferred. 37 | public let supportedContentTypes: [String] 38 | 39 | /// Creates an item without any representation using an identifier. 40 | /// 41 | /// - Parameters: 42 | /// - itemIdentifier: The local identifier of the item. 43 | public init(itemIdentifier: String) { 44 | self.itemIdentifier = itemIdentifier 45 | supportedContentTypes = [] 46 | } 47 | 48 | /// Loads an object using a representation of the item by matching content types. 49 | /// 50 | /// The representation corresponding to the first matching content type of the item will be used. 51 | /// If multiple encodings are available for the matched content type, the preferred item encoding provided to the Photos picker decides which encoding to use. 52 | /// An error will be thrown if the object doesn't support any of the supported content types of the item. 53 | /// 54 | /// - Parameters: 55 | /// - type: The actual type of the object. 56 | /// - Throws: The encountered error while loading the object. 57 | /// - Returns: The loaded object, or `nil` if no supported content type is found. 58 | /// 59 | /// - Note: Supported types are `Data`, `UIImage` or `Image` exclusively. Attempting to pass any other value here will result in an error. 60 | public func loadTransferable(type: T.Type) async throws -> T? { 61 | switch type { 62 | case is Image.Type: 63 | fatalError() 64 | case is UIImage.Type: 65 | fatalError() 66 | case is Data.Type: 67 | fatalError() 68 | default: 69 | throw PhotoError() 70 | } 71 | } 72 | 73 | private struct PhotoError: LocalizedError { 74 | var errorDescription: String? { 75 | "Could not load photo as \(T.self)" 76 | } 77 | } 78 | } 79 | } 80 | #endif 81 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/Presentation/DragIndicator.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 16) 5 | @available(tvOS, deprecated: 16) 6 | @available(macOS, deprecated: 13) 7 | @available(watchOS, deprecated: 9) 8 | @MainActor 9 | public extension Backport where Wrapped: View { 10 | 11 | /// Sets the visibility of the drag indicator on top of a sheet. 12 | /// 13 | /// You can show a drag indicator when it isn't apparent that a 14 | /// sheet can resize or when the sheet can't dismiss interactively. 15 | /// 16 | /// struct ContentView: View { 17 | /// @State private var showSettings = false 18 | /// 19 | /// var body: some View { 20 | /// Button("View Settings") { 21 | /// showSettings = true 22 | /// } 23 | /// .sheet(isPresented: $showSettings) { 24 | /// SettingsView() 25 | /// .presentationDetents:([.medium, .large]) 26 | /// .presentationDragIndicator(.visible) 27 | /// } 28 | /// } 29 | /// } 30 | /// 31 | /// - Parameter visibility: The preferred visibility of the drag indicator. 32 | @ViewBuilder 33 | func presentationDragIndicator(_ visibility: Backport.Visibility) -> some View { 34 | #if os(iOS) 35 | if #available(iOS 15, *) { 36 | wrapped.background(Backport.Representable(visibility: visibility)) 37 | } else { 38 | wrapped 39 | } 40 | #else 41 | wrapped 42 | #endif 43 | } 44 | 45 | } 46 | 47 | #if os(iOS) 48 | @available(iOS 15, *) 49 | private extension Backport where Wrapped == Any { 50 | struct Representable: UIViewControllerRepresentable { 51 | let visibility: Backport.Visibility 52 | 53 | func makeUIViewController(context: Context) -> Backport.Representable.Controller { 54 | Controller(visibility: visibility) 55 | } 56 | 57 | func updateUIViewController(_ controller: Backport.Representable.Controller, context: Context) { 58 | controller.update(visibility: visibility) 59 | } 60 | } 61 | } 62 | 63 | @available(iOS 15, *) 64 | private extension Backport.Representable { 65 | final class Controller: UIViewController { 66 | 67 | var visibility: Backport.Visibility 68 | 69 | init(visibility: Backport.Visibility) { 70 | self.visibility = visibility 71 | super.init(nibName: nil, bundle: nil) 72 | } 73 | 74 | required init?(coder: NSCoder) { 75 | fatalError("init(coder:) has not been implemented") 76 | } 77 | 78 | override func willMove(toParent parent: UIViewController?) { 79 | super.willMove(toParent: parent) 80 | update(visibility: visibility) 81 | } 82 | 83 | override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { 84 | super.willTransition(to: newCollection, with: coordinator) 85 | update(visibility: visibility) 86 | } 87 | 88 | func update(visibility: Backport.Visibility) { 89 | self.visibility = visibility 90 | 91 | if let controller = parent?.sheetPresentationController { 92 | controller.animateChanges { 93 | controller.prefersGrabberVisible = visibility == .visible 94 | controller.prefersScrollingExpandsWhenScrolledToEdge = true 95 | } 96 | } 97 | } 98 | 99 | } 100 | } 101 | #endif 102 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/ProgressView/Styles/LinearProgressViewStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 14) 5 | @available(macOS, deprecated: 11) 6 | @available(tvOS, deprecated: 14.0) 7 | @available(watchOS, deprecated: 7.0) 8 | extension Backport where Wrapped == Any { 9 | 10 | /// A progress view that visually indicates its progress using a horizontal bar. 11 | /// 12 | /// You can also use ``ProgressViewStyle/linear`` to construct this style. 13 | public struct LinearProgressViewStyle: BackportProgressViewStyle { 14 | 15 | /// Creates a linear progress view style. 16 | public init() { } 17 | 18 | /// Creates a view representing the body of a progress view. 19 | /// 20 | /// - Parameter configuration: The properties of the progress view being 21 | /// created. 22 | /// 23 | /// The view hierarchy calls this method for each progress view where this 24 | /// style is the current progress view style. 25 | /// 26 | /// - Parameter configuration: The properties of the progress view, such as 27 | /// its preferred progress type. 28 | public func makeBody(configuration: Configuration) -> some View { 29 | #if os(macOS) 30 | VStack(alignment: .leading, spacing: 0) { 31 | configuration.label 32 | .foregroundColor(.primary) 33 | 34 | LinearRepresentable(configuration: configuration) 35 | 36 | configuration.currentValueLabel 37 | .foregroundColor(.secondary) 38 | } 39 | .controlSize(.small) 40 | #else 41 | VStack(alignment: .leading, spacing: 5) { 42 | if configuration.fractionCompleted == nil { 43 | CircularProgressViewStyle().makeBody(configuration: configuration) 44 | } else { 45 | configuration.label? 46 | .foregroundColor(.primary) 47 | 48 | #if !os(watchOS) 49 | LinearRepresentable(configuration: configuration) 50 | #endif 51 | 52 | configuration.currentValueLabel? 53 | .foregroundColor(.secondary) 54 | .font(.caption) 55 | } 56 | } 57 | #endif 58 | } 59 | } 60 | 61 | } 62 | 63 | public extension BackportProgressViewStyle where Self == Backport.LinearProgressViewStyle { 64 | static var linear: Self { .init() } 65 | } 66 | 67 | #if os(macOS) 68 | private struct LinearRepresentable: NSViewRepresentable { 69 | let configuration: Backport.ProgressViewStyleConfiguration 70 | 71 | func makeNSView(context: Context) -> NSProgressIndicator { 72 | .init() 73 | } 74 | 75 | func updateNSView(_ view: NSProgressIndicator, context: Context) { 76 | if let value = configuration.fractionCompleted { 77 | view.doubleValue = value 78 | view.maxValue = configuration.max 79 | 80 | view.display() 81 | } 82 | 83 | view.style = .bar 84 | view.isIndeterminate = configuration.fractionCompleted == nil 85 | view.isDisplayedWhenStopped = true 86 | view.startAnimation(nil) 87 | } 88 | } 89 | #elseif !os(watchOS) 90 | private struct LinearRepresentable: UIViewRepresentable { 91 | let configuration: Backport.ProgressViewStyleConfiguration 92 | 93 | func makeUIView(context: Context) -> UIProgressView { 94 | .init(progressViewStyle: .default) 95 | } 96 | 97 | func updateUIView(_ view: UIProgressView, context: Context) { 98 | view.progress = Float(configuration.fractionCompleted ?? 0) 99 | } 100 | } 101 | #endif 102 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Toolbar/ToolbarBackgroundModifier.swift: -------------------------------------------------------------------------------- 1 | import SwiftBackports 2 | 3 | #if os(iOS) 4 | import SwiftUI 5 | 6 | internal struct ToolbarBackgroundModifier: ViewModifier { 7 | @Environment(\.toolbarViews) private var toolbarViews 8 | @Environment(\.toolbarVisibility) private var toolbarVisibility 9 | 10 | @Backport.StateObject private var wrapper: ControllerWrapper = .init() 11 | 12 | func body(content: Content) -> some View { 13 | content 14 | .ancestor(forType: UIViewController.self) { proxy in 15 | wrapper.controller = proxy.instance 16 | } 17 | .backport.task { 18 | updateNavigationBar() 19 | updateBottomBar() 20 | updateTabBar() 21 | } 22 | } 23 | 24 | private func updateNavigationBar() { 25 | guard toolbarVisibility.navigationBar != nil || toolbarViews.navigationBar != nil else { return } 26 | guard let bar = wrapper.controller?.navigationController?.navigationBar else { return } 27 | let appearance = UINavigationBarAppearance() 28 | 29 | if let visibilty = toolbarVisibility.navigationBar, visibilty == .hidden { 30 | appearance.configureWithTransparentBackground() 31 | } else if let view = toolbarViews.navigationBar { 32 | appearance.backgroundImage = Backport.ImageRenderer( 33 | content: view 34 | .frame(width: bar.bounds.width, height: bar.bounds.height) 35 | .edgesIgnoringSafeArea(.all) 36 | ).uiImage 37 | } else { 38 | appearance.configureWithDefaultBackground() 39 | } 40 | 41 | bar.standardAppearance = appearance 42 | bar.compactAppearance = appearance 43 | bar.scrollEdgeAppearance = appearance 44 | if #available(iOS 15.0, *) { 45 | bar.compactScrollEdgeAppearance = appearance 46 | } 47 | } 48 | 49 | private func updateBottomBar() { 50 | guard toolbarVisibility.bottomBar != nil || toolbarViews.bottomBar != nil else { return } 51 | guard let bar = wrapper.controller?.navigationController?.toolbar else { return } 52 | let appearance = UIToolbarAppearance() 53 | 54 | if let visibilty = toolbarVisibility.bottomBar, visibilty == .hidden { 55 | appearance.configureWithTransparentBackground() 56 | } else { 57 | appearance.configureWithDefaultBackground() 58 | } 59 | 60 | bar.standardAppearance = appearance 61 | bar.compactAppearance = appearance 62 | if #available(iOS 15.0, *) { 63 | bar.scrollEdgeAppearance = appearance 64 | } 65 | if #available(iOS 15.0, *) { 66 | bar.compactScrollEdgeAppearance = appearance 67 | } 68 | } 69 | 70 | private func updateTabBar() { 71 | guard toolbarVisibility.tabBar != nil || toolbarViews.tabBar != nil else { return } 72 | guard let bar = wrapper.controller?.tabBarController?.tabBar else { return } 73 | let appearance = UITabBarAppearance() 74 | 75 | if let visibilty = toolbarVisibility.tabBar, visibilty == .hidden { 76 | appearance.configureWithTransparentBackground() 77 | } else if let view = toolbarViews.tabBar { 78 | appearance.backgroundImage = Backport.ImageRenderer( 79 | content: view 80 | .frame(width: bar.bounds.width, height: bar.bounds.height) 81 | .edgesIgnoringSafeArea(.all) 82 | ).uiImage 83 | } else { 84 | appearance.configureWithDefaultBackground() 85 | } 86 | 87 | bar.standardAppearance = appearance 88 | if #available(iOS 15.0, *) { 89 | bar.scrollEdgeAppearance = appearance 90 | } 91 | } 92 | } 93 | 94 | private final class ControllerWrapper: ObservableObject { 95 | weak var controller: UIViewController? 96 | } 97 | #endif 98 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/Presentation/InteractiveDetent.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 16) 5 | @available(tvOS, deprecated: 16) 6 | @available(macOS, deprecated: 13) 7 | @available(watchOS, deprecated: 9) 8 | @MainActor 9 | public extension Backport where Wrapped: View { 10 | 11 | /// Removes dimming from detents higher (and including) the provided identifier 12 | /// 13 | /// This has two affects on dentents higher than the identifier provided: 14 | /// 1. Touches will passthrough to the views below the sheet. 15 | /// 2. Touches will no longer dismiss the sheet automatically when tapping outside of the sheet. 16 | /// 17 | /// ``` 18 | /// struct ContentView: View { 19 | /// @State private var showSettings = false 20 | /// 21 | /// var body: some View { 22 | /// Button("View Settings") { 23 | /// showSettings = true 24 | /// } 25 | /// .sheet(isPresented: $showSettings) { 26 | /// SettingsView() 27 | /// .presentationDetents:([.medium, .large]) 28 | /// .presentationUndimmed(from: .medium) 29 | /// } 30 | /// } 31 | /// } 32 | /// ``` 33 | /// 34 | /// - Parameter identifier: The identifier of the largest detent that is not dimmed. 35 | @ViewBuilder 36 | @available(iOS, deprecated: 13, message: "Please use backport.presentationDetents(_:selection:largestUndimmedDetent:)") 37 | func presentationUndimmed(from identifier: Backport.PresentationDetent.Identifier?) -> some View { 38 | #if os(iOS) 39 | if #available(iOS 15, *) { 40 | wrapped.background(Backport.Representable(identifier: identifier)) 41 | } else { 42 | wrapped 43 | } 44 | #else 45 | wrapped 46 | #endif 47 | } 48 | 49 | } 50 | 51 | #if os(iOS) 52 | @available(iOS 15, *) 53 | private extension Backport where Wrapped == Any { 54 | struct Representable: UIViewControllerRepresentable { 55 | let identifier: Backport.PresentationDetent.Identifier? 56 | 57 | func makeUIViewController(context: Context) -> Backport.Representable.Controller { 58 | Controller(identifier: identifier) 59 | } 60 | 61 | func updateUIViewController(_ controller: Backport.Representable.Controller, context: Context) { 62 | controller.update(identifier: identifier) 63 | } 64 | } 65 | } 66 | 67 | @available(iOS 15, *) 68 | private extension Backport.Representable { 69 | final class Controller: UIViewController { 70 | 71 | var identifier: Backport.PresentationDetent.Identifier? 72 | 73 | init(identifier: Backport.PresentationDetent.Identifier?) { 74 | self.identifier = identifier 75 | super.init(nibName: nil, bundle: nil) 76 | } 77 | 78 | required init?(coder: NSCoder) { 79 | fatalError("init(coder:) has not been implemented") 80 | } 81 | 82 | override func willMove(toParent parent: UIViewController?) { 83 | super.willMove(toParent: parent) 84 | update(identifier: identifier) 85 | } 86 | 87 | func update(identifier: Backport.PresentationDetent.Identifier?) { 88 | self.identifier = identifier 89 | 90 | if let controller = parent?.sheetPresentationController { 91 | controller.animateChanges { 92 | controller.presentingViewController.view.tintAdjustmentMode = .normal 93 | controller.largestUndimmedDetentIdentifier = identifier.flatMap { 94 | .init(rawValue: $0.rawValue) 95 | } 96 | } 97 | } 98 | } 99 | 100 | } 101 | } 102 | #endif 103 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/Quicklook/Quicklook.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | #if canImport(QuickLook) 5 | import QuickLook 6 | #endif 7 | 8 | @available(iOS, deprecated: 14, message: "Use QuickLook framework instead") 9 | @available(macOS, deprecated: 11, message: "Use QuickLook framework instead") 10 | @available(tvOS, unavailable) 11 | @available(watchOS, unavailable) 12 | extension Backport where Wrapped: View { 13 | 14 | /// Presents a Quick Look preview of the URLs you provide. 15 | /// 16 | /// The Quick Look preview appears when you set the binding to a non-`nil` item. 17 | /// When you set the item back to `nil`, Quick Look dismisses the preview. 18 | /// If the value of the selection binding isn’t contained in the items collection, Quick Look treats it the same as a `nil` selection. 19 | /// 20 | /// Quick Look updates the value of the selection binding to match the URL of the file the user is previewing. 21 | /// Upon dismissal by the user, Quick Look automatically sets the item binding to `nil`. 22 | /// 23 | /// - Parameters: 24 | /// - selection: A to an element that’s part of the items collection. This is the URL that you currently want to preview. 25 | /// - items: A collection of URLs to preview. 26 | /// 27 | /// - Returns: A view that presents the preview of the contents of the URL. 28 | public func quickLookPreview(_ selection: Binding, in items: Items) -> some View where Items: RandomAccessCollection, Items.Element == URL { 29 | #if os(iOS) || os(macOS) 30 | wrapped.background(QuicklookSheet(selection: selection, items: items)) 31 | #else 32 | wrapped 33 | #endif 34 | } 35 | 36 | 37 | /// Presents a Quick Look preview of the contents of a single URL. 38 | /// 39 | /// The Quick Look preview appears when you set the binding to a non-`nil` item. 40 | /// When you set the item back to `nil`, Quick Look dismisses the preview. 41 | /// 42 | /// Upon dismissal by the user, Quick Look automatically sets the item binding to `nil`. 43 | /// Quick Look displays the preview when a non-`nil` item is set. 44 | /// Set `item` to `nil` to dismiss the preview. 45 | /// 46 | /// - Parameters: 47 | /// - item: A to a URL that should be previewed. 48 | /// 49 | /// - Returns: A view that presents the preview of the contents of the URL. 50 | public func quickLookPreview(_ item: Binding) -> some View { 51 | #if os(iOS) || os(macOS) 52 | wrapped.background(QuicklookSheet(selection: item, items: [item.wrappedValue].compactMap { $0 })) 53 | #else 54 | wrapped 55 | #endif 56 | } 57 | 58 | } 59 | 60 | #if os(macOS) 61 | import QuickLookUI 62 | 63 | private struct QuicklookSheet: NSViewControllerRepresentable where Items: RandomAccessCollection, Items.Element == URL { 64 | let selection: Binding 65 | let items: Items 66 | 67 | func makeNSViewController(context: Context) -> PreviewController { 68 | .init(selection: selection, in: items) 69 | } 70 | 71 | func updateNSViewController(_ controller: PreviewController, context: Context) { 72 | controller.selection = selection 73 | controller.items = items 74 | } 75 | } 76 | 77 | #elseif os(iOS) 78 | 79 | private struct QuicklookSheet: UIViewControllerRepresentable where Items: RandomAccessCollection, Items.Element == URL { 80 | let selection: Binding 81 | let items: Items 82 | 83 | func makeUIViewController(context: Context) -> PreviewController { 84 | .init(selection: selection, in: items) 85 | } 86 | 87 | func updateUIViewController(_ controller: PreviewController, context: Context) { 88 | controller.items = items 89 | controller.selection = selection 90 | } 91 | } 92 | 93 | #endif 94 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchAssetCollection.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import Photos 3 | import SwiftUI 4 | import SwiftBackports 5 | 6 | /// Fetches a set of asset collections from the `Photos` framework 7 | @propertyWrapper 8 | internal struct FetchAssetCollection: DynamicProperty where Result: PHAssetCollection { 9 | 10 | @ObservedObject 11 | internal private(set) var observer: ResultsObserver 12 | 13 | /// Represents the results of the fetch 14 | public var wrappedValue: MediaResults { 15 | get { MediaResults(observer.result) } 16 | set { observer.result = newValue.result } 17 | } 18 | 19 | } 20 | 21 | internal extension FetchAssetCollection { 22 | 23 | /// Instantiates a fetch with an existing `PHFetchResult` instance 24 | init(result: PHFetchResult) { 25 | self.init(observer: ResultsObserver(result: result)) 26 | } 27 | 28 | /// Instantiates a fetch with a custom `PHFetchOptions` instance 29 | init(_ options: PHFetchOptions? = nil) { 30 | let result = PHAssetCollection.fetchTopLevelUserCollections(with: options) 31 | self.init(observer: ResultsObserver(result: result as! PHFetchResult)) 32 | } 33 | 34 | /// Instantiates a fetch with a filter and sort options 35 | /// - Parameters: 36 | /// - filter: The predicate to apply when filtering the results 37 | /// - sort: The keyPaths to apply when sorting the results 38 | init(filter: NSPredicate? = nil, 39 | sort: [(KeyPath, ascending: Bool)]) { 40 | let options = PHFetchOptions() 41 | options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) } 42 | options.predicate = filter 43 | self.init(options) 44 | } 45 | 46 | } 47 | 48 | internal extension FetchAssetCollection { 49 | 50 | /// Instantiates a fetch for the specified album type and subtype 51 | /// - Parameters: 52 | /// - album: The album type to fetch 53 | /// - kind: The album subtype to fetch 54 | /// - options: Any additional options to apply to the fetch 55 | init(album: PHAssetCollectionType, 56 | kind: PHAssetCollectionSubtype = .any, 57 | options: PHFetchOptions? = nil) { 58 | let result = PHAssetCollection.fetchAssetCollections(with: album, subtype: kind, options: options) 59 | self.init(observer: ResultsObserver(result: result as! PHFetchResult)) 60 | } 61 | 62 | /// Instantiates a fetch for the specified album type and subtype 63 | /// - Parameters: 64 | /// - album: The album type to fetch 65 | /// - kind: The album subtype to fetch 66 | /// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results 67 | /// - filter: The predicate to apply when filtering the results 68 | init(album: PHAssetCollectionType, 69 | kind: PHAssetCollectionSubtype = .any, 70 | fetchLimit: Int = 0, 71 | filter: NSPredicate? = nil) { 72 | let options = PHFetchOptions() 73 | options.fetchLimit = fetchLimit 74 | options.predicate = filter 75 | self.init(album: album, kind: kind, options: options) 76 | } 77 | 78 | /// Instantiates a fetch for the specified album type and subtype 79 | /// - Parameters: 80 | /// - album: The album type to fetch 81 | /// - kind: The album subtype to fetch 82 | /// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results 83 | /// - filter: The predicate to apply when filtering the results 84 | /// - sort: The keyPaths to apply when sorting the results 85 | init(album: PHAssetCollectionType, 86 | kind: PHAssetCollectionSubtype = .any, 87 | fetchLimit: Int = 0, 88 | filter: NSPredicate? = nil, 89 | sort: [(KeyPath, ascending: Bool)]) { 90 | let options = PHFetchOptions() 91 | options.fetchLimit = fetchLimit 92 | options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) } 93 | options.predicate = filter 94 | self.init(album: album, kind: kind, options: options) 95 | } 96 | 97 | } 98 | #endif 99 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/GeometryChange/GeometryChange.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 18, message: "Used View.onGeometryChange instead") 5 | @available(tvOS, deprecated: 18, message: "Used View.onGeometryChange instead") 6 | @available(macOS, deprecated: 15, message: "Used View.onGeometryChange instead") 7 | @available(watchOS, deprecated: 10, message: "Used View.onGeometryChange instead") 8 | extension Backport where Wrapped: View { 9 | /// Adds an action to be performed when a value, created from a 10 | /// geometry proxy, changes. 11 | /// 12 | /// The geometry of a view can change frequently, especially if 13 | /// the view is contained within a ``ScrollView`` and that scroll view 14 | /// is scrolling. 15 | /// 16 | /// You should avoid updating large parts of your app whenever 17 | /// the scroll geometry changes. To aid in this, you provide two 18 | /// closures to this modifier: 19 | /// * transform: This converts a value of ``GeometryProxy`` to your 20 | /// own data type. 21 | /// * action: This provides the data type you created in `of` 22 | /// and is called whenever the data type changes. 23 | /// 24 | /// For example, you can use this modifier to know how much of a view 25 | /// is visible on screen. In the following example, 26 | /// the data type you convert to is a ``Bool`` and the action is called 27 | /// whenever the ``Bool`` changes. 28 | /// 29 | /// ScrollView(.horizontal) { 30 | /// LazyHStack { 31 | /// ForEach(videos) { video in 32 | /// VideoView(video) 33 | /// } 34 | /// } 35 | /// } 36 | /// 37 | /// struct VideoView: View { 38 | /// var video: VideoModel 39 | /// 40 | /// var body: some View { 41 | /// VideoPlayer(video) 42 | /// .backport.onGeometryChange(for: Bool.self) { proxy in 43 | /// let frame = proxy.frame(in: .scrollView) 44 | /// let bounds = proxy.bounds(of: .scrollView) ?? .zero 45 | /// let intersection = frame.intersection( 46 | /// CGRect(origin: .zero, size: bounds.size)) 47 | /// let visibleHeight = intersection.size.height 48 | /// return (visibleHeight / frame.size.height) > 0.75 49 | /// } action: { isVisible in 50 | /// video.updateAutoplayingState( 51 | /// isVisible: isVisible) 52 | /// } 53 | /// } 54 | /// } 55 | /// 56 | /// - Parameters: 57 | /// - type: The type of value transformed from a geometry proxy. 58 | /// - transform: A closure that transforms a ``GeometryProxy`` 59 | /// to your type. 60 | /// - action: A closure to run when the transformed data changes. 61 | /// - oldValue: The old value that failed the comparison check. 62 | /// - newValue: The new value that failed the comparison check. 63 | public nonisolated func onGeometryChange( 64 | for type: T.Type, 65 | of transform: @escaping @Sendable (GeometryProxy) -> T, 66 | action: @escaping (_ oldValue: T, _ newValue: T) -> Void 67 | ) -> some View where T: Equatable & Sendable { 68 | wrapped.modifier( 69 | GeometryChangeModifier( 70 | transform: transform, 71 | action: action 72 | ) 73 | ) 74 | } 75 | } 76 | 77 | private struct GeometryChangeModifier: ViewModifier where T: Equatable & Sendable { 78 | @State private var value: T? 79 | var transform: @Sendable (GeometryProxy) -> T 80 | var action: (_ oldValue: T, _ newValue: T) -> Void 81 | 82 | func body(content: Content) -> some View { 83 | content 84 | .backport.background { 85 | GeometryReader { proxy in 86 | let value = transform(proxy) 87 | Color.clear 88 | .onAppear { action(value, value) } 89 | .backport.onChange(of: value) { newValue in 90 | action(value, newValue) 91 | } 92 | } 93 | .allowsHitTesting(false) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/Presentation/ContentInteraction.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | @available(iOS, deprecated: 16.4) 5 | @available(tvOS, deprecated: 16.4) 6 | @available(macOS, deprecated: 13.3) 7 | @available(watchOS, deprecated: 9.4) 8 | public extension Backport { 9 | /// A behavior that you can use to influence how a presentation responds to 10 | /// swipe gestures. 11 | /// 12 | /// Use values of this type with the 13 | /// ``View/presentationContentInteraction(_:)`` modifier. 14 | struct PresentationContentInteraction: Hashable { 15 | enum Interaction: Hashable { 16 | case automatic 17 | case resizes 18 | case scrolls 19 | } 20 | 21 | let interaction: Interaction 22 | 23 | /// The default swipe behavior for the presentation. 24 | public static var automatic: PresentationContentInteraction { .init(interaction: .automatic) } 25 | 26 | /// A behavior that prioritizes resizing a presentation when swiping, rather 27 | /// than scrolling the content of the presentation. 28 | public static var resizes: PresentationContentInteraction { .init(interaction: .resizes) } 29 | 30 | /// A behavior that prioritizes scrolling the content of a presentation when 31 | /// swiping, rather than resizing the presentation. 32 | public static var scrolls: PresentationContentInteraction { .init(interaction: .scrolls) } 33 | } 34 | } 35 | 36 | @available(iOS, deprecated: 16.4) 37 | @available(tvOS, deprecated: 16.4) 38 | @available(macOS, deprecated: 13.3) 39 | @available(watchOS, deprecated: 9.4) 40 | @MainActor 41 | public extension Backport where Wrapped: View { 42 | @ViewBuilder 43 | func presentationContentInteraction(_ interaction: Backport.PresentationContentInteraction) -> some View { 44 | #if os(iOS) 45 | if #available(iOS 15, *) { 46 | wrapped.background(Backport.Representable(interaction: interaction)) 47 | } else { 48 | wrapped 49 | } 50 | #else 51 | wrapped 52 | #endif 53 | } 54 | } 55 | 56 | #if os(iOS) 57 | @available(iOS 15, *) 58 | private extension Backport where Wrapped == Any { 59 | struct Representable: UIViewControllerRepresentable { 60 | let interaction: Backport.PresentationContentInteraction 61 | 62 | func makeUIViewController(context: Context) -> Backport.Representable.Controller { 63 | Controller(interaction: interaction) 64 | } 65 | 66 | func updateUIViewController(_ controller: Backport.Representable.Controller, context: Context) { 67 | controller.update(interaction: interaction) 68 | } 69 | } 70 | } 71 | 72 | @available(iOS 15, *) 73 | private extension Backport.Representable { 74 | final class Controller: UIViewController, UISheetPresentationControllerDelegate { 75 | var interaction: Backport.PresentationContentInteraction 76 | 77 | init(interaction: Backport.PresentationContentInteraction) { 78 | self.interaction = interaction 79 | super.init(nibName: nil, bundle: nil) 80 | } 81 | 82 | required init?(coder: NSCoder) { 83 | fatalError("init(coder:) has not been implemented") 84 | } 85 | 86 | override func willMove(toParent parent: UIViewController?) { 87 | super.willMove(toParent: parent) 88 | update(interaction: interaction) 89 | } 90 | 91 | override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { 92 | super.willTransition(to: newCollection, with: coordinator) 93 | update(interaction: interaction) 94 | } 95 | 96 | func update(interaction: Backport.PresentationContentInteraction) { 97 | self.interaction = interaction 98 | 99 | if let controller = parent?.sheetPresentationController { 100 | controller.animateChanges { 101 | switch interaction.interaction { 102 | case .automatic, .resizes: 103 | controller.prefersScrollingExpandsWhenScrolledToEdge = true 104 | case .scrolls: 105 | controller.prefersScrollingExpandsWhenScrolledToEdge = false 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } 112 | #endif 113 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/iOS/FocusState/ViewFocused.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftBackports 3 | 4 | #if os(iOS) 5 | @MainActor 6 | public extension Backport where Wrapped: View { 7 | func focused(_ binding: Binding, equals value: Value) -> some View where Value: Hashable { 8 | wrapped.modifier(FocusModifier(focused: binding, value: value)) 9 | } 10 | } 11 | 12 | private struct FocusModifier: ViewModifier { 13 | @Environment(\.backportSubmit) private var submit 14 | @Backport.StateObject private var coordinator = Coordinator() 15 | @Binding var focused: Value? 16 | var value: Value 17 | 18 | func body(content: Content) -> some View { 19 | content 20 | // this ensures when the field goes out of view, it doesn't retain focus 21 | .onWillDisappear { focused = nil } 22 | .sibling(forType: UITextField.self) { proxy in 23 | let view = proxy.instance 24 | coordinator.observe(field: view) 25 | 26 | coordinator.onBegin = { 27 | focused = value 28 | } 29 | 30 | coordinator.onReturn = { 31 | submit() 32 | } 33 | 34 | coordinator.onEnd = { 35 | guard focused == value else { return } 36 | focused = nil 37 | } 38 | 39 | if focused == value, view.isUserInteractionEnabled, view.isEnabled { 40 | view.becomeFirstResponder() 41 | } 42 | } 43 | .backport.onChange(of: focused) { newValue in 44 | if newValue == nil { 45 | coordinator.field?.resignFirstResponder() 46 | } 47 | } 48 | } 49 | } 50 | 51 | private final class Coordinator: NSObject, ObservableObject, UITextFieldDelegate { 52 | private(set) weak var field: UITextField? 53 | weak var _delegate: UITextFieldDelegate? 54 | 55 | var onBegin: () -> Void = { } 56 | var onReturn: () -> Void = { } 57 | var onEnd: () -> Void = { } 58 | 59 | override init() { } 60 | 61 | func observe(field: UITextField) { 62 | self.field = field 63 | 64 | if field.delegate !== self && _delegate == nil { 65 | _delegate = field.delegate 66 | field.delegate = self 67 | } 68 | } 69 | 70 | func textFieldDidBeginEditing(_ textField: UITextField) { 71 | _delegate?.textFieldDidBeginEditing?(textField) 72 | onBegin() 73 | } 74 | 75 | func textFieldDidEndEditing(_ textField: UITextField) { 76 | _delegate?.textFieldDidEndEditing?(textField) 77 | onEnd() 78 | } 79 | 80 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 81 | onReturn() 82 | // prevent auto-resign 83 | return false 84 | } 85 | 86 | override func responds(to aSelector: Selector!) -> Bool { 87 | if super.responds(to: aSelector) { return true } 88 | if _delegate?.responds(to: aSelector) ?? false { return true } 89 | return false 90 | } 91 | 92 | override func forwardingTarget(for aSelector: Selector!) -> Any? { 93 | if super.responds(to: aSelector) { return self } 94 | return _delegate 95 | } 96 | } 97 | 98 | private struct WillDisappearHandler: UIViewControllerRepresentable { 99 | 100 | let onWillDisappear: () -> Void 101 | 102 | func makeUIViewController(context: Context) -> UIViewController { 103 | ViewWillDisappearViewController(onWillDisappear: onWillDisappear) 104 | } 105 | 106 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 107 | 108 | private class ViewWillDisappearViewController: UIViewController { 109 | let onWillDisappear: () -> Void 110 | 111 | init(onWillDisappear: @escaping () -> Void) { 112 | self.onWillDisappear = onWillDisappear 113 | super.init(nibName: nil, bundle: nil) 114 | } 115 | 116 | required init?(coder: NSCoder) { 117 | fatalError("init(coder:) has not been implemented") 118 | } 119 | 120 | override func viewWillDisappear(_ animated: Bool) { 121 | super.viewWillDisappear(animated) 122 | onWillDisappear() 123 | } 124 | } 125 | } 126 | 127 | private extension View { 128 | func onWillDisappear(_ perform: @escaping () -> Void) -> some View { 129 | background(WillDisappearHandler(onWillDisappear: perform)) 130 | } 131 | } 132 | #endif 133 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/PhotosPicker/PickerFilter.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import SwiftUI 3 | import PhotosUI 4 | import CoreServices 5 | import SwiftBackports 6 | 7 | @available(iOS, introduced: 13, deprecated: 16) 8 | public extension Backport where Wrapped == Any { 9 | /// A filter that restricts which types of assets to show 10 | struct PHPickerFilter { 11 | internal let mediaTypes: [CFString] 12 | internal let filter: Any? 13 | 14 | @available(iOS 13, *) 15 | internal init(mediaTypes: [CFString]) { 16 | self.mediaTypes = mediaTypes 17 | self.filter = nil 18 | } 19 | 20 | @available(iOS 14, *) 21 | internal init(filter: PhotosUI.PHPickerFilter) { 22 | self.filter = filter 23 | self.mediaTypes = [] 24 | } 25 | 26 | @available(iOS 14, *) 27 | var photosFilter: PhotosUI.PHPickerFilter { 28 | filter as! PhotosUI.PHPickerFilter 29 | } 30 | } 31 | } 32 | 33 | @available(iOS 13, *) 34 | public extension Backport.PHPickerFilter { 35 | /// The filter for images. 36 | static var images: Self { 37 | if #available(iOS 14, *) { 38 | return .init(filter: .images) 39 | } else { 40 | return .init(mediaTypes: [kUTTypeImage]) 41 | } 42 | } 43 | 44 | /// The filter for videos. 45 | static var videos: Self { 46 | if #available(iOS 14, *) { 47 | return .init(filter: .videos) 48 | } else { 49 | return .init(mediaTypes: [kUTTypeMovie]) 50 | } 51 | } 52 | 53 | /// The filter for live photos. 54 | static var livePhotos: Self { 55 | if #available(iOS 14, *) { 56 | return .init(filter: .livePhotos) 57 | } else { 58 | return .init(mediaTypes: [kUTTypeLivePhoto, kUTTypeImage]) 59 | } 60 | } 61 | } 62 | 63 | @available(iOS 14, *) 64 | public extension Backport.PHPickerFilter { 65 | /// Returns a new filter formed by AND-ing the filters in a given array. 66 | static func any(of subfilters: [Self]) -> Self { 67 | .init(filter: .any(of: subfilters.map { $0.photosFilter })) 68 | } 69 | } 70 | 71 | @available(iOS 15, *) 72 | public extension Backport.PHPickerFilter { 73 | /// The filter for panorama photos. 74 | static var panoramas: Self { 75 | .init(filter: .panoramas) 76 | } 77 | 78 | /// The filter for screenshots. 79 | static var screenshots: Self { 80 | .init(filter: .screenshots) 81 | } 82 | 83 | /// The filter for Slow-Mo videos. 84 | static var slomoVideos: Self { 85 | .init(filter: .slomoVideos) 86 | } 87 | 88 | /// The filter for time-lapse videos. 89 | static var timelapseVideos: Self { 90 | .init(filter: .timelapseVideos) 91 | } 92 | 93 | /// Returns a new filter based on the asset playback style. 94 | static func playbackStyle(_ playbackStyle: PHAsset.PlaybackStyle) -> Self { 95 | .init(filter: .playbackStyle(playbackStyle)) 96 | } 97 | 98 | /// Returns a new filter formed by AND-ing the filters in a given array. 99 | static func all(of subfilters: [Self]) -> Self { 100 | .init(filter: .all(of: subfilters.map { $0.photosFilter })) 101 | } 102 | 103 | /// Returns a new filter formed by negating the given filter. 104 | static func not(_ filter: Self) -> Self { 105 | .init(filter: .not(filter.photosFilter)) 106 | } 107 | } 108 | 109 | @available(iOS 16, *) 110 | public extension Backport.PHPickerFilter { 111 | /// The filter for Depth Effect photos. 112 | static var depthEffectPhotos: Self { 113 | .init(filter: .depthEffectPhotos) 114 | } 115 | 116 | /// The filter for Cinematic videos. 117 | static var cinematicVideos: Self { 118 | .init(filter: .cinematicVideos) 119 | } 120 | 121 | } 122 | #endif 123 | 124 | /** 125 | ---------------------- 126 | Class for Fetch Method 127 | ---------------------- 128 | PHAsset 129 | SELF, localIdentifier, creationDate, modificationDate, mediaType, mediaSubtypes, duration, pixelWidth, pixelHeight, isFavorite (or isFavorite), isHidden (or isHidden), burstIdentifier 130 | ---------------------- 131 | PHAssetCollection 132 | SELF, localIdentifier, localizedTitle (or title), startDate, endDate, estimatedAssetCount 133 | ---------------------- 134 | PHCollectionList 135 | SELF, localIdentifier, localizedTitle (or title), startDate, endDate 136 | ---------------------- 137 | PHCollection (can fetch a mix of PHCollectionList and PHAssetCollection objects) 138 | SELF, localIdentifier, localizedTitle (or title), startDate, endDate 139 | ---------------------- 140 | */ 141 | -------------------------------------------------------------------------------- /Sources/SwiftUIBackports/Shared/PhotosPicker/Fetch/FetchCollectionList.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import Photos 3 | import SwiftUI 4 | import SwiftBackports 5 | 6 | @propertyWrapper 7 | internal struct FetchCollectionList: DynamicProperty where Result: PHCollectionList { 8 | 9 | @ObservedObject 10 | internal private(set) var observer: ResultsObserver 11 | 12 | public var wrappedValue: MediaResults { 13 | get { MediaResults(observer.result) } 14 | set { observer.result = newValue.result } 15 | } 16 | 17 | } 18 | 19 | internal extension FetchCollectionList { 20 | 21 | /// Instantiates a fetch with an existing `PHFetchResult` instance 22 | init(_ result: PHFetchResult) { 23 | observer = ResultsObserver(result: result as! PHFetchResult) 24 | } 25 | 26 | /// Instantiates a fetch with a custom `PHFetchOptions` instance 27 | init(_ options: PHFetchOptions?) { 28 | let result = PHCollectionList.fetchTopLevelUserCollections(with: options) 29 | self.init(observer: ResultsObserver(result: result as! PHFetchResult)) 30 | } 31 | 32 | /// Instantiates a fetch by applying the specified sort and filter options 33 | /// - Parameters: 34 | /// - filter: The predicate to apply when filtering the results 35 | init(filter: NSPredicate? = nil) { 36 | let options = PHFetchOptions() 37 | options.predicate = filter 38 | self.init(options) 39 | } 40 | 41 | /// Instantiates a fetch by applying the specified sort and filter options 42 | /// - Parameters: 43 | /// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results 44 | /// - filter: The predicate to apply when filtering the results 45 | /// - sort: The keyPaths to apply when sorting the results 46 | init(fetchLimit: Int = 0, 47 | filter: NSPredicate? = nil, 48 | sort: [(KeyPath, ascending: Bool)]) { 49 | let options = PHFetchOptions() 50 | options.fetchLimit = fetchLimit 51 | options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) } 52 | options.predicate = filter 53 | self.init(options) 54 | } 55 | 56 | } 57 | 58 | internal extension FetchCollectionList { 59 | 60 | /// Fetches all lists of the specified type and subtyle 61 | /// - Parameters: 62 | /// - list: The list type to filter by 63 | /// - kind: The list subtype to filter by 64 | /// - options: Any additional options to apply to the request 65 | init(list: PHCollectionListType, 66 | kind: PHCollectionListSubtype = .any, 67 | options: PHFetchOptions? = nil) { 68 | let result = PHCollectionList.fetchCollectionLists(with: list, subtype: kind, options: options) 69 | self.init(observer: ResultsObserver(result: result as! PHFetchResult)) 70 | } 71 | 72 | /// Fetches all lists of the specified type and subtyle 73 | /// - Parameters: 74 | /// - list: The list type to filter by 75 | /// - kind: The list subtype to filter by 76 | /// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results 77 | /// - filter: The predicate to apply when filtering the results 78 | init(list: PHCollectionListType, 79 | kind: PHCollectionListSubtype = .any, 80 | fetchLimit: Int = 0, 81 | filter: NSPredicate) { 82 | let options = PHFetchOptions() 83 | options.fetchLimit = fetchLimit 84 | options.predicate = filter 85 | self.init(list: list, kind: kind, options: options) 86 | } 87 | 88 | /// Fetches all lists of the specified type and subtyle 89 | /// - Parameters: 90 | /// - list: The list type to filter by 91 | /// - kind: The list subtype to filter by 92 | /// - fetchLimit: The fetch limit to apply to the fetch, this may improve performance but limits results 93 | /// - filter: The predicate to apply when filtering the results 94 | /// - sort: The keyPaths to apply when sorting the results 95 | init(list: PHCollectionListType, 96 | kind: PHCollectionListSubtype = .any, 97 | fetchLimit: Int = 0, 98 | filter: NSPredicate? = nil, 99 | sort: [(KeyPath, ascending: Bool)]) { 100 | let options = PHFetchOptions() 101 | options.fetchLimit = fetchLimit 102 | options.sortDescriptors = sort.map { NSSortDescriptor(keyPath: $0.0, ascending: $0.ascending) } 103 | options.predicate = filter 104 | self.init(list: list, kind: kind, options: options) 105 | } 106 | 107 | } 108 | #endif 109 | --------------------------------------------------------------------------------