Skip to content

Popup View - Collapsable elements #147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Sep 19, 2022
68 changes: 21 additions & 47 deletions Examples/Examples/PopupExampleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,22 @@ struct PopupExampleView: View {

return Map(item: portalItem1)
}

/// The map displayed in the map view.
@StateObject private var map = makeMap()

/// The point on the screen the user tapped on to identify a feature.
@State private var identifyScreenPoint: CGPoint?

/// The result of the layer identify operation.
@State private var identifyResult: Result<[IdentifyLayerResult], Error>?


/// The popup to be shown as the result of the layer identify operation.
@State private var popup: Popup?

/// A Boolean value specifying whether the popup view should be shown or not.
@State private var showPopup = false

/// The detent value specifying the initial `FloatingPanelDetent`. Defaults to "full".
@State private var floatingPanelDetent: FloatingPanelDetent = .full

var body: some View {
MapViewReader { proxy in
VStack {
Expand All @@ -76,16 +82,19 @@ struct PopupExampleView: View {
return
}

self.identifyResult = identifyResult
self.identifyScreenPoint = nil
self.popup = try? identifyResult.get().first?.popups.first
self.showPopup = self.popup != nil
}
.overlay(alignment: .topLeading) {
.floatingPanel(
detent: $floatingPanelDetent,
horizontalAlignment: .leading,
isPresented: $showPopup
) {
Group {
if identifyScreenPoint != nil {
ProgressView()
.esriBorder()
} else if let identifyResult = identifyResult {
IdentifyResultView(identifyResult: identifyResult)
if let popup = popup {
PopupView(popup: popup, isPresented: $showPopup)
.showCloseButton(true)
}
}
.padding()
Expand All @@ -94,38 +103,3 @@ struct PopupExampleView: View {
}
}
}

/// A view displaying the results of an identify operation.
private struct IdentifyResultView: View {
var identifyResult: Result<[IdentifyLayerResult], Error>

@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass

/// If `true`, will draw the popup view at half height, exposing a portion of the
/// underlying map below the view on an iPhone in portrait orientation (and certain iPad multitasking
/// configurations). If `false`, will draw the popup view full size.
private var useHalfHeightResults: Bool {
horizontalSizeClass == .compact && verticalSizeClass == .regular
}

var body: some View {
switch identifyResult {
case .success(let identifyLayerResults):
// Get the first popup from the first layer result.
if let popup = identifyLayerResults.first?.popups.first {
GeometryReader { geometry in
PopupView(popup: popup)
.frame(
maxWidth: useHalfHeightResults ? .infinity : 400,
maxHeight: useHalfHeightResults ? geometry.size.height / 2 : nil
)
.esriBorder()
}
}
case .failure(let error):
Text("Identify error: \(error.localizedDescription).")
.esriBorder()
}
}
}
90 changes: 47 additions & 43 deletions Sources/ArcGISToolkit/Components/FloatingPanel/FloatingPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ struct FloatingPanel<Content>: View where Content: View {
@ViewBuilder content: () -> Content
) {
self.backgroundColor = backgroundColor
self.activeDetent = detent
self.isPresented = isPresented
self.content = content()
_activeDetent = detent
_isPresented = isPresented
}

/// A binding to the currently selected detent.
@Binding private var activeDetent: FloatingPanelDetent
private var activeDetent: Binding<FloatingPanelDetent>

/// The color of the handle.
@State private var handleColor: Color = .defaultHandleColor
Expand All @@ -61,7 +61,7 @@ struct FloatingPanel<Content>: View where Content: View {
@State private var height: CGFloat = .minHeight

/// A binding to a Boolean value that determines whether the view is presented.
@Binding private var isPresented: Bool
private var isPresented: Binding<Bool>

/// The maximum allowed height of the content.
@State private var maximumHeight: CGFloat = .infinity
Expand All @@ -72,50 +72,54 @@ struct FloatingPanel<Content>: View where Content: View {
}

public var body: some View {
GeometryReader { geometryProxy in
VStack(spacing: 0) {
if isCompact && isPresented {
makeHandleView()
if isPresented.wrappedValue {
GeometryReader { geometryProxy in
VStack(spacing: 0) {
if isCompact {
makeHandleView()
Divider()
}
content
.frame(height: height)
.padding(.bottom, isCompact ? 25 : .zero)
if !isCompact {
Divider()
makeHandleView()
}
}
content
.frame(height: height)
.padding(.bottom, isCompact ? 25 : .zero)
if !isCompact && isPresented {
makeHandleView()
.background(backgroundColor)
.cornerRadius(10, corners: isCompact ? [.topLeft, .topRight] : [.allCorners])
.shadow(radius: 10)
.opacity(isPresented.wrappedValue ? 1.0 : .zero)
.frame(
width: geometryProxy.size.width,
height: geometryProxy.size.height,
alignment: isCompact ? .bottom : .top
)
.onSizeChange {
maximumHeight = $0.height
if height > maximumHeight {
height = maximumHeight
}
}
}
.background(backgroundColor)
.cornerRadius(10, corners: isCompact ? [.topLeft, .topRight] : [.allCorners])
.shadow(radius: 10)
.opacity(isPresented ? 1.0 : .zero)
.frame(
width: geometryProxy.size.width,
height: geometryProxy.size.height,
alignment: isCompact ? .bottom : .top
)
.onSizeChange {
maximumHeight = $0.height
if height > maximumHeight {
height = maximumHeight
.onChange(of: activeDetent.wrappedValue) { _ in
withAnimation {
height = heightFor(detent: activeDetent.wrappedValue)
}
}
}
.onChange(of: activeDetent) { _ in
withAnimation {
height = heightFor(detent: activeDetent)
.onChange(of: isPresented.wrappedValue) {
height = $0 ? heightFor(detent: activeDetent.wrappedValue) : .zero
}
}
.onChange(of: isPresented) {
height = $0 ? heightFor(detent: activeDetent) : .zero
}
.onAppear {
withAnimation {
height = heightFor(detent: activeDetent)
.onAppear {
withAnimation {
height = heightFor(detent: activeDetent.wrappedValue)
}
}
.animation(.default, value: isPresented.wrappedValue)
}
.animation(.default, value: isPresented)
.padding([.leading, .top, .trailing], isCompact ? 0 : 10)
.padding([.bottom], isCompact ? 0 : 50)
}
.padding([.leading, .top, .trailing], isCompact ? 0 : 10)
.padding([.bottom], isCompact ? 0 : 50)
}

var drag: some Gesture {
Expand All @@ -139,7 +143,7 @@ struct FloatingPanel<Content>: View where Content: View {
.onEnded { _ in
handleColor = .defaultHandleColor
withAnimation {
activeDetent = closestDetent
activeDetent.wrappedValue = closestDetent
height = heightFor(detent: closestDetent)
}
}
Expand All @@ -163,7 +167,7 @@ struct FloatingPanel<Content>: View where Content: View {
case .half:
return maximumHeight * 0.4
case .full:
return maximumHeight * 0.90
return maximumHeight * (isCompact ? 0.90 : 1.0)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,17 @@ struct AttachmentsPopupElementView: View {
)
}

@State var isExpanded: Bool = true

var body: some View {
Group {
if isLoadingAttachments {
ProgressView()
.padding()
} else if viewModel.attachmentModels.count > 0 {
Divider()
VStack(alignment: .leading) {
PopupElementHeader(
title: popupElement.title,
description: popupElement.description
)

DisclosureGroup(isExpanded: $isExpanded) {
Divider()
.padding(.bottom, 4)
switch popupElement.displayType {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you get this warning too?

Screen Shot 2022-09-19 at 09 57 43

case .list:
AttachmentList(attachmentModels: viewModel.attachmentModels)
Expand All @@ -68,7 +66,15 @@ struct AttachmentsPopupElementView: View {
AttachmentList(attachmentModels: viewModel.attachmentModels)
}
}
} label: {
VStack(alignment: .leading) {
PopupElementHeader(
title: popupElement.displayTitle,
description: popupElement.description
)
}
}
Divider()
}
}
.task {
Expand All @@ -81,3 +87,10 @@ struct AttachmentsPopupElementView: View {
}
}
}

private extension AttachmentsPopupElement {
/// Provides a default title to display if `title` is empty.
var displayTitle: String {
title.isEmpty ? "Attachments" : title
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,22 @@ struct FieldsPopupElementView: View {
/// The labels and values to display, as an array of `DisplayField`s.
private let displayFields: [DisplayField]

@State var isExpanded: Bool = true

var body: some View {
DisclosureGroup(isExpanded: $isExpanded) {
Divider()
.padding(.bottom, 4)
FieldsList(fields: displayFields)
} label: {
VStack(alignment: .leading) {
PopupElementHeader(
title: popupElement.displayTitle,
description: popupElement.description
)
}
}
Divider()
PopupElementHeader(
title: popupElement.displayTitle,
description: popupElement.description
)
.padding([.bottom], 4)
FieldsList(fields: displayFields)
}

/// A view displaying the labels and values.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,26 @@ struct MediaPopupElementView: View {
/// The `PopupElement` to display.
var popupElement: MediaPopupElement

@State var isExpanded: Bool = true

var body: some View {
if displayableMediaCount > 0 {
DisclosureGroup(isExpanded: $isExpanded) {
Divider()
.padding(.bottom, 4)
PopupMediaView(
popupMedia: popupElement.media,
displayableMediaCount: displayableMediaCount
)
} label: {
VStack(alignment: .leading) {
PopupElementHeader(
title: popupElement.displayTitle,
description: popupElement.description
)
}
}
Divider()
PopupElementHeader(
title: popupElement.title,
description: popupElement.description
)
PopupMediaView(
popupMedia: popupElement.media,
displayableMediaCount: displayableMediaCount
)
}
}

Expand Down Expand Up @@ -104,3 +113,10 @@ struct MediaPopupElementView: View {
}

extension PopupMedia: Identifiable {}

private extension MediaPopupElement {
/// Provides a default title to display if `title` is empty.
var displayTitle: String {
title.isEmpty ? "Media" : title
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,18 @@ struct PopupElementHeader: View {
// a view, so conditionally check for an empty title and description.
if !title.isEmpty {
Text(title)
.multilineTextAlignment(.leading)
.font(.title2)
.foregroundColor(.primary)
}

if !description.isEmpty {
Text(description)
.multilineTextAlignment(.leading)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.padding([.bottom], 4)

if !title.isEmpty ||
!description.isEmpty {
Divider()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
// limitations under the License.

import SwiftUI
import Charts

/// A view displaying details for pie chart popup media.
struct PieChart: View {
Expand Down
Loading