导入 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。
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// DispatchWorkHolder.swift
|
||||
// SSToastMessage
|
||||
//
|
||||
// Created by Ankit Panchal on 08/09/20.
|
||||
// Copyright © 2020 Simform Solution Pvt. Ltd. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
class DispatchWorkHolder {
|
||||
var work: DispatchWorkItem?
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// View+Extension.swift
|
||||
// SSToastMessage
|
||||
//
|
||||
// Created by Ankit Panchal on 08/09/20.
|
||||
// Copyright © 2020 Simform Solution Pvt. Ltd. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
|
||||
/// Add this modifier to the top most views in hierarchy.
|
||||
/// Parameters:
|
||||
/// - isPresented: binding to determine if the message view should be seen on-screen or hidden
|
||||
/// - type: set type of view alert, toast and float.
|
||||
/// - position: top or bottom (for default case it just determines animation direction).
|
||||
/// - animation: custom animation for message view sliding onto screen.
|
||||
/// - duration: time after which message view should disappear.
|
||||
/// - closeOnTap: on message view tap it should disappear.
|
||||
/// - onTap: on message view tap perform any action or navigation.
|
||||
/// - onToastDismiss: on toast dismiss perform any action.
|
||||
/// - closeOnTapOutside: on outside tap message view should disappear.
|
||||
/// - view: view you want to display on your message view
|
||||
/// - Returns: void
|
||||
public func present<MessageContent: View>(
|
||||
isPresented: Binding<Bool>,
|
||||
type: MessageView<MessageContent>.MessageType = .alert,
|
||||
position: MessageView<MessageContent>.Position = .bottom,
|
||||
animation: Animation = Animation.easeOut(duration: 0.3),
|
||||
horizontalPadding: CGFloat? = 0,
|
||||
duration: Double? = 3.0,
|
||||
closeOnTap: Bool = true,
|
||||
closeOnTapOutside: Bool = false,
|
||||
onTap: (() -> Void)? = nil,
|
||||
onToastDismiss: (() -> Void)? = nil,
|
||||
view: @escaping () -> MessageContent) -> some View {
|
||||
self.modifier(
|
||||
MessageView(
|
||||
isPresented: isPresented,
|
||||
type: type,
|
||||
position: position,
|
||||
animation: animation,
|
||||
duration: duration,
|
||||
horizontalPadding: horizontalPadding,
|
||||
closeOnTap: closeOnTap,
|
||||
onTap: onTap ?? {},
|
||||
onToastDismiss: onToastDismiss ?? {},
|
||||
closeOnTapOutside: closeOnTapOutside,
|
||||
view: view)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// ViewModel.swift
|
||||
// SSToastMessage
|
||||
//
|
||||
// Created by Yagnik Bavishi on 17/04/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
class ToastViewModel: ObservableObject {
|
||||
|
||||
@Published var updateView: Bool = false
|
||||
@Published var isLeftRightToastView: Bool = false
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user