├── .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 |
--------------------------------------------------------------------------------