Files
admin d6aeede2bc 导入 XuXuCheck 工程(iOS 干化学尿液试纸分析 App)
从桌面工程复制而来,作为本仓库的 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。
2026-09-23 14:17:31 +08:00

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
}
}