diff --git a/Demo/AutoResizingSheetDemo/AutoResizingSheetDemoApp.swift b/Demo/AutoResizingSheetDemo/AutoResizingSheetDemoApp.swift index 6efea49..0a4969d 100644 --- a/Demo/AutoResizingSheetDemo/AutoResizingSheetDemoApp.swift +++ b/Demo/AutoResizingSheetDemo/AutoResizingSheetDemoApp.swift @@ -3,26 +3,26 @@ import AutoResizingSheet @main struct AutoResizingSheetDemoApp: App { - @State private var configuration = AutoResizingSheetConfiguration() - - var body: some Scene { - WindowGroup { - TabView { - SwiftUIExampleView(configuration: configuration) - .tabItem { - Label("SwiftUI", systemImage: "square.stack") - } - - UIKitExampleViewRepresentable(configuration: configuration) - .tabItem { - Label("UIKit", systemImage: "poweroutlet.type.a") - } - - ConfigurationSettingsView(configuration: $configuration) - .tabItem { - Label("Config", systemImage: "gear") - } - } - } + @State private var configuration = AutoResizingSheetConfiguration() + + var body: some Scene { + WindowGroup { + TabView { + SwiftUIExampleView(configuration: configuration) + .tabItem { + Label("SwiftUI", systemImage: "square.stack") + } + + UIKitExampleViewRepresentable(configuration: configuration) + .tabItem { + Label("UIKit", systemImage: "poweroutlet.type.a") + } + + ConfigurationSettingsView(configuration: $configuration) + .tabItem { + Label("Config", systemImage: "gear") + } + } } + } } diff --git a/Demo/AutoResizingSheetDemo/Views/SheetContentView/AsyncSheetContentView.swift b/Demo/AutoResizingSheetDemo/Views/SheetContentView/AsyncSheetContentView.swift index 6ad5490..5a34999 100644 --- a/Demo/AutoResizingSheetDemo/Views/SheetContentView/AsyncSheetContentView.swift +++ b/Demo/AutoResizingSheetDemo/Views/SheetContentView/AsyncSheetContentView.swift @@ -1,63 +1,63 @@ import SwiftUI struct AsyncSheetContentView: View { - @State private var downloadedImage: UIImage? - - var body: some View { - VStack(spacing: 15) { - Text("Lorem ipsum dolor sit amet") - .font(.title) - .frame(maxWidth: .infinity, alignment: .leading) - - if let downloadedImage { - Image(uiImage: downloadedImage) - .resizable() - .scaledToFit() - .frame(width: 100) - .fixedSize(horizontal: false, vertical: true) - } else { - ProgressView() - .frame(maxWidth: .infinity, alignment: .center) - } - - Text("⬆️ You can resize the sheet to full size using the grabber") - .font(.caption) - .frame(maxWidth: .infinity, alignment: .leading) - - Text("📜 By default the sheets content is scrollable") - .font(.caption) - .frame(maxWidth: .infinity, alignment: .leading) - - Text("🛜 The sheet simulates a async sheet where the content changes it's size after data is loaded") - .font(.caption) - .frame(maxWidth: .infinity, alignment: .leading) - .fixedSize(horizontal: false, vertical: true) - - Button { - Task { - downloadedImage = nil - downloadedImage = await downloadExampleImage() - } - } label: { - Text("Reload image") - } - } - .padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20)) - .onAppear { - Task { - downloadedImage = await downloadExampleImage() - } + @State private var downloadedImage: UIImage? + + var body: some View { + VStack(spacing: 15) { + Text("Lorem ipsum dolor sit amet") + .font(.title) + .frame(maxWidth: .infinity, alignment: .leading) + + if let downloadedImage { + Image(uiImage: downloadedImage) + .resizable() + .scaledToFit() + .frame(width: 100) + .fixedSize(horizontal: false, vertical: true) + } else { + ProgressView() + .frame(maxWidth: .infinity, alignment: .center) + } + + Text("⬆️ You can resize the sheet to full size using the grabber") + .font(.caption) + .frame(maxWidth: .infinity, alignment: .leading) + + Text("📜 By default the sheets content is scrollable") + .font(.caption) + .frame(maxWidth: .infinity, alignment: .leading) + + Text("🛜 The sheet simulates a async sheet where the content changes it's size after data is loaded") + .font(.caption) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + + Button { + Task { + downloadedImage = nil + downloadedImage = await downloadExampleImage() } + } label: { + Text("Reload image") + } } - - // Example function for downloading an image - private func downloadExampleImage() async -> UIImage? { - let urlString = "https://picsum.photos/1000/1200" - guard let url = URL(string: urlString), - let (data, _) = try? await URLSession.shared.data(from: url), - let image = UIImage(data: data) - else { return nil } - - return image + .padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20)) + .onAppear { + Task { + downloadedImage = await downloadExampleImage() + } } + } + + // Example function for downloading an image + private func downloadExampleImage() async -> UIImage? { + let urlString = "https://picsum.photos/1000/1200" + guard let url = URL(string: urlString), + let (data, _) = try? await URLSession.shared.data(from: url), + let image = UIImage(data: data) + else { return nil } + + return image + } } diff --git a/Demo/AutoResizingSheetDemo/Views/SheetContentView/ConfigurationSettingsView.swift b/Demo/AutoResizingSheetDemo/Views/SheetContentView/ConfigurationSettingsView.swift index a182d2b..6971428 100644 --- a/Demo/AutoResizingSheetDemo/Views/SheetContentView/ConfigurationSettingsView.swift +++ b/Demo/AutoResizingSheetDemo/Views/SheetContentView/ConfigurationSettingsView.swift @@ -2,18 +2,18 @@ import AutoResizingSheet import SwiftUI struct ConfigurationSettingsView: View { - @Binding var configuration: AutoResizingSheetConfiguration - - var body: some View { - VStack(spacing: 15) { - Toggle("scrollable", isOn: $configuration.scrollable) - - Toggle("showGrabber", isOn: $configuration.showGrabber) - - Toggle("extendableToFullSize", isOn: $configuration.extendableToFullSize) - - Spacer() - } - .padding(20) + @Binding var configuration: AutoResizingSheetConfiguration + + var body: some View { + VStack(spacing: 15) { + Toggle("scrollable", isOn: $configuration.scrollable) + + Toggle("showGrabber", isOn: $configuration.showGrabber) + + Toggle("extendableToFullSize", isOn: $configuration.extendableToFullSize) + + Spacer() } + .padding(20) + } } diff --git a/Demo/AutoResizingSheetDemo/Views/SheetContentView/DynamicSheetContentView.swift b/Demo/AutoResizingSheetDemo/Views/SheetContentView/DynamicSheetContentView.swift index 56e89ee..53edbdb 100644 --- a/Demo/AutoResizingSheetDemo/Views/SheetContentView/DynamicSheetContentView.swift +++ b/Demo/AutoResizingSheetDemo/Views/SheetContentView/DynamicSheetContentView.swift @@ -1,9 +1,9 @@ import SwiftUI struct DynamicSheetContentView: View { - @State private var text: String - private let textToAppend = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam " - private let defaultText = """ + @State private var text: String + private let textToAppend = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam " + private let defaultText = """ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam \ nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, \ sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. \ @@ -11,50 +11,50 @@ struct DynamicSheetContentView: View { Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam \ nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. """ - - init() { - _text = State(initialValue: defaultText) - } - - var body: some View { - VStack(spacing: 15) { - Text("Lorem ipsum dolor sit amet") - .font(.title) - .frame(maxWidth: .infinity, alignment: .leading) - - Text(text) - .font(.caption) - .fixedSize(horizontal: false, vertical: true) - - Text("⬆️ You can resize the sheet to full size using the grabber") - .font(.caption) - .frame(maxWidth: .infinity, alignment: .leading) - - Text("📜 By default the sheets content is scrollable") - .font(.caption) - .frame(maxWidth: .infinity, alignment: .leading) - - Text("↕️ Test the resizing by appending text or resetting it back to default") - .font(.caption) - .frame(maxWidth: .infinity, alignment: .leading) - .fixedSize(horizontal: false, vertical: true) - - HStack { - Button { - text = defaultText - } label: { - Text("Reset text") - } - - Spacer() - - Button { - text += textToAppend - } label: { - Text("Append text") - } - } + + init() { + _text = State(initialValue: defaultText) + } + + var body: some View { + VStack(spacing: 15) { + Text("Lorem ipsum dolor sit amet") + .font(.title) + .frame(maxWidth: .infinity, alignment: .leading) + + Text(text) + .font(.caption) + .fixedSize(horizontal: false, vertical: true) + + Text("⬆️ You can resize the sheet to full size using the grabber") + .font(.caption) + .frame(maxWidth: .infinity, alignment: .leading) + + Text("📜 By default the sheets content is scrollable") + .font(.caption) + .frame(maxWidth: .infinity, alignment: .leading) + + Text("↕️ Test the resizing by appending text or resetting it back to default") + .font(.caption) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + + HStack { + Button { + text = defaultText + } label: { + Text("Reset text") + } + + Spacer() + + Button { + text += textToAppend + } label: { + Text("Append text") } - .padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20)) + } } + .padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20)) + } } diff --git a/Demo/AutoResizingSheetDemo/Views/SheetContentView/StaticSheetContentView.swift b/Demo/AutoResizingSheetDemo/Views/SheetContentView/StaticSheetContentView.swift index c275864..2b96bbc 100644 --- a/Demo/AutoResizingSheetDemo/Views/SheetContentView/StaticSheetContentView.swift +++ b/Demo/AutoResizingSheetDemo/Views/SheetContentView/StaticSheetContentView.swift @@ -1,15 +1,15 @@ import SwiftUI struct StaticSheetContentView: View { - @Environment(\.dismiss) private var dismiss - - var body: some View { - VStack(spacing: 15) { - Text("Lorem ipsum dolor sit amet") - .font(.title) - .frame(maxWidth: .infinity, alignment: .leading) - - Text(""" + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack(spacing: 15) { + Text("Lorem ipsum dolor sit amet") + .font(.title) + .frame(maxWidth: .infinity, alignment: .leading) + + Text(""" Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam \ nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, \ sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. \ @@ -17,30 +17,30 @@ struct StaticSheetContentView: View { Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam \ nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. """) - .font(.caption) - .fixedSize(horizontal: false, vertical: true) - - Text("⬆️ You can resize the sheet to full size using the grabber") - .font(.caption) - .frame(maxWidth: .infinity, alignment: .leading) - - Text("📜 By default the sheets content is scrollable") - .font(.caption) - .frame(maxWidth: .infinity, alignment: .leading) - - Text("❌ The sheet can be closed via the button or by swiping it down / touching the outside area") - .font(.caption) - .frame(maxWidth: .infinity, alignment: .leading) - .fixedSize(horizontal: false, vertical: true) - - Button { - dismiss() - } label: { - Image(systemName: "xmark") - Text("Close sheet") - } - .frame(maxWidth: .infinity, alignment: .center) - } - .padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20)) + .font(.caption) + .fixedSize(horizontal: false, vertical: true) + + Text("⬆️ You can resize the sheet to full size using the grabber") + .font(.caption) + .frame(maxWidth: .infinity, alignment: .leading) + + Text("📜 By default the sheets content is scrollable") + .font(.caption) + .frame(maxWidth: .infinity, alignment: .leading) + + Text("❌ The sheet can be closed via the button or by swiping it down / touching the outside area") + .font(.caption) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + + Button { + dismiss() + } label: { + Image(systemName: "xmark") + Text("Close sheet") + } + .frame(maxWidth: .infinity, alignment: .center) } + .padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20)) + } } diff --git a/Demo/AutoResizingSheetDemo/Views/SwiftUIExampleView.swift b/Demo/AutoResizingSheetDemo/Views/SwiftUIExampleView.swift index e2a8daa..5fc4926 100644 --- a/Demo/AutoResizingSheetDemo/Views/SwiftUIExampleView.swift +++ b/Demo/AutoResizingSheetDemo/Views/SwiftUIExampleView.swift @@ -2,58 +2,58 @@ import SwiftUI import AutoResizingSheet struct SwiftUIExampleView: View { - let configuration: AutoResizingSheetConfiguration - @State private var showStaticSheet = false - @State private var showDynamicSheet = false - @State private var showAsyncSheet = false - - var body: some View { - VStack(spacing: 15) { - Spacer() - - Button { - showStaticSheet.toggle() - } label: { - Text("Show static sheet") - } - - Button { - showDynamicSheet.toggle() - } label: { - Text("Show dynamic sheet") - } - - Button { - showAsyncSheet.toggle() - } label: { - Text("Show async sheet") - } - - Spacer() - } - .padding() - .autoResizingSheet( - isPresented: $showStaticSheet, - configuration: configuration - ) { - StaticSheetContentView() - - } - .autoResizingSheet( - isPresented: $showDynamicSheet, - configuration: configuration - ) { - DynamicSheetContentView() - } - .autoResizingSheet( - isPresented: $showAsyncSheet, - configuration: configuration - ) { - AsyncSheetContentView() - } + let configuration: AutoResizingSheetConfiguration + @State private var showStaticSheet = false + @State private var showDynamicSheet = false + @State private var showAsyncSheet = false + + var body: some View { + VStack(spacing: 15) { + Spacer() + + Button { + showStaticSheet.toggle() + } label: { + Text("Show static sheet") + } + + Button { + showDynamicSheet.toggle() + } label: { + Text("Show dynamic sheet") + } + + Button { + showAsyncSheet.toggle() + } label: { + Text("Show async sheet") + } + + Spacer() } + .padding() + .autoResizingSheet( + isPresented: $showStaticSheet, + configuration: configuration + ) { + StaticSheetContentView() + + } + .autoResizingSheet( + isPresented: $showDynamicSheet, + configuration: configuration + ) { + DynamicSheetContentView() + } + .autoResizingSheet( + isPresented: $showAsyncSheet, + configuration: configuration + ) { + AsyncSheetContentView() + } + } } #Preview { - SwiftUIExampleView(configuration: AutoResizingSheetConfiguration()) + SwiftUIExampleView(configuration: AutoResizingSheetConfiguration()) } diff --git a/Demo/AutoResizingSheetDemo/Views/UIKitExampleView.swift b/Demo/AutoResizingSheetDemo/Views/UIKitExampleView.swift index 7d4f0b7..99db0c9 100644 --- a/Demo/AutoResizingSheetDemo/Views/UIKitExampleView.swift +++ b/Demo/AutoResizingSheetDemo/Views/UIKitExampleView.swift @@ -3,94 +3,94 @@ import AutoResizingSheet import UIKit class UIKitExampleView: UIView { - private var configuration: AutoResizingSheetConfiguration - private let staticButton = UIButton(type: .system) - private let dynamicButton = UIButton(type: .system) - private let asyncButton = UIButton(type: .system) - private weak var hostingController: UIViewController? + private var configuration: AutoResizingSheetConfiguration + private let staticButton = UIButton(type: .system) + private let dynamicButton = UIButton(type: .system) + private let asyncButton = UIButton(type: .system) + private weak var hostingController: UIViewController? + + init(configuration: AutoResizingSheetConfiguration, hostingController: UIViewController?) { + self.configuration = configuration + self.hostingController = hostingController + super.init(frame: .zero) + setupView() + } + + required init?(coder: NSCoder) { + self.configuration = AutoResizingSheetConfiguration() + super.init(coder: coder) + setupView() + } + + private func setupView() { + backgroundColor = UIColor.systemBackground - init(configuration: AutoResizingSheetConfiguration, hostingController: UIViewController?) { - self.configuration = configuration - self.hostingController = hostingController - super.init(frame: .zero) - setupView() - } + staticButton.setTitle("Show static sheet", for: .normal) + staticButton.addTarget(self, action: #selector(showStaticSheet), for: .touchUpInside) + staticButton.translatesAutoresizingMaskIntoConstraints = false + addSubview(staticButton) - required init?(coder: NSCoder) { - self.configuration = AutoResizingSheetConfiguration() - super.init(coder: coder) - setupView() - } + dynamicButton.setTitle("Show dynamic sheet", for: .normal) + dynamicButton.addTarget(self, action: #selector(showDynamicSheet), for: .touchUpInside) + dynamicButton.translatesAutoresizingMaskIntoConstraints = false + addSubview(dynamicButton) - private func setupView() { - backgroundColor = UIColor.systemBackground - - staticButton.setTitle("Show static sheet", for: .normal) - staticButton.addTarget(self, action: #selector(showStaticSheet), for: .touchUpInside) - staticButton.translatesAutoresizingMaskIntoConstraints = false - addSubview(staticButton) - - dynamicButton.setTitle("Show dynamic sheet", for: .normal) - dynamicButton.addTarget(self, action: #selector(showDynamicSheet), for: .touchUpInside) - dynamicButton.translatesAutoresizingMaskIntoConstraints = false - addSubview(dynamicButton) - - asyncButton.setTitle("Show async sheet", for: .normal) - asyncButton.addTarget(self, action: #selector(showAsyncSheet), for: .touchUpInside) - asyncButton.translatesAutoresizingMaskIntoConstraints = false - addSubview(asyncButton) - - NSLayoutConstraint.activate([ - staticButton.centerXAnchor.constraint(equalTo: centerXAnchor), - staticButton.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -40), - - dynamicButton.centerXAnchor.constraint(equalTo: centerXAnchor), - dynamicButton.topAnchor.constraint(equalTo: staticButton.bottomAnchor, constant: 20), - - asyncButton.centerXAnchor.constraint(equalTo: centerXAnchor), - asyncButton.topAnchor.constraint(equalTo: dynamicButton.bottomAnchor, constant: 20) - ]) - } + asyncButton.setTitle("Show async sheet", for: .normal) + asyncButton.addTarget(self, action: #selector(showAsyncSheet), for: .touchUpInside) + asyncButton.translatesAutoresizingMaskIntoConstraints = false + addSubview(asyncButton) - @objc private func showStaticSheet() { - let sheetContentView = StaticSheetContentView() - hostingController?.presentViewAsAutoResizingSheet( - content: sheetContentView, - configuration: configuration - ) - } - - @objc private func showDynamicSheet() { - let sheetContentView = DynamicSheetContentView() - hostingController?.presentViewAsAutoResizingSheet( - content: sheetContentView, - configuration: configuration - ) - } - - @objc private func showAsyncSheet() { - let sheetContentView = AsyncSheetContentView() - hostingController?.presentViewAsAutoResizingSheet( - content: sheetContentView, - configuration: configuration - ) - } - - func updateConfiguration(with configuration: AutoResizingSheetConfiguration) { - self.configuration = configuration - } + NSLayoutConstraint.activate([ + staticButton.centerXAnchor.constraint(equalTo: centerXAnchor), + staticButton.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -40), + + dynamicButton.centerXAnchor.constraint(equalTo: centerXAnchor), + dynamicButton.topAnchor.constraint(equalTo: staticButton.bottomAnchor, constant: 20), + + asyncButton.centerXAnchor.constraint(equalTo: centerXAnchor), + asyncButton.topAnchor.constraint(equalTo: dynamicButton.bottomAnchor, constant: 20) + ]) + } + + @objc private func showStaticSheet() { + let sheetContentView = StaticSheetContentView() + hostingController?.presentViewAsAutoResizingSheet( + content: sheetContentView, + configuration: configuration + ) + } + + @objc private func showDynamicSheet() { + let sheetContentView = DynamicSheetContentView() + hostingController?.presentViewAsAutoResizingSheet( + content: sheetContentView, + configuration: configuration + ) + } + + @objc private func showAsyncSheet() { + let sheetContentView = AsyncSheetContentView() + hostingController?.presentViewAsAutoResizingSheet( + content: sheetContentView, + configuration: configuration + ) + } + + func updateConfiguration(with configuration: AutoResizingSheetConfiguration) { + self.configuration = configuration + } } struct UIKitExampleViewRepresentable: UIViewRepresentable { - let configuration: AutoResizingSheetConfiguration - - func makeUIView(context: Context) -> UIKitExampleView { - let hostingController = UIApplication.shared.windows.first?.rootViewController - let uiKitView = UIKitExampleView(configuration: configuration, hostingController: hostingController) - return uiKitView - } - - func updateUIView(_ uiView: UIKitExampleView, context: Context) { - uiView.updateConfiguration(with: configuration) - } + let configuration: AutoResizingSheetConfiguration + + func makeUIView(context: Context) -> UIKitExampleView { + let hostingController = UIApplication.shared.windows.first?.rootViewController + let uiKitView = UIKitExampleView(configuration: configuration, hostingController: hostingController) + return uiKitView + } + + func updateUIView(_ uiView: UIKitExampleView, context: Context) { + uiView.updateConfiguration(with: configuration) + } } diff --git a/Package.swift b/Package.swift index badef55..87b45a2 100644 --- a/Package.swift +++ b/Package.swift @@ -4,24 +4,24 @@ import PackageDescription let package = Package( - name: "AutoResizingSheet", - platforms: [.iOS(.v16)], - products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. - .library( - name: "AutoResizingSheet", - targets: ["AutoResizingSheet"]), - ], - targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. - .target( - name: "AutoResizingSheet", - path: "Sources", - resources: [.copy("PrivacyInfo.xcprivacy")] - ), - .testTarget( - name: "AutoResizingSheetTests", - dependencies: ["AutoResizingSheet"]), - ] + name: "AutoResizingSheet", + platforms: [.iOS(.v16)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "AutoResizingSheet", + targets: ["AutoResizingSheet"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "AutoResizingSheet", + path: "Sources", + resources: [.copy("PrivacyInfo.xcprivacy")] + ), + .testTarget( + name: "AutoResizingSheetTests", + dependencies: ["AutoResizingSheet"]), + ] ) diff --git a/Sources/AutoResizingSheet/AutoResizingSheet.swift b/Sources/AutoResizingSheet/AutoResizingSheet.swift index a151c3d..4ccce4f 100644 --- a/Sources/AutoResizingSheet/AutoResizingSheet.swift +++ b/Sources/AutoResizingSheet/AutoResizingSheet.swift @@ -3,57 +3,57 @@ import SwiftUI /// Used to present an auto resizing sheet as a `UIHostingController` public extension UIViewController { - /// Presents a SwiftUI view as sheet that will update its height automatically. - /// If the height of the content changes (e.g. after fetching data), the height is updated with an animation - /// - /// - Note: The resizing will not work properly if your view is wrapped inside a `ScrollView`. Use `scrollable` of `AutoResizingSheetConfiguration` instead, to make the content scrollable. - /// - /// - Parameters: - /// - content: The content of the sheet view (e.g. a `VStack`). - /// - configuration: The configuration to use for the sheet. - /// - onDismiss: The closure to execute when dismissing the sheet. - func presentViewAsAutoResizingSheet( - content: Content, - configuration: AutoResizingSheetConfiguration = AutoResizingSheetConfiguration(), - onDismiss: (() -> Void)? = nil - ) { - let autoResizingSheetView = AutoResizingSheetView( - sheetPresentationController: nil, - scrollable: configuration.scrollable, - content: { content } - ) - let hostingController = AutoResizingSheetHostingController( - isPresented: .constant(true), - onDismiss: onDismiss, - rootView: autoResizingSheetView - ) - hostingController.setSheetPresentationStyle(with: configuration) - hostingController.rootView.sheetPresentationController = hostingController.sheetPresentationController - - self.present(hostingController, animated: true) - } + /// Presents a SwiftUI view as sheet that will update its height automatically. + /// If the height of the content changes (e.g. after fetching data), the height is updated with an animation + /// + /// - Note: The resizing will not work properly if your view is wrapped inside a `ScrollView`. Use `scrollable` of `AutoResizingSheetConfiguration` instead, to make the content scrollable. + /// + /// - Parameters: + /// - content: The content of the sheet view (e.g. a `VStack`). + /// - configuration: The configuration to use for the sheet. + /// - onDismiss: The closure to execute when dismissing the sheet. + func presentViewAsAutoResizingSheet( + content: Content, + configuration: AutoResizingSheetConfiguration = AutoResizingSheetConfiguration(), + onDismiss: (() -> Void)? = nil + ) { + let autoResizingSheetView = AutoResizingSheetView( + sheetPresentationController: nil, + scrollable: configuration.scrollable, + content: { content } + ) + let hostingController = AutoResizingSheetHostingController( + isPresented: .constant(true), + onDismiss: onDismiss, + rootView: autoResizingSheetView + ) + hostingController.setSheetPresentationStyle(with: configuration) + hostingController.rootView.sheetPresentationController = hostingController.sheetPresentationController + + self.present(hostingController, animated: true) + } } /// Used to present an auto resizing sheet in a SwiftUI `View public extension View { - /// Creates a sheet view with content that will update its height automatically. - /// If the height of the content changes (e.g. after fetching data), the height is updated with an animation - /// - /// - Note: The resizing will not work properly if your view is wrapped inside a `ScrollView`. Use `scrollable` of `AutoResizingSheetConfiguration` instead, to make the content scrollable. - /// - /// - Parameters: - /// - isPresented: Binding to control the presentation of the sheet. - /// - configuration: The configuration to use for the sheet. - /// - content: The content of the sheet view (e.g. a `VStack`). - func autoResizingSheet( - isPresented: Binding, - configuration: AutoResizingSheetConfiguration = AutoResizingSheetConfiguration(), - @ViewBuilder content: @escaping () -> Content - ) -> some View { - self.modifier(AutoResizingSheetViewModifier( - isPresented: isPresented, - configuration: configuration, - sheetContent: content - )) - } + /// Creates a sheet view with content that will update its height automatically. + /// If the height of the content changes (e.g. after fetching data), the height is updated with an animation + /// + /// - Note: The resizing will not work properly if your view is wrapped inside a `ScrollView`. Use `scrollable` of `AutoResizingSheetConfiguration` instead, to make the content scrollable. + /// + /// - Parameters: + /// - isPresented: Binding to control the presentation of the sheet. + /// - configuration: The configuration to use for the sheet. + /// - content: The content of the sheet view (e.g. a `VStack`). + func autoResizingSheet( + isPresented: Binding, + configuration: AutoResizingSheetConfiguration = AutoResizingSheetConfiguration(), + @ViewBuilder content: @escaping () -> Content + ) -> some View { + self.modifier(AutoResizingSheetViewModifier( + isPresented: isPresented, + configuration: configuration, + sheetContent: content + )) + } } diff --git a/Sources/AutoResizingSheet/Controller/AutoResizingSheetHostingController.swift b/Sources/AutoResizingSheet/Controller/AutoResizingSheetHostingController.swift index 7fd54ea..60475ae 100644 --- a/Sources/AutoResizingSheet/Controller/AutoResizingSheetHostingController.swift +++ b/Sources/AutoResizingSheet/Controller/AutoResizingSheetHostingController.swift @@ -1,26 +1,26 @@ import SwiftUI class AutoResizingSheetHostingController: UIHostingController { - @Binding var isPresented: Bool - let onDismiss: (() -> Void)? - - init( - isPresented: Binding, - onDismiss: (() -> Void)?, - rootView: Content - ) { - _isPresented = isPresented - self.onDismiss = onDismiss - super.init(rootView: rootView) - } - - @objc required dynamic init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - isPresented = false - onDismiss?() - } + @Binding var isPresented: Bool + let onDismiss: (() -> Void)? + + init( + isPresented: Binding, + onDismiss: (() -> Void)?, + rootView: Content + ) { + _isPresented = isPresented + self.onDismiss = onDismiss + super.init(rootView: rootView) + } + + @objc required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + isPresented = false + onDismiss?() + } } diff --git a/Sources/AutoResizingSheet/Extensions/UIApplication+Extension.swift b/Sources/AutoResizingSheet/Extensions/UIApplication+Extension.swift index 64ee7c3..370bdba 100644 --- a/Sources/AutoResizingSheet/Extensions/UIApplication+Extension.swift +++ b/Sources/AutoResizingSheet/Extensions/UIApplication+Extension.swift @@ -1,10 +1,10 @@ import UIKit extension UIApplication { - var firstKeyWindow: UIWindow? { - return UIApplication.shared.connectedScenes - .compactMap { $0 as? UIWindowScene } - .filter { $0.activationState == .foregroundActive } - .first?.keyWindow - } + var firstKeyWindow: UIWindow? { + return UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .filter { $0.activationState == .foregroundActive } + .first?.keyWindow + } } diff --git a/Sources/AutoResizingSheet/Extensions/UIHostingController+Extension.swift b/Sources/AutoResizingSheet/Extensions/UIHostingController+Extension.swift index 2ce145b..02142ab 100644 --- a/Sources/AutoResizingSheet/Extensions/UIHostingController+Extension.swift +++ b/Sources/AutoResizingSheet/Extensions/UIHostingController+Extension.swift @@ -1,18 +1,18 @@ import SwiftUI extension UIHostingController { - /// Sets the correct sheet presentation style for sheets. - /// - /// - Parameter configuration: The `AutoResizingSheetConfiguration` to use. - func setSheetPresentationStyle(with configuration: AutoResizingSheetConfiguration) { - self.modalPresentationStyle = .pageSheet - self.sheetPresentationController?.prefersGrabberVisible = configuration.showGrabber - var detents: [UISheetPresentationController.Detent] = [.medium()] - if !detents.contains(.large()), - configuration.showGrabber, - configuration.extendableToFullSize { - detents.append(.large()) - } - self.sheetPresentationController?.detents = detents + /// Sets the correct sheet presentation style for sheets. + /// + /// - Parameter configuration: The `AutoResizingSheetConfiguration` to use. + func setSheetPresentationStyle(with configuration: AutoResizingSheetConfiguration) { + self.modalPresentationStyle = .pageSheet + self.sheetPresentationController?.prefersGrabberVisible = configuration.showGrabber + var detents: [UISheetPresentationController.Detent] = [.medium()] + if !detents.contains(.large()), + configuration.showGrabber, + configuration.extendableToFullSize { + detents.append(.large()) } + self.sheetPresentationController?.detents = detents + } } diff --git a/Sources/AutoResizingSheet/Extensions/UISheetPresentationController+Extension.swift b/Sources/AutoResizingSheet/Extensions/UISheetPresentationController+Extension.swift index 83e8db7..7ff5559 100644 --- a/Sources/AutoResizingSheet/Extensions/UISheetPresentationController+Extension.swift +++ b/Sources/AutoResizingSheet/Extensions/UISheetPresentationController+Extension.swift @@ -1,46 +1,46 @@ import UIKit extension UISheetPresentationController { - /// Updates the height of the `UISheetPresentationController` with animation - /// - /// - Parameter height: New height for the `UISheetPresentationController`. - func updateHeight(to height: CGFloat?) { - guard let height else { return } - - let heightDetent: UISheetPresentationController.Detent = .custom { _ in - height - } - var detents = [heightDetent] - - // Only add the large detent to resize if the grabber is shown and the heightDetent is not already large size - if self.prefersGrabberVisible, - !isHeightLargerThanLargeDetent(height: height) { - detents.append(.large()) - } - - animateDetentChange(to: detents) + /// Updates the height of the `UISheetPresentationController` with animation + /// + /// - Parameter height: New height for the `UISheetPresentationController`. + func updateHeight(to height: CGFloat?) { + guard let height else { return } + + let heightDetent: UISheetPresentationController.Detent = .custom { _ in + height } + var detents = [heightDetent] - private func isHeightLargerThanLargeDetent(height: CGFloat) -> Bool { - if let safeAreaInsetTop = containerView?.safeAreaInsets.top, - let safeAreaInsetBottom = containerView?.safeAreaInsets.bottom { - return (height + safeAreaInsetTop + safeAreaInsetBottom) > getViewHeight() - } else { - return (getViewHeight() * 0.9) < height - } + // Only add the large detent to resize if the grabber is shown and the heightDetent is not already large size + if self.prefersGrabberVisible, + !isHeightLargerThanLargeDetent(height: height) { + detents.append(.large()) } - private func getViewHeight() -> CGFloat { - guard let viewHeight = self.presentedView?.window?.windowScene?.screen.bounds.size.height else { - return UIScreen.main.bounds.height - } - - return viewHeight + animateDetentChange(to: detents) + } + + private func isHeightLargerThanLargeDetent(height: CGFloat) -> Bool { + if let safeAreaInsetTop = containerView?.safeAreaInsets.top, + let safeAreaInsetBottom = containerView?.safeAreaInsets.bottom { + return (height + safeAreaInsetTop + safeAreaInsetBottom) > getViewHeight() + } else { + return (getViewHeight() * 0.9) < height + } + } + + private func getViewHeight() -> CGFloat { + guard let viewHeight = self.presentedView?.window?.windowScene?.screen.bounds.size.height else { + return UIScreen.main.bounds.height } - private func animateDetentChange(to detents: [UISheetPresentationController.Detent]) { - self.animateChanges { - self.detents = detents - } + return viewHeight + } + + private func animateDetentChange(to detents: [UISheetPresentationController.Detent]) { + self.animateChanges { + self.detents = detents } + } } diff --git a/Sources/AutoResizingSheet/Extensions/View+Extension.swift b/Sources/AutoResizingSheet/Extensions/View+Extension.swift index 1bc0846..cea1c04 100644 --- a/Sources/AutoResizingSheet/Extensions/View+Extension.swift +++ b/Sources/AutoResizingSheet/Extensions/View+Extension.swift @@ -1,22 +1,22 @@ import SwiftUI extension View { - func readSize(onChange: @escaping (CGSize) -> Void) -> some View { - background( - GeometryReader { geometryProxy in - Color.clear - .preference(key: SizePreferenceKey.self, value: geometryProxy.size) - } - ) - .onPreferenceChange(SizePreferenceKey.self) { newValue in - DispatchQueue.main.async { - onChange(newValue) - } - } + func readSize(onChange: @escaping (CGSize) -> Void) -> some View { + background( + GeometryReader { geometryProxy in + Color.clear + .preference(key: SizePreferenceKey.self, value: geometryProxy.size) + } + ) + .onPreferenceChange(SizePreferenceKey.self) { newValue in + DispatchQueue.main.async { + onChange(newValue) + } } + } } fileprivate struct SizePreferenceKey: PreferenceKey { - static var defaultValue: CGSize = .zero - static func reduce(value: inout CGSize, nextValue: () -> CGSize) {} + static var defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) {} } diff --git a/Sources/AutoResizingSheet/Models/AutoResizingSheetConfiguration.swift b/Sources/AutoResizingSheet/Models/AutoResizingSheetConfiguration.swift index 2faaf85..f215596 100644 --- a/Sources/AutoResizingSheet/Models/AutoResizingSheetConfiguration.swift +++ b/Sources/AutoResizingSheet/Models/AutoResizingSheetConfiguration.swift @@ -2,34 +2,34 @@ import UIKit /// `AutoResizingSheetConfiguration` is a struct that defines the configuration for the auto resizing sheet. public struct AutoResizingSheetConfiguration { - public var scrollable: Bool - public var showGrabber: Bool - public var extendableToFullSize: Bool - public var scrollBackground: UIColor - - /// Creates a new instance of `AutoResizingSheetConfiguration`. - /// - /// - Note: The resizing will not work properly if your view is wrapped inside a `ScrollView`. Use `scrollable` of `AutoResizingSheetConfiguration` instead, to make the content scrollable. - /// - /// - Parameters: - /// - scrollable: Should the content be wrapped inside a scroll view. - /// Defaults to `true`. - /// - showGrabber: If the grabber should be shown. - /// Defaults to `true`. - /// - extendableToFullSize: If the sheet is extendable to full size using the grabber. - /// Defaults to `true`, will be `false` if `showGrabber` is `false`. - /// - scrollBackground: If scrollable, defines the background color of the `ScrollView`. - /// Defaults to `.clear`. - /// - public init( - scrollable: Bool = true, - showGrabber: Bool = true, - extendableToFullSize: Bool = true, - scrollBackground: UIColor = .clear - ) { - self.scrollable = scrollable - self.showGrabber = showGrabber - self.scrollBackground = scrollBackground - self.extendableToFullSize = extendableToFullSize - } + public var scrollable: Bool + public var showGrabber: Bool + public var extendableToFullSize: Bool + public var scrollBackground: UIColor + + /// Creates a new instance of `AutoResizingSheetConfiguration`. + /// + /// - Note: The resizing will not work properly if your view is wrapped inside a `ScrollView`. Use `scrollable` of `AutoResizingSheetConfiguration` instead, to make the content scrollable. + /// + /// - Parameters: + /// - scrollable: Should the content be wrapped inside a scroll view. + /// Defaults to `true`. + /// - showGrabber: If the grabber should be shown. + /// Defaults to `true`. + /// - extendableToFullSize: If the sheet is extendable to full size using the grabber. + /// Defaults to `true`, will be `false` if `showGrabber` is `false`. + /// - scrollBackground: If scrollable, defines the background color of the `ScrollView`. + /// Defaults to `.clear`. + /// + public init( + scrollable: Bool = true, + showGrabber: Bool = true, + extendableToFullSize: Bool = true, + scrollBackground: UIColor = .clear + ) { + self.scrollable = scrollable + self.showGrabber = showGrabber + self.scrollBackground = scrollBackground + self.extendableToFullSize = extendableToFullSize + } } diff --git a/Sources/AutoResizingSheet/View/AutoResizingSheetView.swift b/Sources/AutoResizingSheet/View/AutoResizingSheetView.swift index 7430076..ebb1986 100644 --- a/Sources/AutoResizingSheet/View/AutoResizingSheetView.swift +++ b/Sources/AutoResizingSheet/View/AutoResizingSheetView.swift @@ -8,47 +8,47 @@ import SwiftUI /// - scrollable: Should the content be wrapped inside a scroll view. /// - content: The content of the sheet view (e.g. a `VStack` with elements) struct AutoResizingSheetView: View { - var sheetPresentationController: UISheetPresentationController? - private let scrollable: Bool - private var content: Content - @State private var viewHeight: CGFloat? - - init( - sheetPresentationController: UISheetPresentationController?, - scrollable: Bool, - @ViewBuilder content: @escaping () -> Content - ) { - self.sheetPresentationController = sheetPresentationController - self.scrollable = scrollable - self.content = content() - } - - var body: some View { - Group { - if scrollable { - ScrollView(.vertical) { - content - .readSize { size in - updateViewHeight(newHeight: size.height) - } - } - } else { - VStack { - content - .readSize { size in - updateViewHeight(newHeight: size.height) - } - - Spacer() - } + var sheetPresentationController: UISheetPresentationController? + private let scrollable: Bool + private var content: Content + @State private var viewHeight: CGFloat? + + init( + sheetPresentationController: UISheetPresentationController?, + scrollable: Bool, + @ViewBuilder content: @escaping () -> Content + ) { + self.sheetPresentationController = sheetPresentationController + self.scrollable = scrollable + self.content = content() + } + + var body: some View { + Group { + if scrollable { + ScrollView(.vertical) { + content + .readSize { size in + updateViewHeight(newHeight: size.height) } } - } - - private func updateViewHeight(newHeight: CGFloat) { - if newHeight != viewHeight { - viewHeight = newHeight - sheetPresentationController?.updateHeight(to: viewHeight) + } else { + VStack { + content + .readSize { size in + updateViewHeight(newHeight: size.height) + } + + Spacer() } + } + } + } + + private func updateViewHeight(newHeight: CGFloat) { + if newHeight != viewHeight { + viewHeight = newHeight + sheetPresentationController?.updateHeight(to: viewHeight) } + } } diff --git a/Sources/AutoResizingSheet/ViewModifiers/AutoResizingSheetViewModifier.swift b/Sources/AutoResizingSheet/ViewModifiers/AutoResizingSheetViewModifier.swift index 557b869..bacc109 100644 --- a/Sources/AutoResizingSheet/ViewModifiers/AutoResizingSheetViewModifier.swift +++ b/Sources/AutoResizingSheet/ViewModifiers/AutoResizingSheetViewModifier.swift @@ -1,83 +1,83 @@ import SwiftUI struct AutoResizingSheetViewModifier: ViewModifier { - @Binding var isPresented: Bool - let configuration: AutoResizingSheetConfiguration - let sheetContent: () -> SheetContent - @State private var detents: Set - @State private var selectedDetent: PresentationDetent + @Binding var isPresented: Bool + let configuration: AutoResizingSheetConfiguration + let sheetContent: () -> SheetContent + @State private var detents: Set + @State private var selectedDetent: PresentationDetent + + init( + isPresented: Binding, + configuration: AutoResizingSheetConfiguration, + sheetContent: @escaping () -> SheetContent + ) { + _isPresented = isPresented + self.configuration = configuration + self.sheetContent = sheetContent as () -> SheetContent - init( - isPresented: Binding, - configuration: AutoResizingSheetConfiguration, - sheetContent: @escaping () -> SheetContent - ) { - _isPresented = isPresented - self.configuration = configuration - self.sheetContent = sheetContent as () -> SheetContent - - let initialDetent: PresentationDetent = .medium - _selectedDetent = State(initialValue: initialDetent) - _detents = State(initialValue: [initialDetent]) - } - - func body(content: Content) -> some View { - content - .sheet(isPresented: $isPresented) { - if configuration.scrollable { - ScrollView { - sheetContent() - .presentationDetents(detents, selection: $selectedDetent) - .presentationDragIndicator(configuration.showGrabber ? .visible : .hidden) - .readSize { newSize in - updateDetents(newHeigh: newSize.height) - } - } - .background(Color(configuration.scrollBackground)) - } else { - sheetContent() - .presentationDetents(detents, selection: $selectedDetent) - .presentationDragIndicator(configuration.showGrabber ? .visible : .hidden) - .readSize { newSize in - updateDetents(newHeigh: newSize.height) - } - } + let initialDetent: PresentationDetent = .medium + _selectedDetent = State(initialValue: initialDetent) + _detents = State(initialValue: [initialDetent]) + } + + func body(content: Content) -> some View { + content + .sheet(isPresented: $isPresented) { + if configuration.scrollable { + ScrollView { + sheetContent() + .presentationDetents(detents, selection: $selectedDetent) + .presentationDragIndicator(configuration.showGrabber ? .visible : .hidden) + .readSize { newSize in + updateDetents(newHeigh: newSize.height) + } + } + .background(Color(configuration.scrollBackground)) + } else { + sheetContent() + .presentationDetents(detents, selection: $selectedDetent) + .presentationDragIndicator(configuration.showGrabber ? .visible : .hidden) + .readSize { newSize in + updateDetents(newHeigh: newSize.height) } + } + } + } + + private func updateDetents(newHeigh: CGFloat) { + let newDetent = getNewDetent(height: newHeigh) + + if !detents.contains(newDetent) { + detents.insert(newDetent) } - private func updateDetents(newHeigh: CGFloat) { - let newDetent = getNewDetent(height: newHeigh) - - if !detents.contains(newDetent) { - detents.insert(newDetent) - } - - // Remove the initial medium detent - if detents.contains(.medium) { - detents.remove(.medium) - } - - // Adjust full size detent - let shouldShowFullSize = configuration.extendableToFullSize && configuration.showGrabber - if shouldShowFullSize, - !detents.contains(.large) { - detents.insert(.large) - } else { - detents.remove(.large) - } - - selectedDetent = newDetent + // Remove the initial medium detent + if detents.contains(.medium) { + detents.remove(.medium) } - private func getNewDetent(height: CGFloat) -> PresentationDetent { - if isHeightFullScreenHeight(height: height) { - return .large - } else { - return .height(height) - } + // Adjust full size detent + let shouldShowFullSize = configuration.extendableToFullSize && configuration.showGrabber + if shouldShowFullSize, + !detents.contains(.large) { + detents.insert(.large) + } else { + detents.remove(.large) } - private func isHeightFullScreenHeight(height: CGFloat) -> Bool { - height >= UIScreen.main.bounds.height + selectedDetent = newDetent + } + + private func getNewDetent(height: CGFloat) -> PresentationDetent { + if isHeightFullScreenHeight(height: height) { + return .large + } else { + return .height(height) } + } + + private func isHeightFullScreenHeight(height: CGFloat) -> Bool { + height >= UIScreen.main.bounds.height + } } diff --git a/Tests/AutoResizingSheetTests/AutoResizingSheetTests.swift b/Tests/AutoResizingSheetTests/AutoResizingSheetTests.swift index b1a9816..4f7c54a 100644 --- a/Tests/AutoResizingSheetTests/AutoResizingSheetTests.swift +++ b/Tests/AutoResizingSheetTests/AutoResizingSheetTests.swift @@ -2,14 +2,14 @@ import XCTest @testable import AutoResizingSheet final class AutoResizingSheetTests: XCTestCase { - func testConfigurationInit() { - let configuration = AutoResizingSheetConfiguration( - scrollable: false, - showGrabber: false, - extendableToFullSize: false - ) - XCTAssertEqual(configuration.scrollable, false) - XCTAssertEqual(configuration.showGrabber, false) - XCTAssertEqual(configuration.extendableToFullSize, false) - } + func testConfigurationInit() { + let configuration = AutoResizingSheetConfiguration( + scrollable: false, + showGrabber: false, + extendableToFullSize: false + ) + XCTAssertEqual(configuration.scrollable, false) + XCTAssertEqual(configuration.showGrabber, false) + XCTAssertEqual(configuration.extendableToFullSize, false) + } }