Initial commit
This commit is contained in:
@ -6,13 +6,86 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import Vision
|
||||
|
||||
struct CameraView: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
func performOCR(on uiImage: UIImage, completion: @escaping (String) -> Void) {
|
||||
guard let cgImage = uiImage.cgImage else { return }
|
||||
|
||||
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
|
||||
let request = VNRecognizeTextRequest { (request, error) in
|
||||
guard let observations = request.results as? [VNRecognizedTextObservation] else { return }
|
||||
let recognizedStrings = observations.compactMap { $0.topCandidates(1).first?.string }
|
||||
completion(recognizedStrings.joined(separator: "\n"))
|
||||
}
|
||||
request.recognitionLanguages = ["en-US", "zh-Hans"] // 根据需要设置支持的语言
|
||||
request.usesLanguageCorrection = true
|
||||
|
||||
do {
|
||||
try handler.perform([request])
|
||||
} catch {
|
||||
print("OCR失败: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CameraView()
|
||||
|
||||
|
||||
struct CameraView: UIViewControllerRepresentable {
|
||||
@Binding var textInput: String
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||
let picker = UIImagePickerController()
|
||||
picker.delegate = context.coordinator
|
||||
picker.sourceType = .camera
|
||||
picker.allowsEditing = true // 启用编辑模式
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
|
||||
var parent: CameraView
|
||||
|
||||
init(_ parent: CameraView) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
// 尝试获取裁剪后的图片
|
||||
if let editedImage = info[.editedImage] as? UIImage {
|
||||
// 使用裁剪后的图片进行OCR
|
||||
performOCR(on: editedImage) { recognizedText in
|
||||
// 更新文本输入
|
||||
self.parent.textInput = recognizedText
|
||||
self.parent.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
} else if let originalImage = info[.originalImage] as? UIImage {
|
||||
// 如果用户没有裁剪图片,回退到使用原始图片
|
||||
performOCR(on: originalImage) { recognizedText in
|
||||
self.parent.textInput = recognizedText
|
||||
self.parent.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
} else {
|
||||
// 如果获取图片失败,直接关闭相机视图
|
||||
self.parent.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
func imagePickerController2(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
if let uiImage = info[.originalImage] as? UIImage {
|
||||
// 调用OCR处理函数
|
||||
performOCR(on: uiImage) { recognizedText in
|
||||
// 更新父视图的文本输入
|
||||
self.parent.textInput = recognizedText
|
||||
}
|
||||
}
|
||||
parent.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,13 +6,197 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import StoreKit
|
||||
|
||||
struct IAPTestView: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
enum IAPProductTest: String, CaseIterable {
|
||||
case premiumFeature1 = "grammar_1_month"
|
||||
case premiumFeature2 = "grammar_1_week"
|
||||
|
||||
}
|
||||
|
||||
|
||||
class IAPManagerTest: ObservableObject {
|
||||
@Published var products: [Product] = []
|
||||
@Published var purchasedProducts: [Product] = []
|
||||
|
||||
init() {
|
||||
Task {
|
||||
await requestProducts()
|
||||
await updatePurchasedProducts()
|
||||
await listenForTransactions()
|
||||
}
|
||||
}
|
||||
|
||||
func requestProducts() async {
|
||||
do {
|
||||
let products = try await Product.products(for: IAPProductTest.allCases.map { $0.rawValue })
|
||||
DispatchQueue.main.async {
|
||||
// 按照产品的权重排序
|
||||
self.products = products
|
||||
|
||||
// 打印每个产品及其相关变量
|
||||
for product in self.products {
|
||||
print("--------------------------")
|
||||
print("Product ID: \(product.id)")
|
||||
print("Product Title: \(product.displayName)")
|
||||
print("Product Description: \(product.description)")
|
||||
print("Product Price: \(product.price)")
|
||||
print("Product displayPrice: \(product.displayPrice)")
|
||||
print("Product priceFormatStyle: \(product.priceFormatStyle)")
|
||||
print("Product subscriptionPeriodFormatStyle: \(product.subscriptionPeriodFormatStyle)")
|
||||
print("Product subscriptionPeriodUnitFormatStyle: \(product.subscriptionPeriodUnitFormatStyle)")
|
||||
print("--------------------------")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Failed to fetch products: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func buy(product: Product) async {
|
||||
do {
|
||||
let uuid = UUID()
|
||||
let token = Product.PurchaseOption.appAccountToken(uuid)
|
||||
print("purchase appAccountToken: \(uuid.uuidString)")
|
||||
|
||||
let result = try await product.purchase(options: [token])
|
||||
switch result {
|
||||
case .success(let verification):
|
||||
if case .verified(let transaction) = verification {
|
||||
// 在这里不需要处理交易完成,详细处理放在 listenForTransactions 中
|
||||
// 如果购买成功了,再点击这个按钮,会直接到这里
|
||||
print("Purchase initiated for product: \(product.id)")
|
||||
}
|
||||
case .userCancelled:
|
||||
print("User cancelled the purchase.")
|
||||
case .pending:
|
||||
print("Purchase is pending.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
print("Failed to purchase product: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func updatePurchasedProducts() async {
|
||||
for await result in Transaction.currentEntitlements {
|
||||
if case .verified(let transaction) = result {
|
||||
if let product = products.first(where: { $0.id == transaction.productID }) {
|
||||
DispatchQueue.main.async {
|
||||
self.purchasedProducts.append(product)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func listenForTransactions() async {
|
||||
for await transactionResult in Transaction.updates {
|
||||
if case .verified(let transaction) = transactionResult {
|
||||
if let product = products.first(where: { $0.id == transaction.productID }) {
|
||||
DispatchQueue.main.async {
|
||||
self.purchasedProducts.append(product)
|
||||
}
|
||||
}
|
||||
|
||||
// 打印详细的交易信息,为什么会有历史交易?
|
||||
print("Transaction ID: \(transaction.id)")
|
||||
print("Product ID: \(transaction.productID)")
|
||||
print("Purchase Date: \(transaction.purchaseDate)")
|
||||
//print("Transaction State: \(transaction.revocationReason ?? "None")")
|
||||
//print("Original Transaction ID: \(transaction.originalID ?? "None")")
|
||||
|
||||
// 获取票据数据
|
||||
|
||||
// 获取 jsonRepresentation
|
||||
let jsonData = try transaction.jsonRepresentation
|
||||
|
||||
// 将 Data 转换为 String
|
||||
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
||||
print("Transaction Receipt: \(jsonString)")
|
||||
} else {
|
||||
print("Failed to convert JSON data to string.")
|
||||
}
|
||||
|
||||
await transaction.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restorePurchases() async {
|
||||
do {
|
||||
try await AppStore.sync()
|
||||
await updatePurchasedProducts()
|
||||
print("Purchases restored")
|
||||
} catch {
|
||||
print("Failed to restore purchases: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct IAPTestView: View {
|
||||
@StateObject var iapManager = IAPManager()
|
||||
//@StateObject var iapManager = IAPManagerTest()
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
if iapManager.products.isEmpty {
|
||||
Text("Loading products...")
|
||||
} else {
|
||||
ForEach(iapManager.products, id: \.id) { product in
|
||||
VStack {
|
||||
Text(product.displayName)
|
||||
.font(.title)
|
||||
|
||||
Button("Buy \(product.displayName)") {
|
||||
Task {
|
||||
await iapManager.buy(product: product){ result in
|
||||
switch result {
|
||||
case .success(let message):
|
||||
logger.info("succ")
|
||||
case .failure(let error):
|
||||
logger.error("error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
}
|
||||
|
||||
Button("Restore Purchases") {
|
||||
Task {
|
||||
await iapManager.restorePurchases(){ result in
|
||||
switch result {
|
||||
case .success(let message):
|
||||
logger.info("restore purchase succ. message: \(message)")
|
||||
case .failure(let error):
|
||||
logger.error("restore purchase error. message: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.green)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
Task {
|
||||
//await iapManager.requestProducts()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
IAPTestView()
|
||||
}
|
||||
|
||||
@ -6,13 +6,224 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ToastUI
|
||||
|
||||
fileprivate struct ProgressBar: View {
|
||||
@Binding var value: Float
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
Rectangle()
|
||||
.foregroundColor(.gray.opacity(0.3))
|
||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||
Rectangle()
|
||||
.foregroundColor(Color.green)
|
||||
.frame(width: min(CGFloat(self.value) * geometry.size.width, geometry.size.width), height: geometry.size.height)
|
||||
.animation(.linear, value: value)
|
||||
}
|
||||
.cornerRadius(45.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InputView: View {
|
||||
@Binding var textInput: String
|
||||
@Binding var progressValue: Float
|
||||
@Binding var showKeyboard: Bool
|
||||
@Binding var showBuyProView: Bool
|
||||
@Binding var showResult: Bool
|
||||
@Binding var results : [GrammarRes]
|
||||
@Binding var isLoading: Bool // 控制加载指示器的显示
|
||||
@Binding var showingToast: Bool // 控制是否显示toast
|
||||
@Binding var toastText: String
|
||||
|
||||
@FocusState private var isTextEditorFocused: Bool
|
||||
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
VStack {
|
||||
// TextEditor设计成无边框形式
|
||||
TextEditor(text: $textInput)
|
||||
.onTapGesture {
|
||||
// 用户点击TextEditor时更新状态
|
||||
self.showKeyboard = true // 显示键盘
|
||||
self.showBuyProView = false // 隐藏BuyProView
|
||||
}
|
||||
.focused($isTextEditorFocused)
|
||||
.padding(5)
|
||||
.background(Color.white)
|
||||
.cornerRadius(5)
|
||||
.padding(5)
|
||||
.onAppear {
|
||||
if showKeyboard {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.isTextEditorFocused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Divider() // 添加分割线,区分TextEditor和下方控件
|
||||
|
||||
// 进度条和按钮
|
||||
ProgressBar(value: $progressValue).frame(height: 3)
|
||||
|
||||
HStack {
|
||||
Button("Clear") {
|
||||
textInput = ""
|
||||
progressValue = 0
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Check") {
|
||||
// 清除输入内容前后的空格
|
||||
let trimmedText = textInput.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// 检查是否为空
|
||||
if trimmedText.isEmpty {
|
||||
showingToast = true
|
||||
toastText = "Please enter some text."
|
||||
return // 提前返回,不执行网络请求
|
||||
}
|
||||
|
||||
// 检查长度是否超过200个字符
|
||||
let MaxLen = globalEnvironment.isVip ? globalEnvironment.MaxLenGrammarCheckVIP : globalEnvironment.MaxLenGrammarCheckFree
|
||||
if trimmedText.count > MaxLen {
|
||||
showingToast = true
|
||||
toastText = "Input too long. Please re-enter the text."
|
||||
logger.info("input too lang, inputlen: \(trimmedText.count), maxlen: \(MaxLen), vip: \(globalEnvironment.isVip)")
|
||||
return // 提前返回,不执行网络请求
|
||||
}
|
||||
|
||||
// 检查是否含有非法字符
|
||||
let checkRes = CommonFunc.shared.validateInputEng(input: trimmedText)
|
||||
if !checkRes.isValid {
|
||||
showingToast = true
|
||||
toastText = checkRes.message
|
||||
return
|
||||
}
|
||||
/*
|
||||
if trimmedText.range(of: "[^a-zA-Z0-9 .,;:!?'\"@-]", options: .regularExpression) != nil {
|
||||
showingToast = true
|
||||
toastText = "Please enter valid characters."
|
||||
return // 提前返回,不执行网络请求
|
||||
}
|
||||
*/
|
||||
|
||||
loadData() // 提交的显示
|
||||
|
||||
// Send the request to the server
|
||||
NetworkManager.shared.checkGrammar(inputText: trimmedText) { result in
|
||||
switch result {
|
||||
case .success(let results):
|
||||
// Update the main UI with the results
|
||||
loadComplete()
|
||||
DispatchQueue.main.async {
|
||||
self.results = results
|
||||
self.showResult = true
|
||||
self.showKeyboard = false
|
||||
self.showBuyProView = false
|
||||
hideKeyboard()
|
||||
|
||||
logger.info("grammar check succ.")
|
||||
}
|
||||
case .failure(let error):
|
||||
loadComplete()
|
||||
switch error {
|
||||
case .businessError(let ret, let message):
|
||||
// 业务错误,比如无免费可用次数等,需要处理逻辑。
|
||||
logger.error("Business error - Ret: \(ret), Message: \(message)")
|
||||
DispatchQueue.main.async {
|
||||
showingToast = true
|
||||
switch ret{
|
||||
case globalEnvironment.GrammarCheckOK:
|
||||
toastText = globalEnvironment.GrammarOKToast
|
||||
case globalEnvironment.RetCodeFreeLimited:
|
||||
toastText = globalEnvironment.FreeLimitedToast
|
||||
case globalEnvironment.RetCodeDirtyInput:
|
||||
toastText = globalEnvironment.DirtyInputErrToast
|
||||
default:
|
||||
toastText = globalEnvironment.OtherServerErrToast
|
||||
}
|
||||
}
|
||||
case .other(let error):
|
||||
logger.error("network error occurred: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
showingToast = true
|
||||
toastText = globalEnvironment.NetWorkErrToast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 40)
|
||||
.background(Color.green)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(5)
|
||||
.font(.subheadline) // 调整字体大小为标题大小
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding() // 为所有内容添加padding
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.white) // 设置背景色为白色
|
||||
)
|
||||
.padding(5)
|
||||
.onTapGesture {
|
||||
// 点击背景时隐藏键盘
|
||||
self.isTextEditorFocused = false
|
||||
showBuyProView = true
|
||||
}
|
||||
}
|
||||
// 模拟加载数据
|
||||
func loadData() {
|
||||
isLoading = true
|
||||
}
|
||||
func loadComplete(){
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
// 用于外部调用的方法来控制焦点
|
||||
func toggleFocus() {
|
||||
isTextEditorFocused.toggle()
|
||||
}
|
||||
|
||||
// 函数用于收起键盘
|
||||
private func hideKeyboard() {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct InputView_Preview: View{
|
||||
|
||||
@State private var textInput: String
|
||||
@State private var progressValue: Float = 0
|
||||
@State private var showKeyboard: Bool = false
|
||||
@State private var showBuyProView: Bool = true
|
||||
@State private var showResult : Bool = false
|
||||
@State private var results : [GrammarRes]
|
||||
|
||||
// 提交等待,错误提示等
|
||||
@State private var isLoading = false // 控制加载指示器的显示
|
||||
@State private var showingToast = false // 控制是否显示toast
|
||||
@State private var toastText = ""
|
||||
|
||||
init(){
|
||||
let demoGrammarData = GrammarData.demoInstance()
|
||||
self.textInput = demoGrammarData.inputText
|
||||
self.results = demoGrammarData.results
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
InputView(textInput: $textInput, progressValue: $progressValue, showKeyboard: $showKeyboard, showBuyProView: $showBuyProView, showResult: $showResult, results: $results, isLoading: $isLoading, showingToast: $showingToast, toastText: $toastText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
InputView()
|
||||
InputView_Preview()
|
||||
}
|
||||
|
||||
@ -6,13 +6,201 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
// 确保SingleCorrectionCard只能在本文件内访问
|
||||
fileprivate struct SingleCorrectionCard: View {
|
||||
let correction: String
|
||||
|
||||
var body: some View {
|
||||
Text(correction)
|
||||
.padding(.vertical, 6)
|
||||
.font(.system(size: UIFontMetrics.default.scaledValue(for: 15) * 4 / 5))
|
||||
.background(Color.yellow.opacity(0.5))
|
||||
.cornerRadius(5)
|
||||
}
|
||||
}
|
||||
|
||||
struct GrammarDetailsCardView: View {
|
||||
let res: GrammarRes
|
||||
|
||||
var body: some View {
|
||||
// 添加黄色虚线作为分隔
|
||||
Divider()
|
||||
.background(Color.yellow)
|
||||
.frame(height: 1)
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
|
||||
.foregroundColor(.yellow)
|
||||
)
|
||||
.padding(3)
|
||||
|
||||
VStack {
|
||||
|
||||
Text("Error: \(res.plain)")
|
||||
.frame(maxWidth: .infinity, alignment: .leading) // 横向铺满
|
||||
.padding(.vertical, 8) // 调整高度为默认高度的2/3
|
||||
.background(Color.gray.opacity(0.5)) // 设置背景色为Gray
|
||||
.foregroundColor(.black)
|
||||
.cornerRadius(5)
|
||||
|
||||
Text("Reason: \(res.reason)")
|
||||
.frame(maxWidth: .infinity, alignment: .leading) // 横向铺满
|
||||
|
||||
Text("Correction:")
|
||||
.frame(maxWidth: .infinity, alignment: .leading) // 横向铺满
|
||||
.padding(.top, 6) // 在Correction上方添加一些间隔
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
ForEach(Array(res.correction.enumerated()), id: \.offset) { index, correction in
|
||||
SingleCorrectionCard(correction: correction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 3) // 在卡片的底部添加一些间隔
|
||||
.padding(.bottom, 3) // 在卡片的底部添加一些间隔
|
||||
.background(Color.gray.opacity(0.2)) // 设置整个卡片的背景色为LightGray
|
||||
.cornerRadius(10)
|
||||
}
|
||||
}
|
||||
|
||||
struct ResultView: View {
|
||||
// 假设的错误数据和文本内容
|
||||
@Binding var textContent: String
|
||||
@Binding var results: [GrammarRes]
|
||||
@Binding var showResult : Bool
|
||||
@Binding var showKeyboard : Bool
|
||||
|
||||
@State private var selectedCardIndex: Int? = 0
|
||||
@State private var textRes = AttributedString()
|
||||
|
||||
@State private var resContent: String = String("")
|
||||
|
||||
func getColoredText(str : String, type : String, index : Int?) -> AttributedString {
|
||||
var text = AttributedString( str )
|
||||
if(type == GrammarResType.ok.rawValue){
|
||||
return text
|
||||
}
|
||||
if (type == GrammarResType.grammar.rawValue ){
|
||||
text.foregroundColor = .red
|
||||
}else
|
||||
{
|
||||
text.backgroundColor = .yellow
|
||||
}
|
||||
text.link = URL(string: String(index!))
|
||||
return text
|
||||
}
|
||||
|
||||
func styledText() -> Text {
|
||||
var outstr = AttributedString()
|
||||
var index = 0
|
||||
|
||||
let substr: String = textContent
|
||||
var currentIndex = substr.startIndex
|
||||
|
||||
for res in results {
|
||||
if let range = substr.range(of: res.plain, range: currentIndex..<substr.endIndex){
|
||||
let tmpABStr = AttributedString(String(substr[currentIndex..<range.lowerBound]))
|
||||
outstr.append(tmpABStr)
|
||||
outstr.append(getColoredText(str: res.plain, type: res.type, index: index))
|
||||
index = index + 1
|
||||
currentIndex = range.upperBound
|
||||
}
|
||||
}
|
||||
let tmpABStr = AttributedString(String(substr[currentIndex..<substr.endIndex]))
|
||||
outstr.append(tmpABStr)
|
||||
return Text(outstr)
|
||||
}
|
||||
|
||||
/*
|
||||
func styledText() -> Text {
|
||||
var outstr = AttributedString()
|
||||
var index = 0
|
||||
|
||||
for res in results {
|
||||
let tmp = getColoredText(str: res.plain, type: res.type, index: index)
|
||||
outstr.append(tmp)
|
||||
index = index + 1
|
||||
}
|
||||
return Text(outstr)
|
||||
}
|
||||
*/
|
||||
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
VStack {
|
||||
// 为styledText提供滚动视图
|
||||
ScrollView {
|
||||
styledText()
|
||||
.padding()
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
let path = url.absoluteString
|
||||
print("index: \(path)")
|
||||
self.selectedCardIndex = Int(path)
|
||||
return .handled
|
||||
})
|
||||
Spacer() // 使用Spacer确保Text组件横向拉伸
|
||||
}
|
||||
.onTapGesture {
|
||||
self.showResult = false
|
||||
self.showKeyboard = true
|
||||
}
|
||||
.background(Color.white) // 设置背景色为Gray
|
||||
.frame(maxHeight: .infinity) // 限制styledText占用半个屏幕
|
||||
|
||||
//与上面的文本进行联动,对每个卡片设置一个index
|
||||
ScrollViewReader { scrollView in
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(Array(results.enumerated()), id: \.offset) { index, res in
|
||||
if(res.type != GrammarResType.ok.rawValue){
|
||||
GrammarDetailsCardView(res: res)
|
||||
.id(index) // 给每个CardView分配一个ID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedCardIndex) { newIndex in
|
||||
if let newIndex = newIndex {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(newIndex, anchor: .top)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity) // 限制ErrorCard占用半个屏幕 */
|
||||
}
|
||||
.padding() // 为所有内容添加padding
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.white) // 设置背景色为白色
|
||||
)
|
||||
.padding(5)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
struct ResultView_Preview: View {
|
||||
|
||||
@State var input : String = "this is demo text"
|
||||
@State var results : [GrammarRes]
|
||||
@State var showResult : Bool = true
|
||||
@State var showKeyboard : Bool = true
|
||||
|
||||
init() {
|
||||
let demoGrammarData = GrammarData.demoInstance()
|
||||
self.input = demoGrammarData.inputText
|
||||
self.results = demoGrammarData.results
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ResultView(textContent: $input, results: $results, showResult: $showResult, showKeyboard: $showKeyboard)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ResultView()
|
||||
ResultView_Preview()
|
||||
}
|
||||
|
||||
@ -7,12 +7,197 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
struct RichText: View {
|
||||
var text1: AttributedString {
|
||||
var text = AttributedString(localized:"登录即表示同意")
|
||||
text.foregroundColor = .gray
|
||||
return text
|
||||
}
|
||||
var text2: AttributedString {
|
||||
var text = AttributedString(localized:"用户协议")
|
||||
text.link = URL(string: "111")
|
||||
text.foregroundColor = .red
|
||||
return text
|
||||
}
|
||||
var text3: AttributedString {
|
||||
var text = AttributedString(localized:"和")
|
||||
text.foregroundColor = .gray
|
||||
return text
|
||||
}
|
||||
var text4: AttributedString {
|
||||
var text = AttributedString(localized:"隐私协议")
|
||||
text.link = URL(string: "222")
|
||||
text.foregroundColor = .red
|
||||
return text
|
||||
}
|
||||
|
||||
var text: AttributedString {
|
||||
text1 + text2 + text3 + text4
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
VStack {
|
||||
|
||||
Text(text)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
let path = url.absoluteString
|
||||
if path.hasPrefix("111") {
|
||||
print("111...")
|
||||
} else if path.hasPrefix("222") {
|
||||
print("222...")
|
||||
}
|
||||
return .handled
|
||||
})
|
||||
|
||||
Text("Device ID: \(globalEnvironment.deviceID)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
class RichTextViewController: UIViewController, UITextViewDelegate {
|
||||
var textView: UITextView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// 初始化 UITextView 并设置代理
|
||||
textView = UITextView(frame: self.view.bounds)
|
||||
textView.delegate = self
|
||||
|
||||
// 允许富文本交互
|
||||
textView.isEditable = false
|
||||
textView.isSelectable = true
|
||||
|
||||
// 创建富文本
|
||||
let attributedString = NSMutableAttributedString(string: "点击这里进行测试")
|
||||
|
||||
// 设置点击部分的样式和链接
|
||||
let linkAttributes: [NSAttributedString.Key: Any] = [
|
||||
.link: URL(string: "http://baidu.com")!, // 使用自定义URL scheme
|
||||
.foregroundColor: UIColor.blue
|
||||
]
|
||||
|
||||
attributedString.setAttributes(linkAttributes, range: NSRange(location: 0, length: 4)) // 假设"点击这里"是可点击的
|
||||
|
||||
textView.attributedText = attributedString
|
||||
|
||||
// 将 UITextView 添加到当前视图
|
||||
self.view.addSubview(textView)
|
||||
}
|
||||
|
||||
// 处理富文本链接点击事件
|
||||
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||
print("特定文本部分被点击")
|
||||
print(URL.scheme as Any)
|
||||
if URL.scheme == "http" {
|
||||
// 在这里添加点击后的处理逻辑
|
||||
return false // 返回false表示不让系统处理这个URL
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
struct RichTextView: UIViewRepresentable {
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
// 确保 RichTextViewController 实例在这里创建,并立即返回 textView
|
||||
// 这样可以避免 textView 在使用之前未被初始化
|
||||
let controller = RichTextViewController()
|
||||
controller.loadViewIfNeeded() // 确保视图控制器的视图被加载,从而textView被初始化
|
||||
return controller.textView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||
// 更新UI视图(如果需要)
|
||||
}
|
||||
}
|
||||
|
||||
struct RichText: View {
|
||||
var body: some View {
|
||||
VStack{
|
||||
Text("请点击下面的文本 请点击下面的文本 请点击下面的文本 请点击下面的文本 请点击下面的文本 请点击下面的文本 ")
|
||||
// 使用我们的 RichTextView
|
||||
RichTextView()
|
||||
.frame(maxHeight: .infinity) // 设置一个合适的高度
|
||||
//.edgesIgnoringSafeArea(.all) // 让视图延伸到屏幕的边缘
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct AttributedText: UIViewRepresentable {
|
||||
var attributedString: NSAttributedString
|
||||
|
||||
func makeUIView(context: Context) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.numberOfLines = 0 // 支持多行显示
|
||||
label.attributedText = attributedString
|
||||
|
||||
// 添加手势识别器
|
||||
let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.labelTapped(_:)))
|
||||
label.addGestureRecognizer(tapGesture)
|
||||
label.isUserInteractionEnabled = true
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UILabel, context: Context) {
|
||||
// 更新富文本字符串
|
||||
uiView.attributedText = attributedString
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject {
|
||||
var parent: AttributedText
|
||||
|
||||
init(_ parent: AttributedText) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
@objc func labelTapped(_ sender: UITapGestureRecognizer) {
|
||||
// 处理点击事件,这里需要根据点击位置来确定用户点击的是哪一部分文本
|
||||
// 这部分较为复杂,可能需要使用UILabel的子类来自定义实现
|
||||
print("Label was tapped")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct RichText: View {
|
||||
var body: some View {
|
||||
// 示例使用AttributedText
|
||||
AttributedText(attributedString: attributedString)
|
||||
}
|
||||
|
||||
var attributedString: NSAttributedString {
|
||||
let fullString = NSMutableAttributedString(string: "Tap on ")
|
||||
|
||||
let clickablePart = NSAttributedString(string: "this text", attributes: [
|
||||
.foregroundColor: UIColor.blue,
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue
|
||||
])
|
||||
|
||||
fullString.append(clickablePart)
|
||||
fullString.append(NSAttributedString(string: " to see action."))
|
||||
|
||||
return fullString
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
#Preview {
|
||||
RichText()
|
||||
}
|
||||
|
||||
@ -6,13 +6,16 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct ShareSheet: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
struct ShareSheet: UIViewControllerRepresentable {
|
||||
var itemsToShare: [Any]
|
||||
|
||||
func makeUIViewController(context: Context) -> UIActivityViewController {
|
||||
let controller = UIActivityViewController(activityItems: itemsToShare, applicationActivities: nil)
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ShareSheet()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user