Adds support for limited toolbar placements

This commit is contained in:
Shaps Benkau 2023-01-31 14:46:46 +00:00
parent b53b53e9b0
commit 70abdd08b7
7 changed files with 229 additions and 30 deletions

View File

@ -1,25 +1,21 @@
import SwiftUI
public final class ImageRenderer<Content>: ObservableObject where Content: View {
public var content: Content
public var label: String?
public var proposedSize: ProposedViewSize = .unspecified
public var scale: CGFloat = PlatformScreen.mainScreen.scale
public var isOpaque: Bool = false
public var colorMode: ColorRenderingMode = .nonLinear
public extension Backport<Any> {
final class ImageRenderer<Content>: ObservableObject where Content: View {
public var content: Content
public var label: String?
public var proposedSize: ProposedViewSize = .unspecified
public var scale: CGFloat = PlatformScreen.mainScreen.scale
public var isOpaque: Bool = false
public var colorMode: ColorRenderingMode = .nonLinear
public init(content: Content) {
self.content = content
public init(content: Content) {
self.content = content
}
}
}
//public extension ImageRenderer {
// func render(rasterizationScale: CGFloat = 1, renderer: (CGSize, (CGContext) -> Void) -> Void) {
//
// }
//}
public extension ImageRenderer {
public extension Backport<Any>.ImageRenderer {
var cgImage: CGImage? {
#if os(macOS)
nsImage?.cgImage(forProposedRect: nil, context: .current, hints: nil)

View File

@ -54,7 +54,7 @@ internal struct PhotosPickerView: View {
.fixedSize()
}
Backport.ToolbarItem(placement: .status) {
Backport.ToolbarItem(placement: .bottomBar) {
VStack {
Text(selection.isEmpty ? "Select Items" : "Selected (\(selection.count))")
.font(.subheadline.weight(.semibold))

View File

@ -44,7 +44,7 @@ extension Image: Shareable {
let url = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("\(UUID().uuidString)")
.appendingPathExtension(pathExtension)
let renderer = ImageRenderer(content: self)
let renderer = Backport.ImageRenderer(content: self)
#if os(iOS)
let data = renderer.uiImage?.jpegData(compressionQuality: 0.8)

View File

@ -9,7 +9,7 @@ public extension Backport<Any> {
case cancellationAction
case destructiveAction
case principal
case status
case bottomBar
var isLeading: Bool {
switch self {
@ -64,13 +64,13 @@ struct ToolbarModifier: ViewModifier {
let leadingItems: [Backport<Any>.ToolbarItem]
let trailingItems: [Backport<Any>.ToolbarItem]
let principalItems: [Backport<Any>.ToolbarItem]
let statusItems: [Backport<Any>.ToolbarItem]
let bottomBarItems: [Backport<Any>.ToolbarItem]
init(items: [Backport<Any>.ToolbarItem]) {
leadingItems = items.filter { $0.placement.isLeading }
trailingItems = items.filter { $0.placement.isTrailing }
principalItems = items.filter { $0.placement == .principal }
statusItems = items.filter { $0.placement == .status }
bottomBarItems = items.filter { $0.placement == .bottomBar }
}
@ViewBuilder
@ -107,10 +107,10 @@ struct ToolbarModifier: ViewModifier {
}
@ViewBuilder
private var status: some View {
if !statusItems.isEmpty {
private var bottomBar: some View {
if !bottomBarItems.isEmpty {
HStack {
ForEach(statusItems, id: \.id) { item in
ForEach(bottomBarItems, id: \.id) { item in
item.content
}
}
@ -125,21 +125,38 @@ struct ToolbarModifier: ViewModifier {
.controller { controller in
if !principalItems.isEmpty {
controller?.navigationItem.titleView = UIHostingController(
rootView: principal,
rootView: principal.backport.largeScale(),
ignoreSafeArea: false
).view
controller?.navigationItem.titleView?.backgroundColor = .clear
}
if !statusItems.isEmpty {
if !bottomBarItems.isEmpty {
controller?.navigationController?.setToolbarHidden(false, animated: false)
controller?.toolbarItems = [
]
controller?.toolbarItems = bottomBarItems.map {
let view = UIHostingController(rootView: $0.content.backport.largeScale()).view!
view.backgroundColor = .clear
return .init(customView: view)
}
}
}
}
}
private extension Backport where Wrapped: View {
func largeScale() -> some View {
#if os(macOS)
if #available(macOS 11, *) {
content.imageScale(.large)
} else {
content
}
#else
content.imageScale(.large)
#endif
}
}
@available(iOS, introduced: 13, deprecated: 14)
public extension Backport where Wrapped: View {
@ViewBuilder

View File

@ -0,0 +1,35 @@
import SwiftUI
struct ToolbarViews {
var navigationBar: AnyView?
var bottomBar: AnyView?
var tabBar: AnyView?
}
struct ToolbarVisibility {
var navigationBar: Backport<Any>.Visibility?
var bottomBar: Backport<Any>.Visibility?
var tabBar: Backport<Any>.Visibility?
}
private struct ToolbarViewsKey: EnvironmentKey {
static var defaultValue: ToolbarViews = .init()
}
private struct ToolbarVisibilityKey: EnvironmentKey {
static var defaultValue: ToolbarVisibility = .init()
}
internal extension EnvironmentValues {
var toolbarViews: ToolbarViews {
get { self[ToolbarViewsKey.self] }
set { self[ToolbarViewsKey.self] = newValue }
}
}
internal extension EnvironmentValues {
var toolbarVisibility: ToolbarVisibility {
get { self[ToolbarVisibilityKey.self] }
set { self[ToolbarVisibilityKey.self] = newValue }
}
}

View File

@ -0,0 +1,58 @@
import SwiftUI
public extension Backport<Any> {
struct ToolbarPlacement: Hashable {
enum Placement: Hashable {
var id: Self { self }
case bottomBar
case navigationbar
case tabBar
}
let placement: Placement
/// The bottom toolbar of an app.
@available(macOS, unavailable)
public static var bottomBar: ToolbarPlacement {
.init(placement: .bottomBar)
}
/// The navigation bar of an app.
@available(macOS, unavailable)
public static var navigationBar: ToolbarPlacement {
.init(placement: .navigationbar)
}
/// The tab bar of an app.
@available(macOS, unavailable)
public static var tabBar: ToolbarPlacement {
.init(placement: .tabBar)
}
}
}
//@available(iOS, deprecated: 16)
//@available(macOS, deprecated: 13)
//@available(tvOS, unavailable)
//@available(watchOS, unavailable)
//public extension Backport where Wrapped: View {
// func toolbarBackground(_ visibility: Backport<Any>.Visibility, for bars: Backport<Any>.ToolbarPlacement...) -> some View {
// content
// .modifier(ToolbarBackgroundModifier())
// .environment(\.toolbarVisibility, .init(
// navigationBar: bars.contains(.navigationBar) ? visibility : nil,
// bottomBar: bars.contains(.bottomBar) ? visibility : nil,
// tabBar: bars.contains(.tabBar) ? visibility : nil)
// )
// }
//
// func toolbarBackground<S>(_ style: S, for bars: Backport<Any>.ToolbarPlacement...) -> some View where S: ShapeStyle & View {
// content
// .modifier(ToolbarBackgroundModifier())
// .environment(\.toolbarViews, .init(
// navigationBar: bars.contains(.navigationBar) ? .init(style) : nil,
// bottomBar: bars.contains(.bottomBar) ? .init(style) : nil,
// tabBar: bars.contains(.tabBar) ? .init(style) : nil)
// )
// }
//}

View File

@ -0,0 +1,93 @@
import SwiftUI
internal struct ToolbarBackgroundModifier: ViewModifier {
@Environment(\.toolbarViews) private var toolbarViews
@Environment(\.toolbarVisibility) private var toolbarVisibility
@Backport.StateObject private var wrapper: ControllerWrapper = .init()
func body(content: Content) -> some View {
content
.controller { controller in
wrapper.controller = controller
}
.backport.task {
updateNavigationBar()
updateBottomBar()
updateTabBar()
}
}
private func updateNavigationBar() {
guard toolbarVisibility.navigationBar != nil || toolbarViews.navigationBar != nil else { return }
guard let bar = wrapper.controller?.navigationController?.navigationBar else { return }
let appearance = UINavigationBarAppearance()
if let visibilty = toolbarVisibility.navigationBar, visibilty == .hidden {
appearance.configureWithTransparentBackground()
} else if let view = toolbarViews.navigationBar {
appearance.backgroundImage = Backport.ImageRenderer(
content: view
.frame(width: bar.bounds.width, height: bar.bounds.height)
.edgesIgnoringSafeArea(.all)
).uiImage
} else {
appearance.configureWithDefaultBackground()
}
bar.standardAppearance = appearance
bar.compactAppearance = appearance
bar.scrollEdgeAppearance = appearance
if #available(iOS 15.0, *) {
bar.compactScrollEdgeAppearance = appearance
}
}
private func updateBottomBar() {
guard toolbarVisibility.bottomBar != nil || toolbarViews.bottomBar != nil else { return }
guard let bar = wrapper.controller?.navigationController?.toolbar else { return }
let appearance = UIToolbarAppearance()
if let visibilty = toolbarVisibility.bottomBar, visibilty == .hidden {
appearance.configureWithTransparentBackground()
} else {
appearance.configureWithDefaultBackground()
}
bar.standardAppearance = appearance
bar.compactAppearance = appearance
if #available(iOS 15.0, *) {
bar.scrollEdgeAppearance = appearance
}
if #available(iOS 15.0, *) {
bar.compactScrollEdgeAppearance = appearance
}
}
private func updateTabBar() {
guard toolbarVisibility.tabBar != nil || toolbarViews.tabBar != nil else { return }
guard let bar = wrapper.controller?.tabBarController?.tabBar else { return }
let appearance = UITabBarAppearance()
if let visibilty = toolbarVisibility.tabBar, visibilty == .hidden {
appearance.configureWithTransparentBackground()
} else if let view = toolbarViews.tabBar {
appearance.backgroundImage = Backport.ImageRenderer(
content: view
.frame(width: bar.bounds.width, height: bar.bounds.height)
.edgesIgnoringSafeArea(.all)
).uiImage
} else {
appearance.configureWithDefaultBackground()
}
bar.standardAppearance = appearance
if #available(iOS 15.0, *) {
bar.scrollEdgeAppearance = appearance
}
}
}
private final class ControllerWrapper: ObservableObject {
weak var controller: UIViewController?
}