diff --git a/LNPopupUIExample/LNPopupUIExample/DemoScenes/PopupDemoView.swift b/LNPopupUIExample/LNPopupUIExample/DemoScenes/PopupDemoView.swift index 0a045dd..d037634 100644 --- a/LNPopupUIExample/LNPopupUIExample/DemoScenes/PopupDemoView.swift +++ b/LNPopupUIExample/LNPopupUIExample/DemoScenes/PopupDemoView.swift @@ -177,17 +177,16 @@ extension View { .popupImage(Image("genre\(demoContent.imageNumber)")) .popupProgress(0.5) .popupBarItems({ - HStack(spacing: 20) { - Button(action: { - print("Play") - }) { - Image(systemName: "play.fill") - } - Button(action: { - print("Next") - }) { - Image(systemName: "forward.fill") - } + Button(action: { + print("Play") + }) { + Image(systemName: "play.fill") + } + + Button(action: { + print("Next") + }) { + Image(systemName: "forward.fill") } }) } diff --git a/LNPopupUIExample/LNPopupUIExample/MusicScene/PlayerView.swift b/LNPopupUIExample/LNPopupUIExample/MusicScene/PlayerView.swift index 04347ad..1abd9d2 100644 --- a/LNPopupUIExample/LNPopupUIExample/MusicScene/PlayerView.swift +++ b/LNPopupUIExample/LNPopupUIExample/MusicScene/PlayerView.swift @@ -127,18 +127,16 @@ struct PlayerView: View { .popupImage(Image(song.imageName).resizable()) .popupProgress(playbackProgress) .popupBarItems({ - HStack(spacing: 20) { - Button(action: { - isPlaying.toggle() - }) { - Image(systemName: isPlaying ? "pause.fill" : "play.fill") - } - - Button(action: { - print("Next") - }) { - Image(systemName: "forward.fill") - } + Button(action: { + isPlaying.toggle() + }) { + Image(systemName: isPlaying ? "pause.fill" : "play.fill") + } + + Button(action: { + print("Next") + }) { + Image(systemName: "forward.fill") } }) } diff --git a/Package.swift b/Package.swift index 47f09d8..8da807c 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,7 @@ let package = Package( ], dependencies: [ // .package(path: "../LNPopupController") - .package(url: "https://github.com/LeoNatan/LNPopupController.git", from: Version(stringLiteral: "2.14.0")) + .package(url: "https://github.com/LeoNatan/LNPopupController.git", from: Version(stringLiteral: "2.14.1")) ], targets: [ .target( diff --git a/README.md b/README.md index 5bc972f..27ac580 100644 --- a/README.md +++ b/README.md @@ -86,18 +86,16 @@ VStack { .popupImage(Image(song.imageName)) .popupProgress(playbackProgress) .popupBarItems({ - HStack(spacing: 20) { - Button(action: { - isPlaying.toggle() - }) { - Image(systemName: "play.fill") - } - - Button(action: { - next() - }) { - Image(systemName: "forward.fill") - } + Button(action: { + isPlaying.toggle() + }) { + Image(systemName: "play.fill") + } + + Button(action: { + next() + }) { + Image(systemName: "forward.fill") } }) ``` diff --git a/Sources/LNPopupUI/LNPopupUI.swift b/Sources/LNPopupUI/LNPopupUI.swift index ead0d71..eb9eca8 100644 --- a/Sources/LNPopupUI/LNPopupUI.swift +++ b/Sources/LNPopupUI/LNPopupUI.swift @@ -8,6 +8,11 @@ import SwiftUI @_exported import LNPopupController +@available(iOS 14.0, *) +public extension ToolbarItemPlacement { + static let popupBar: ToolbarItemPlacement = .bottomBar +} + public extension View { /// Presents a popup bar with popup content. @@ -279,8 +284,9 @@ public extension View { /// /// - Parameters: /// - image: The image to use. - func popupImage(_ image: Image) -> some View { - return preference(key: LNPopupImagePreferenceKey.self, value: image) + /// - resizable: Mark the image as resizable. Defaults to `true`. If you'd like to control this on your own, set this parameter to `false`. + func popupImage(_ image: Image, resizable: Bool = true) -> some View { + return preference(key: LNPopupImagePreferenceKey.self, value: resizable ? image.resizable() : image) } /// Configures the view's popup bar progress. @@ -291,40 +297,127 @@ public extension View { return preference(key: LNPopupProgressPreferenceKey.self, value: progress) } + fileprivate func barItemContainer(@ViewBuilder _ content: () -> Content) -> AnyView where Content : View { + if #available(iOS 14.0, *) { + let view = NavigationView { + Color.clear.toolbar { + ToolbarItemGroup(placement: .popupBar) { + content() + } + } + }.navigationViewStyle(.stack) + + return AnyView(view) + } else { + return AnyView(content().edgesIgnoringSafeArea(.all)) + } + } + + @available(iOS 14.0, *) + fileprivate func barItemContainer(@ToolbarContentBuilder _ content: () -> Content) -> AnyView where Content : ToolbarContent { + let view = NavigationView { + Color.clear.toolbar { + content() + } + }.navigationViewStyle(.stack) + + return AnyView(view) + } + /// Sets the bar button items to display on the popup bar. /// /// @note For compact popup bars, this is equivalent to trailing button items. /// - /// - Parameter content: A view that appears on the trailing edge of the popup bar. + /// - Parameter content: A view representing the bar button items that appear on the popup bar. func popupBarItems(@ViewBuilder _ content: () -> Content) -> some View where Content : View { - return preference(key: LNPopupTrailingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: AnyView(content().edgesIgnoringSafeArea(.all)))) + let anyView = barItemContainer(content) + + return preference(key: LNPopupTrailingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: anyView)) + } + + /// Sets the bar button items to display on the popup bar. + /// + /// @note Only `ToolbarItem` and `ToolbarItemGroup` with a `.popupBar` placements are supported. For compact popup bars, this is equivalent to trailing button items. + /// + /// - Parameter content: Toolbar content representing the bar button items that appear on the popup bar. + @available(iOS 14.0, *) + func popupBarItems(@ToolbarContentBuilder _ content: () -> Content) -> some View where Content : ToolbarContent { + let anyView = barItemContainer(content) + + return preference(key: LNPopupTrailingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: AnyView(anyView))) } /// Sets the bar button items to display on the popup bar. /// /// @note For prominent popup bars, leading bar items are positioned in the trailing edge of the popup bar. /// - /// - Parameter leading: A view that appears on the leading edge of the popup bar. + /// - Parameter leading: A view representing the bar button items that appear on the leading edge of the popup bar. func popupBarItems(@ViewBuilder leading: () -> LeadingContent) -> some View where LeadingContent: View { - return preference(key: LNPopupLeadingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: AnyView(leading().edgesIgnoringSafeArea(.all)))) + let anyView = barItemContainer(leading) + + return preference(key: LNPopupLeadingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: anyView)) } /// Sets the bar button items to display on the popup bar. /// - /// - Parameter trailing: A view that appears on the trailing edge of the popup bar. + /// @note Only `ToolbarItem` and `ToolbarItemGroup` with a `.popupBar` placements are supported. For prominent popup bars, leading bar items are positioned in the trailing edge of the popup bar. + /// + /// - Parameter leading: Toolbar content representing the bar button items that appear on the leading edge of the popup bar. + @available(iOS 14.0, *) + func popupBarItems(@ToolbarContentBuilder leading: () -> LeadingContent) -> some View where LeadingContent: ToolbarContent { + let anyView = barItemContainer(leading) + + return preference(key: LNPopupLeadingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: anyView)) + } + + /// Sets the bar button items to display on the popup bar. + /// + /// - Parameter trailing: A view representing the bar button items that appear on the trailing edge of the popup bar. func popupBarItems(@ViewBuilder trailing: () -> TrailingContent) -> some View where TrailingContent: View { - return preference(key: LNPopupTrailingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: AnyView(trailing().edgesIgnoringSafeArea(.all)))) + let anyView = barItemContainer(trailing) + + return preference(key: LNPopupTrailingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: anyView)) + } + + /// Sets the bar button items to display on the popup bar. + /// + /// @note Only `ToolbarItem` and `ToolbarItemGroup` with a `.popupBar` placements are supported. + /// + /// - Parameter trailing: Toolbar content representing the bar button items that appear on the trailing edge of the popup bar. + @available(iOS 14.0, *) + func popupBarItems(@ToolbarContentBuilder trailing: () -> TrailingContent) -> some View where TrailingContent: ToolbarContent { + let anyView = barItemContainer(trailing) + + return preference(key: LNPopupTrailingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: anyView)) } /// Sets the bar button items to display on the popup bar. /// /// @note For prominent popup bars, leading and trailing bar items are positioned in the trailing edge of the popup bar. /// - /// - Parameter leading: A view that appears on the leading edge of the popup bar. - /// - Parameter trailing: A view that appears on the trailing edge of the popup bar. + /// - Parameter leading: A view representing the bar button items that appear on the leading edge of the popup bar. + /// - Parameter trailing: A view representing the bar button items that appear on the trailing edge of the popup bar. func popupBarItems(@ViewBuilder leading: () -> LeadingContent, @ViewBuilder trailing: () -> TrailingContent) -> some View where LeadingContent: View, TrailingContent: View { - return preference(key: LNPopupLeadingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: AnyView((leading())))) - .preference(key: LNPopupTrailingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: AnyView((trailing())))) + let leadingAnyView = barItemContainer(leading) + let trailingAnyView = barItemContainer(trailing) + + return preference(key: LNPopupLeadingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: leadingAnyView)) + .preference(key: LNPopupTrailingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: trailingAnyView)) + } + + /// Sets the bar button items to display on the popup bar. + /// + /// @note Only `ToolbarItem` and `ToolbarItemGroup` with a `.popupBar` placements are supported. For prominent popup bars, leading and trailing bar items are positioned in the trailing edge of the popup bar. + /// + /// - Parameter leading: Toolbar content representing the bar button items that appear on the leading edge of the popup bar. + /// - Parameter trailing: Toolbar content representing the bar button items that appear on the trailing edge of the popup bar. + @available(iOS 14.0, *) + func popupBarItems(@ToolbarContentBuilder leading: () -> LeadingContent, @ToolbarContentBuilder trailing: () -> TrailingContent) -> some View where LeadingContent: ToolbarContent, TrailingContent: ToolbarContent { + let leadingAnyView = barItemContainer(leading) + let trailingAnyView = barItemContainer(trailing) + + return preference(key: LNPopupLeadingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: leadingAnyView)) + .preference(key: LNPopupTrailingBarItemsPreferenceKey.self, value: LNPopupAnyViewWrapper(anyView: trailingAnyView)) } /// Designates this view as the popup interaction container. Only gestures within this view will be considered for popup interaction, such as dismissal. diff --git a/Sources/LNPopupUI/Private/LNPopupProxyViewController.swift b/Sources/LNPopupUI/Private/LNPopupProxyViewController.swift index 7742be3..ba155dd 100644 --- a/Sources/LNPopupUI/Private/LNPopupProxyViewController.swift +++ b/Sources/LNPopupUI/Private/LNPopupProxyViewController.swift @@ -9,6 +9,46 @@ import SwiftUI import UIKit import LNPopupController +internal class LNPopupBarItemAdapter: UIHostingController { + let updater: ([UIBarButtonItem]?) -> Void + var doneUpdating = false + + @objc var overrideSizeClass: UIUserInterfaceSizeClass = .regular { + didSet { + self.setValue(UITraitCollection(verticalSizeClass: overrideSizeClass), forKey: "overrideTraitCollection") + } + } + + required init(rootView: AnyView, updater: @escaping ([UIBarButtonItem]?) -> Void) { + self.updater = updater + + super.init(rootView: rootView) + + self.setValue(UITraitCollection(verticalSizeClass: overrideSizeClass), forKey: "overrideTraitCollection") + } + + required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + guard doneUpdating == false else { + return + } + + let nav = self.children.first as! UINavigationController + let toolbarItems = nav.toolbar.items ?? [] + let allItems = toolbarItems + + if allItems.count > 0 { + self.updater(allItems) + doneUpdating = true + } + } +} + internal class LNPopupProxyViewController : UIHostingController, LNPopupPresentationDelegate, UIContextMenuInteractionDelegate where Content: View, PopupContent: View { var currentPopupState: LNPopupState! = nil var popupViewController: UIViewController? @@ -16,10 +56,13 @@ internal class LNPopupProxyViewController : UIHostingCont var popupContextMenuViewController: UIHostingController? var popupContextMenuInteraction: UIContextMenuInteraction? - var leadingBarItemsController: UIHostingController? = nil - var trailingBarItemsController: UIHostingController? = nil - var leadingBarButtonItem: UIBarButtonItem? = nil - var trailingBarButtonItem: UIBarButtonItem? = nil + var leadingBarItemsController: LNPopupBarItemAdapter? = nil + var trailingBarItemsController: LNPopupBarItemAdapter? = nil + + var legacy_leadingBarItemsController: UIHostingController? = nil + var legacy_trailingBarItemsController: UIHostingController? = nil + var legacy_leadingBarButtonItem: UIBarButtonItem? = nil + var legacy_trailingBarButtonItem: UIBarButtonItem? = nil weak var interactionContainerView: UIView? @@ -47,7 +90,7 @@ internal class LNPopupProxyViewController : UIHostingCont return children.first ?? self } - fileprivate func createOrUpdateHostingControllerForAnyView(_ vc: inout UIHostingController?, view: AnyView, barButtonItem: inout UIBarButtonItem?, targetBarButtons: ([UIBarButtonItem]) -> Void, leadSpacing: Bool, trailingSpacing: Bool) { + fileprivate func legacy_createOrUpdateHostingControllerForAnyView(_ vc: inout UIHostingController?, view: AnyView, barButtonItem: inout UIBarButtonItem?, targetBarButtons: ([UIBarButtonItem]) -> Void, leadSpacing: Bool, trailingSpacing: Bool) { let anyView = AnyView(erasing: view.font(.system(size: 20))) @@ -61,7 +104,7 @@ internal class LNPopupProxyViewController : UIHostingCont vc.view.widthAnchor.constraint(equalToConstant: size.width), vc.view.heightAnchor.constraint(equalToConstant: min(size.height, 44)), ]) - + barButtonItem!.customView = vc.view } else { vc = UIHostingController(rootView: anyView) @@ -73,6 +116,7 @@ internal class LNPopupProxyViewController : UIHostingCont vc!.view.heightAnchor.constraint(equalToConstant: min(size.height, 44)), ]) + vc!.view.clipsToBounds = false barButtonItem = UIBarButtonItem(customView: vc!.view) targetBarButtons([barButtonItem!]) @@ -80,6 +124,17 @@ internal class LNPopupProxyViewController : UIHostingCont } } + @available(iOS 14.0, *) + fileprivate func createOrUpdateBarItemAdapter(_ vc: inout LNPopupBarItemAdapter?, userNavigationViewWrapper anyView: AnyView, barButtonUpdater: @escaping ([UIBarButtonItem]?) -> Void) { + UIView.performWithoutAnimation { + if let vc = vc { + vc.rootView = anyView + } else { + vc = LNPopupBarItemAdapter(rootView: anyView, updater: barButtonUpdater) + } + } + } + func anyView(fromView view: AnyView, isTitle: Bool) -> AnyView { let font = self.target.popupBar.value(forKey: isTitle ? "_titleFont" : "_subtitleFont") as! CTFont let color = self.target.popupBar.value(forKey: isTitle ? "_titleColor" : "_subtitleColor") as! UIColor @@ -133,12 +188,22 @@ internal class LNPopupProxyViewController : UIHostingCont } .onPreferenceChange(LNPopupLeadingBarItemsPreferenceKey.self) { [weak self] view in if let self = self, let anyView = view?.anyView, let popupItem = self.popupViewController?.popupItem { - self.createOrUpdateHostingControllerForAnyView(&self.leadingBarItemsController, view: anyView, barButtonItem: &self.leadingBarButtonItem, targetBarButtons: { popupItem.leadingBarButtonItems = $0 }, leadSpacing: false, trailingSpacing: false) + if #available(iOS 14.0, *) { + self.createOrUpdateBarItemAdapter(&self.leadingBarItemsController, userNavigationViewWrapper: anyView, barButtonUpdater: { popupItem.leadingBarButtonItems = $0 }) + popupItem.setValue(self.leadingBarItemsController!, forKey: "swiftuiHiddenLeadingController") + } else { + self.legacy_createOrUpdateHostingControllerForAnyView(&self.legacy_leadingBarItemsController, view: anyView, barButtonItem: &self.legacy_leadingBarButtonItem, targetBarButtons: { popupItem.leadingBarButtonItems = $0 }, leadSpacing: false, trailingSpacing: false) + } } } .onPreferenceChange(LNPopupTrailingBarItemsPreferenceKey.self) { [weak self] view in if let self = self, let anyView = view?.anyView, let popupItem = self.popupViewController?.popupItem { - self.createOrUpdateHostingControllerForAnyView(&self.trailingBarItemsController, view: anyView, barButtonItem: &self.trailingBarButtonItem, targetBarButtons: { popupItem.trailingBarButtonItems = $0 }, leadSpacing: false, trailingSpacing: false) + if #available(iOS 14.0, *) { + self.createOrUpdateBarItemAdapter(&self.trailingBarItemsController, userNavigationViewWrapper: anyView, barButtonUpdater: { popupItem.trailingBarButtonItems = $0 }) + popupItem.setValue(self.trailingBarItemsController!, forKey: "swiftuiHiddenTrailingController") + } else { + self.legacy_createOrUpdateHostingControllerForAnyView(&self.legacy_trailingBarItemsController, view: anyView, barButtonItem: &self.legacy_trailingBarButtonItem, targetBarButtons: { popupItem.trailingBarButtonItems = $0 }, leadSpacing: false, trailingSpacing: false) + } } } }()