d6aeede2bc
从桌面工程复制而来,作为本仓库的 Xuxucheck/ 子目录。 为控制仓库体积做了两处删减: - 移除 Frameworks/opencv2.xcframework 下的两个 visionOS 切片 (xros-arm64、xros-arm64-simulator,合计约 106 MB),并同步删掉该 xcframework 内 Info.plist 里对应的两条 AvailableLibraries 记录, 只保留 ios-arm64 与 ios-arm64_x86_64-simulator。 - 移除全部 .DS_Store 与 xcuserdata/。 已用 xcodebuild 从本目录完整编译验证:BUILD SUCCEEDED, ProcessXCFramework 以 --platform ios 成功选中框架,产物含 3699 个 cv:: 符号 (OpenCV 确实链入),嵌入 framework 3 个,与移除无用 pod 之后的状态一致。 仓库约 206 MB,其中 OpenCV 203 MB、其余源码与依赖约 3 MB。
351 lines
12 KiB
Swift
351 lines
12 KiB
Swift
//
|
|
// MessageView.swift
|
|
// SSToastMessage
|
|
//
|
|
// Created by Ankit Panchal on 08/09/20.
|
|
// Copyright © 2020 Simform Solution Pvt. Ltd. All rights reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
public struct MessageView<MessageContent>: ViewModifier where MessageContent: View {
|
|
|
|
@ObservedObject var viewModel = ToastViewModel()
|
|
|
|
public enum MessageType: Equatable {
|
|
|
|
case alert
|
|
case toast
|
|
case floater(verticalPadding: CGFloat = 50)
|
|
case leftToastView
|
|
case rightToastView
|
|
|
|
func shouldBeCentered() -> Bool {
|
|
switch self {
|
|
case .alert:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum Position {
|
|
|
|
case top
|
|
case bottom
|
|
}
|
|
|
|
// MARK: - Public Properties
|
|
|
|
/// Tells if the sheet should be presented or not
|
|
@Binding var isPresented: Bool
|
|
|
|
var type: MessageType
|
|
var position: Position
|
|
|
|
var animation: Animation
|
|
|
|
/// If nil - don't dismiss automatically
|
|
var duration: Double?
|
|
|
|
/// Should Add horizontal padding - default - 0
|
|
var horizontalPadding: CGFloat?
|
|
|
|
/// Should close on tap - default is `true`
|
|
var closeOnTap: Bool
|
|
|
|
/// Allow to perform action on tap default is
|
|
var onTap: () -> Void
|
|
|
|
/// Allow to perform any action when toast dismiss
|
|
var onToastDismiss: () -> Void
|
|
|
|
/// Should close on tap outside - default is `false`
|
|
var closeOnTapOutside: Bool
|
|
|
|
/// Use to show Left toast view
|
|
@State var leftToastMessage: Bool = false
|
|
|
|
/// Use to show Right toast view
|
|
@State var rightToastMessage: Bool = false
|
|
|
|
var view: () -> MessageContent
|
|
|
|
@State private var viewHeight: CGFloat = .zero
|
|
@State private var viewWidth: CGFloat = .zero
|
|
|
|
/// holder for autohiding dispatch work (to be able to cancel it when needed)
|
|
var dispatchWorkHolder = DispatchWorkHolder()
|
|
|
|
// MARK: - Private Properties
|
|
|
|
/// The rect of the hosting controller
|
|
@State private var presenterContentRect: CGRect = .zero
|
|
|
|
/// The rect of popup content
|
|
@State private var sheetContentRect: CGRect = .zero
|
|
|
|
/// offset for setting up left & right toast
|
|
@State private var offset: CGFloat = 0
|
|
|
|
/// Toast message width
|
|
@State var toastViewWidth: CGFloat = 0
|
|
|
|
/// Toast message height
|
|
@State var toastViewHeight: CGFloat = 0
|
|
|
|
/// The offset when the popup is displayed
|
|
private var displayedOffset: CGFloat {
|
|
switch type {
|
|
case .alert:
|
|
return -presenterContentRect.midY + viewHeight/2
|
|
case .toast:
|
|
if position == .bottom {
|
|
return viewHeight - presenterContentRect.midY - sheetContentRect.height/2
|
|
} else {
|
|
return -presenterContentRect.midY + sheetContentRect.height/2
|
|
}
|
|
case .floater(let verticalPadding):
|
|
if position == .bottom {
|
|
return viewHeight - presenterContentRect.midY - sheetContentRect.height/2 - verticalPadding
|
|
} else {
|
|
return -presenterContentRect.midY + sheetContentRect.height/2 + verticalPadding
|
|
}
|
|
case .leftToastView, .rightToastView:
|
|
if position == .bottom {
|
|
return leftRightToastViewPosition - 30
|
|
} else {
|
|
return -(leftRightToastViewPosition - 60)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The offset when the popup is hidden
|
|
private var hiddenOffset: CGFloat {
|
|
if position == .top {
|
|
if presenterContentRect.isEmpty {
|
|
return -1000
|
|
}
|
|
#if targetEnvironment(macCatalyst)
|
|
return -presenterContentRect.midY - sheetContentRect.height/2 - 300
|
|
#else
|
|
return -presenterContentRect.midY - sheetContentRect.height/2 - 5
|
|
#endif
|
|
} else {
|
|
if presenterContentRect.isEmpty {
|
|
return 1000
|
|
}
|
|
#if targetEnvironment(macCatalyst)
|
|
return screenHeight - presenterContentRect.midY + sheetContentRect.height/2 + 300
|
|
#else
|
|
return screenHeight - presenterContentRect.midY + sheetContentRect.height/2 + 5
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// Set left and and right view toast view position
|
|
private var leftRightToastViewPosition: CGFloat {
|
|
viewHeight/2 - toastViewHeight/2
|
|
}
|
|
|
|
/// The current offset, based on the **presented** property
|
|
private var currentOffset: CGFloat {
|
|
return isPresented ? displayedOffset : hiddenOffset
|
|
}
|
|
|
|
private var screenHeight: CGFloat {
|
|
#if os(watchOS)
|
|
return WKInterfaceDevice.current().screenBounds.size.height
|
|
#elseif os(macOS)
|
|
return NSScreen.main?.frame.height ?? 0
|
|
#elseif os(iOS)
|
|
return UIScreen.main.bounds.size.height
|
|
#endif
|
|
}
|
|
|
|
private var screenWidth: CGFloat {
|
|
#if os(watchOS)
|
|
return WKInterfaceDevice.current().screenBounds.size.width
|
|
#elseif os(macOS)
|
|
return NSScreen.main?.frame.width ?? 0
|
|
#elseif os(iOS)
|
|
return UIScreen.main.bounds.size.width
|
|
#endif
|
|
}
|
|
|
|
// MARK: - Content Builders
|
|
|
|
public func body(content: Content) -> some View {
|
|
GeometryReader { proxy in
|
|
ZStack {
|
|
content
|
|
.background(
|
|
GeometryReader { proxy -> AnyView in
|
|
let rect = proxy.frame(in: .global)
|
|
// This avoids an infinite layout loop
|
|
if rect.integral != self.presenterContentRect.integral {
|
|
DispatchQueue.main.async {
|
|
viewModel.updateView = true
|
|
self.presenterContentRect = rect
|
|
}
|
|
}
|
|
return AnyView(EmptyView())
|
|
}
|
|
)
|
|
.overlay(presentSheet())
|
|
#if !(os(macOS))
|
|
.navigationBarHidden(true)
|
|
#endif
|
|
if closeOnTapOutside {
|
|
Color.clear
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
self.isPresented = false
|
|
}
|
|
.opacity(isPresented ? 0.5 : 0)
|
|
}
|
|
}.onReceive(viewModel.$updateView, perform: { _ in
|
|
viewHeight = proxy.size.height
|
|
#if os(macOS) || targetEnvironment(macCatalyst)
|
|
viewWidth = proxy.size.width - (horizontalPadding ?? 0)
|
|
#elseif os(iOS)
|
|
viewWidth = screenWidth - (horizontalPadding ?? 0)
|
|
#endif
|
|
})
|
|
.onAppear {
|
|
if type == .leftToastView {
|
|
setLeftToastOffset()
|
|
} else if type == .rightToastView {
|
|
setRightToastOffset()
|
|
}
|
|
}
|
|
.onChange(of: isPresented) { updatedValue in
|
|
if !updatedValue {
|
|
leftToastMessage = false
|
|
rightToastMessage = false
|
|
} else {
|
|
leftToastMessage = type == .leftToastView
|
|
rightToastMessage = type == .rightToastView
|
|
}
|
|
setLeftRightToastOffset()
|
|
}
|
|
.frame(width: proxy.size.width, height: proxy.size.height)
|
|
}
|
|
}
|
|
|
|
/// This is the builder for the sheet content
|
|
func presentSheet() -> some View {
|
|
|
|
// if needed, dispatch autoDismiss and cancel previous one
|
|
if let duration = duration, !(type == .leftToastView || type == .rightToastView) {
|
|
dispatchWorkHolder.work?.cancel()
|
|
dispatchWorkHolder.work = DispatchWorkItem(block: {
|
|
self.isPresented = false
|
|
self.onToastDismiss()
|
|
})
|
|
if isPresented, let work = dispatchWorkHolder.work {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: work)
|
|
}
|
|
}
|
|
|
|
let commonVStack = VStack {
|
|
self.view()
|
|
.simultaneousGesture(TapGesture().onEnded {
|
|
if self.closeOnTap {
|
|
DispatchQueue.main.async {
|
|
self.isPresented = false
|
|
self.onTap()
|
|
}
|
|
}
|
|
})
|
|
.background(
|
|
GeometryReader { proxy -> AnyView in
|
|
let rect = proxy.frame(in: .global)
|
|
// This avoids an infinite layout loop
|
|
if rect.integral != self.sheetContentRect.integral {
|
|
DispatchQueue.main.async {
|
|
self.sheetContentRect = rect
|
|
toastViewWidth = proxy.size.width
|
|
toastViewHeight = proxy.size.height
|
|
}
|
|
}
|
|
return AnyView(EmptyView())
|
|
}
|
|
)
|
|
.offset(x: (leftToastMessage || rightToastMessage) ? offset : 0, y: (leftToastMessage || rightToastMessage) ? displayedOffset : currentOffset)
|
|
.onReceive(viewModel.$isLeftRightToastView, perform: { _ in
|
|
|
|
if leftToastMessage || rightToastMessage {
|
|
withAnimation(.easeInOut(duration: 0.5)) {
|
|
if rightToastMessage {
|
|
#if os(macOS) || targetEnvironment(macCatalyst)
|
|
offset = (viewWidth / 2) - toastViewWidth / 2 - 10
|
|
#elseif os(iOS)
|
|
offset = (screenWidth / 2) - toastViewWidth / 2 - 10
|
|
#endif
|
|
} else if leftToastMessage {
|
|
#if os(macOS) || targetEnvironment(macCatalyst)
|
|
offset = -((viewWidth / 2) - toastViewWidth / 2 - 10)
|
|
#elseif os(iOS)
|
|
offset = -((screenWidth / 2) - toastViewWidth / 2) + 10
|
|
#endif
|
|
}
|
|
}
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + (duration ?? 0)) {
|
|
withAnimation(.easeInOut(duration: 0.5)) {
|
|
setLeftRightToastOffset()
|
|
}
|
|
}
|
|
|
|
Timer.scheduledTimer(withTimeInterval: ((duration ?? 0) + 0.5), repeats: false) { _ in
|
|
onToastDismiss()
|
|
isPresented = false
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
return ZStack() {
|
|
Group {
|
|
if !(leftToastMessage || rightToastMessage) {
|
|
commonVStack
|
|
.frame(width: viewWidth)
|
|
.animation(animation, value: isPresented)
|
|
} else {
|
|
commonVStack
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// offset is used for showing left and right toast view
|
|
func setLeftRightToastOffset() {
|
|
if leftToastMessage {
|
|
setLeftToastOffset()
|
|
} else if rightToastMessage {
|
|
setRightToastOffset()
|
|
}
|
|
}
|
|
|
|
/// offset is used for showing left view
|
|
func setLeftToastOffset() {
|
|
#if os(macOS)
|
|
offset = -(NSScreen.main?.frame.size.width ?? 0)
|
|
#elseif os(iOS)
|
|
offset = -UIScreen.main.bounds.width
|
|
#endif
|
|
}
|
|
|
|
/// offset is used for showing right toast view
|
|
func setRightToastOffset() {
|
|
#if os(macOS)
|
|
offset = NSScreen.main?.frame.size.width ?? 0
|
|
#elseif os(iOS)
|
|
offset = UIScreen.main.bounds.width
|
|
#endif
|
|
}
|
|
}
|
|
|