Skip to content

Commit

Permalink
Improve popup bar item support for SwiftUI
Browse files Browse the repository at this point in the history
  • Loading branch information
LeoNatan committed Oct 10, 2022
1 parent 5164d9b commit 8b1d332
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 56 deletions.
21 changes: 10 additions & 11 deletions LNPopupUIExample/LNPopupUIExample/DemoScenes/PopupDemoView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
})
}
Expand Down
22 changes: 10 additions & 12 deletions LNPopupUIExample/LNPopupUIExample/MusicScene/PlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
})
}
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
22 changes: 10 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
})
```
Expand Down
117 changes: 105 additions & 12 deletions Sources/LNPopupUI/LNPopupUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -291,40 +297,127 @@ public extension View {
return preference(key: LNPopupProgressPreferenceKey.self, value: progress)
}

fileprivate func barItemContainer<Content>(@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<Content>(@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<Content>(@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<Content>(@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<LeadingContent>(@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<LeadingContent>(@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<TrailingContent>(@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<TrailingContent>(@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<LeadingContent, TrailingContent>(@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<LeadingContent, TrailingContent>(@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.
Expand Down
81 changes: 73 additions & 8 deletions Sources/LNPopupUI/Private/LNPopupProxyViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,60 @@ import SwiftUI
import UIKit
import LNPopupController

internal class LNPopupBarItemAdapter: UIHostingController<AnyView> {
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<Content, PopupContent> : UIHostingController<Content>, LNPopupPresentationDelegate, UIContextMenuInteractionDelegate where Content: View, PopupContent: View {
var currentPopupState: LNPopupState<PopupContent>! = nil
var popupViewController: UIViewController?

var popupContextMenuViewController: UIHostingController<AnyView>?
var popupContextMenuInteraction: UIContextMenuInteraction?

var leadingBarItemsController: UIHostingController<AnyView>? = nil
var trailingBarItemsController: UIHostingController<AnyView>? = nil
var leadingBarButtonItem: UIBarButtonItem? = nil
var trailingBarButtonItem: UIBarButtonItem? = nil
var leadingBarItemsController: LNPopupBarItemAdapter? = nil
var trailingBarItemsController: LNPopupBarItemAdapter? = nil

var legacy_leadingBarItemsController: UIHostingController<AnyView>? = nil
var legacy_trailingBarItemsController: UIHostingController<AnyView>? = nil
var legacy_leadingBarButtonItem: UIBarButtonItem? = nil
var legacy_trailingBarButtonItem: UIBarButtonItem? = nil

weak var interactionContainerView: UIView?

Expand Down Expand Up @@ -47,7 +90,7 @@ internal class LNPopupProxyViewController<Content, PopupContent> : UIHostingCont
return children.first ?? self
}

fileprivate func createOrUpdateHostingControllerForAnyView(_ vc: inout UIHostingController<AnyView>?, view: AnyView, barButtonItem: inout UIBarButtonItem?, targetBarButtons: ([UIBarButtonItem]) -> Void, leadSpacing: Bool, trailingSpacing: Bool) {
fileprivate func legacy_createOrUpdateHostingControllerForAnyView(_ vc: inout UIHostingController<AnyView>?, view: AnyView, barButtonItem: inout UIBarButtonItem?, targetBarButtons: ([UIBarButtonItem]) -> Void, leadSpacing: Bool, trailingSpacing: Bool) {

let anyView = AnyView(erasing: view.font(.system(size: 20)))

Expand All @@ -61,7 +104,7 @@ internal class LNPopupProxyViewController<Content, PopupContent> : UIHostingCont
vc.view.widthAnchor.constraint(equalToConstant: size.width),
vc.view.heightAnchor.constraint(equalToConstant: min(size.height, 44)),
])

barButtonItem!.customView = vc.view
} else {
vc = UIHostingController<AnyView>(rootView: anyView)
Expand All @@ -73,13 +116,25 @@ internal class LNPopupProxyViewController<Content, PopupContent> : UIHostingCont
vc!.view.heightAnchor.constraint(equalToConstant: min(size.height, 44)),
])

vc!.view.clipsToBounds = false
barButtonItem = UIBarButtonItem(customView: vc!.view)

targetBarButtons([barButtonItem!])
}
}
}

@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
Expand Down Expand Up @@ -133,12 +188,22 @@ internal class LNPopupProxyViewController<Content, PopupContent> : 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)
}
}
}
}()
Expand Down

0 comments on commit 8b1d332

Please sign in to comment.