AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/SwiftUI/ViewComponents/Drawer/BottomDrawer.swift (204 lines of code) (raw):
//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
import SwiftUI
import FluentUI
internal enum DrawerState {
case gone
case hidden
case visible
}
internal enum DrawerConstants {
// How round is the drawer handle
static let drawerHandleCornerRadius: CGFloat = 4
static let placeHolderPadding: CGFloat = 8
// How round is the drawer itself
static let drawerCornerRadius: CGFloat = 16
// How much shadow is under the drawer
static let drawerShadowRadius: CGFloat = 10
// How much "fill" below the content to push it off the bottom os the screen
static let bottomFillY: CGFloat = 48
// Opacity of faded items (like background overlay)
static let overlayOpacity: CGFloat = 0.4
// How much drag you need on the drawer to dismiss in that way
static let dragThreshold: CGFloat = 200
// After hiding, the delay before making it "gone", accounts for animation
static let delayUntilGone: CGFloat = 0.3
static let collapsedHeight: CGFloat = 200
static let expandedHeight: CGFloat = UIScreen.main.bounds.height * 0.7
static let textBoxHeight: CGFloat = 40
static let textBoxPaddingBottom: CGFloat = 10
}
/// Bottom Drawer w/Swift UI Support
///
/// How it works:
/// When enabling/disabling, state goes Gone->Hidden->Visible, or Visible->HIdden->Gone
/// Gone is means the content is not included in the view
/// Hidden is the "off screen state" to allow for animation transitions
/// Visible is the "on screen state" when it's displayed
///
/// When tapping out, or dragging down on the handle, the hideDrawer functions is called
///
/// Examples Usage:
/// BottomDrawer(isPresented: viewModel.supportFormViewModel.isDisplayed,
/// hideDrawer: viewModel.supportFormViewModel.hideForm) {
/// reportErrorView
/// .accessibilityElement(children: .contain)
/// .accessibilityAddTraits(.isModal)
/// }
///
/// Typically used (if presenting a list) with DrawerListView
///
internal struct BottomDrawer<Content: View>: View {
@State private var drawerState: DrawerState = .gone
@State private var dragOffset: CGFloat = 0
@State private var drawerHeight: CGFloat = DrawerConstants.collapsedHeight
let isPresented: Bool
let hideDrawer: () -> Void
let content: Content
let startIcon: CompositeIcon?
let startIconAction: (() -> Void)?
let title: String?
var dragThreshold: CGFloat = DrawerConstants.dragThreshold
init(isPresented: Bool,
hideDrawer: @escaping () -> Void,
title: String? = nil,
startIcon: CompositeIcon? = nil,
startIconAction: (() -> Void)? = nil,
dragThreshold: CGFloat = DrawerConstants.dragThreshold,
@ViewBuilder content: () -> Content) {
self.isPresented = isPresented
self.content = content()
self.hideDrawer = hideDrawer
self.startIcon = startIcon
self.startIconAction = startIconAction
self.title = title
self.dragThreshold = dragThreshold
}
var body: some View {
ZStack(alignment: .bottom) {
if drawerState != .gone {
overlayView
drawerView
.transition(.move(edge: .bottom))
.offset(y: drawerState == .hidden ? UIScreen.main.bounds.height : max(dragOffset, 0))
.animation(.easeInOut, value: drawerState == .visible)
.gesture(
DragGesture()
.onChanged { value in
dragOffset = value.translation.height
let newHeight = drawerHeight - value.translation.height
drawerHeight = min(max(newHeight, DrawerConstants.collapsedHeight),
DrawerConstants.expandedHeight)
}
.onEnded { value in
withAnimation {
if value.translation.height > dragThreshold {
collapseDrawer()
} else if value.translation.height < -dragThreshold {
expandDrawer()
} else {
resetDrawer()
}
}
dragOffset = 0
}
)
.accessibilityAddTraits(.isModal)
}
}
.onChange(of: isPresented) { newValue in
if newValue {
showDrawer()
} else {
hideDrawerAnimated()
}
}
}
private var drawerView: some View {
VStack {
Spacer()
VStack {
handleView
if title != nil {
titleView
}
content
Spacer().frame(height: DrawerConstants.bottomFillY)
}
.frame(maxWidth: .infinity, alignment: .bottom)
.background(Color(StyleProvider.color.drawerColor))
.cornerRadius(DrawerConstants.drawerCornerRadius)
.shadow(radius: DrawerConstants.drawerShadowRadius)
.padding(.bottom, -DrawerConstants.bottomFillY)
}
}
private var handleView: some View {
RoundedRectangle(cornerRadius: DrawerConstants.drawerHandleCornerRadius)
.fill(Color.gray.opacity(0.6))
.frame(width: 36, height: 4)
.padding(.top, 5)
}
private var titleView: some View {
VStack {
HStack(spacing: 8) {
if let icon = startIcon {
Icon(name: icon, size: DrawerListConstants.iconSize)
.foregroundColor(Color(StyleProvider.color.drawerIconDark))
.padding(.leading, 15)
.padding(.top, 15)
.onTapGesture {
startIconAction?()
}
}
Spacer()
}
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.trailing, 10)
.padding(.top, 5)
}
.overlay(
Text(title ?? "")
.font(.headline)
.foregroundColor(.primary)
.accessibilityAddTraits(.isHeader)
.padding(.top, 20),
alignment: .center
)
}
private var overlayView: some View {
let overlayOpacity = (drawerState == .visible) ? DrawerConstants.overlayOpacity : 0
return Color.black.opacity(overlayOpacity)
.ignoresSafeArea()
.onTapGesture {
hideDrawer()
}
.accessibilityHidden(true)
}
// MARK: - Helper Functions
private func showDrawer() {
drawerState = .hidden
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
withAnimation {
drawerState = .visible
drawerHeight = DrawerConstants.collapsedHeight
}
}
}
private func hideDrawerAnimated() {
withAnimation {
drawerState = .hidden
}
DispatchQueue.main.asyncAfter(deadline: .now() + DrawerConstants.delayUntilGone) {
drawerState = .gone
}
}
private func collapseDrawer() {
drawerState = .hidden
hideDrawer()
drawerHeight = DrawerConstants.collapsedHeight
}
private func expandDrawer() {
drawerState = .visible
drawerHeight = DrawerConstants.expandedHeight
}
private func resetDrawer() {
if drawerHeight > (DrawerConstants.collapsedHeight + DrawerConstants.expandedHeight) / 2 {
expandDrawer()
} else {
collapseDrawer()
}
}
}
struct ButtonActions {
let showSharingViewAction: (() -> Void)?
let showSupportFormAction: (() -> Void)?
let showCaptionsViewAction: (() -> Void)?
let showSpokenLanguage: (() -> Void)?
let showCaptionsLanguage: (() -> Void)?
let showRttView: (() -> Void)?
init(
showSharingViewAction: (() -> Void)? = { },
showSupportFormAction: (() -> Void)? = { },
showCaptionsViewAction: (() -> Void)? = { },
showSpokenLanguage: (() -> Void)? = { },
showCaptionsLanguage: (() -> Void)? = { },
showRttView: (() -> Void)? = { }
) {
self.showSharingViewAction = showSharingViewAction
self.showSupportFormAction = showSupportFormAction
self.showCaptionsViewAction = showCaptionsViewAction
self.showSpokenLanguage = showSpokenLanguage
self.showCaptionsLanguage = showCaptionsLanguage
self.showRttView = showRttView
}
}