├── .gitignore ├── Sources └── SlideOverCard │ ├── SOCModel.swift │ ├── Helper │ ├── Animation+Default.swift │ ├── ColorScheme+UIKit.swift │ ├── WindowAccessor.swift │ ├── UIWindow+TopViewController.swift │ ├── UIScreen+CornerRadius.swift │ └── View+Modifiers.swift │ ├── SOCOptions.swift │ ├── SOCStyle.swift │ ├── SOCModifier.swift │ ├── Accessories.swift │ ├── SOCManager.swift │ └── SlideOverCard.swift ├── Package.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm/xcode 4 | /Packages 5 | /*.xcodeproj 6 | xcuserdata/ 7 | -------------------------------------------------------------------------------- /Sources/SlideOverCard/SOCModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SOCModel.swift 3 | // 4 | // 5 | // Created by João Gabriel Pozzobon dos Santos on 19/03/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A data model shared between `SOCModifier`, `SOCManager` and `SlideOverCard`, containing the state of the card 11 | internal class SOCModel: ObservableObject { 12 | @Published var showCard: Bool = false 13 | } 14 | -------------------------------------------------------------------------------- /Sources/SlideOverCard/Helper/Animation+Default.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Animation+Default.swift 3 | // 4 | // 5 | // Created by João Gabriel Pozzobon dos Santos on 19/03/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Animation { 11 | /// A default spring animation for the card transitions 12 | internal static var defaultSpring: Animation { 13 | .spring(response: 0.35, dampingFraction: 1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SlideOverCard/Helper/ColorScheme+UIKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorScheme+UIKit.swift 3 | // 4 | // 5 | // Created by João Gabriel Pozzobon dos Santos on 20/03/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension ColorScheme { 11 | /// A property that binds a SwiftUI `ColorScheme` to a UIKit `UIUserInterfaceStyle` 12 | internal var uiKit: UIUserInterfaceStyle { 13 | switch self { 14 | case .light: 15 | return .light 16 | case .dark: 17 | return .dark 18 | @unknown default: 19 | return .light 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/SlideOverCard/Helper/WindowAccessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowAccessor.swift 3 | // 4 | // 5 | // Created by João Gabriel Pozzobon dos Santos on 19/03/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A view that provides access to the `UIWindow` of the `View` it is in 11 | internal struct WindowAccessor: UIViewRepresentable { 12 | var callback: (UIWindow) -> () 13 | 14 | func makeUIView(context: Context) -> UIView { 15 | return UIView() 16 | } 17 | 18 | func updateUIView(_ uiView: UIView, context: Context) { 19 | if let window = uiView.window { 20 | callback(window) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/SlideOverCard/Helper/UIWindow+TopViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIWindow+TopViewController.swift 3 | // 4 | // 5 | // Created by João Gabriel Pozzobon dos Santos on 20/03/24. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIWindow { 11 | /// Returns the topmost view controller in the window's hierarchy 12 | internal func topViewController() -> UIViewController? { 13 | var top = self.rootViewController 14 | while true { 15 | if let presented = top?.presentedViewController { 16 | top = presented 17 | } else if let nav = top as? UINavigationController { 18 | top = nav.visibleViewController 19 | } else if let tab = top as? UITabBarController { 20 | top = tab.selectedViewController 21 | } else { 22 | break 23 | } 24 | } 25 | return top 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/SlideOverCard/Helper/UIScreen+CornerRadius.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScreen+CornerRadius.swift 3 | // 4 | // 5 | // Created by João Gabriel Pozzobon dos Santos on 19/03/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // From https://github.com/kylebshr/ScreenCorners 11 | 12 | extension UIScreen { 13 | private static let cornerRadiusKey: String = { 14 | let components = ["Radius", "Corner", "display", "_"] 15 | return components.reversed().joined() 16 | }() 17 | 18 | /// The corner radius of the display. Uses a private property of `UIScreen`, 19 | /// and may report 0 if the API changes. 20 | internal var displayCornerRadius: CGFloat? { 21 | guard let cornerRadius = self.value(forKey: Self.cornerRadiusKey) as? CGFloat else { 22 | assertionFailure("Failed to detect screen corner radius") 23 | return nil 24 | } 25 | 26 | return cornerRadius 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 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: "SlideOverCard", 8 | platforms: [ 9 | .iOS(.v13) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "SlideOverCard", 15 | targets: ["SlideOverCard"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "SlideOverCard", 26 | dependencies: []), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 João Gabriel Pozzobon dos Santos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/SlideOverCard/SOCOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SOCOptions.swift 3 | // 4 | // 5 | // Created by João Gabriel Pozzobon dos Santos on 17/03/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A struct that defines interaction options of a `SlideOverCard` 11 | public struct SOCOptions: OptionSet { 12 | public let rawValue: Int8 13 | 14 | public init(rawValue: Int8) { 15 | self.rawValue = rawValue 16 | } 17 | 18 | /// Disable dragging of the card completely, keeping it static 19 | public static let disableDrag = SOCOptions(rawValue: 1) 20 | /// Disable the ability of dismissing the card by dragging it down 21 | public static let disableDragToDismiss = SOCOptions(rawValue: 1 << 1) 22 | /// Hide the default dismiss button 23 | public static let hideDismissButton = SOCOptions(rawValue: 1 << 2) 24 | /// Disable the ability of dismissing the card by tapping the background view 25 | public static let disableTapToDismiss = SOCOptions(rawValue: 1 << 3) 26 | 27 | /// Create a `SOCOptions` from a set of values 28 | public static func fromValues(disableDrag: Bool = false, 29 | disableDragToDismiss: Bool = false, 30 | hideDismissButton: Bool = false) -> SOCOptions { 31 | var options = SOCOptions() 32 | if disableDrag { options.insert(.disableDrag) } 33 | if disableDragToDismiss { options.insert(.disableDragToDismiss) } 34 | if hideDismissButton { options.insert(.hideDismissButton) } 35 | return options 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SlideOverCard/SOCStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SOCStyle.swift 3 | // 4 | // 5 | // Created by João Gabriel Pozzobon dos Santos on 17/03/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A struct thtat defines the style of a `SlideOverCard` 11 | public struct SOCStyle { 12 | /// Initialize a style with a single value for corner radius 13 | public init(cornerRadius: CGFloat? = nil, 14 | continuous: Bool = true, 15 | innerPadding: CGFloat = 24.0, 16 | outerPadding: CGFloat = 6.0, 17 | dimmingOpacity: CGFloat = 0.3, 18 | style: S = Color(.systemGray6)) { 19 | let cornerRadius = cornerRadius ?? (UIScreen.main.displayCornerRadius ?? 41.5)-outerPadding/2.0 20 | 21 | self.init(cornerRadii: CGSize(width: cornerRadius, height: cornerRadius), 22 | continuous: continuous, 23 | innerPadding: innerPadding, 24 | outerPadding: outerPadding, 25 | dimmingOpacity: dimmingOpacity, 26 | style: style) 27 | } 28 | 29 | /// Initialize a style with a custom size for corner radii 30 | public init(cornerRadii: CGSize, 31 | continuous: Bool = true, 32 | innerPadding: CGFloat = 20.0, 33 | outerPadding: CGFloat = 6.0, 34 | dimmingOpacity: CGFloat = 0.3, 35 | style: S = Color(.systemGray6)) { 36 | self.cornerRadii = cornerRadii 37 | self.continuous = continuous 38 | self.innerPadding = innerPadding 39 | self.outerPadding = outerPadding 40 | self.dimmingOpacity = dimmingOpacity 41 | self.style = style 42 | } 43 | 44 | let cornerRadii: CGSize 45 | let continuous: Bool 46 | 47 | let innerPadding: CGFloat 48 | let outerPadding: CGFloat 49 | 50 | let dimmingOpacity: CGFloat 51 | 52 | let style: S 53 | } 54 | -------------------------------------------------------------------------------- /Sources/SlideOverCard/SOCModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SOCModifier.swift 3 | // 4 | // 5 | // Created by João Gabriel Pozzobon dos Santos on 18/03/24. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | /// A view modifier that presents a `SlideOverCard` over a `View`'s hierarchy through a `SOCManager` based on a `Binding` value 12 | internal struct SOCModifier: ViewModifier { 13 | var model: SOCModel 14 | @Binding var isPresented: Bool 15 | 16 | private let manager: SOCManager 17 | 18 | @Environment(\.colorScheme) var colorScheme 19 | 20 | init(isPresented: Binding, 21 | onDismiss: (() -> Void)? = nil, 22 | options: SOCOptions, 23 | style: SOCStyle