增加push功能。

This commit is contained in:
oscarz
2024-09-10 16:39:30 +08:00
parent 5b3b2f4a5f
commit 434df13a41
6 changed files with 232 additions and 7 deletions

View File

@ -26,6 +26,7 @@
550B85A22C2BC624008834E5 /* InitAPP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 550B85A12C2BC623008834E5 /* InitAPP.swift */; };
551C8C342C79946700B1A88C /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 551C8C332C79946700B1A88C /* GoogleService-Info.plist */; };
555027392C81C0ED00A05441 /* QCloudTTS.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 555027382C81C0ED00A05441 /* QCloudTTS.xcframework */; };
5550273C2C8322F800A05441 /* PushHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5550273B2C8322F800A05441 /* PushHandler.swift */; };
5586E0882C80AD2D00026733 /* TTSManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5586E0872C80AD2D00026733 /* TTSManager.swift */; };
559E6D7C2C34EAE700C971B9 /* IapManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E6D7B2C34EAE700C971B9 /* IapManager.swift */; };
559E6D7E2C35355200C971B9 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E6D7D2C35355200C971B9 /* LogManager.swift */; };
@ -91,6 +92,8 @@
550B85A12C2BC623008834E5 /* InitAPP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitAPP.swift; sourceTree = "<group>"; };
551C8C332C79946700B1A88C /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
555027382C81C0ED00A05441 /* QCloudTTS.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = QCloudTTS.xcframework; sourceTree = "<group>"; };
5550273A2C8311CB00A05441 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
5550273B2C8322F800A05441 /* PushHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushHandler.swift; sourceTree = "<group>"; };
5586E0832C8092C400026733 /* AIGrammar-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AIGrammar-Bridging-Header.h"; sourceTree = "<group>"; };
5586E0872C80AD2D00026733 /* TTSManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTSManager.swift; sourceTree = "<group>"; };
559E6D7B2C34EAE700C971B9 /* IapManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IapManager.swift; sourceTree = "<group>"; };
@ -183,6 +186,7 @@
5500A38E2BB3C7E80065A1D3 /* AIGrammar */ = {
isa = PBXGroup;
children = (
5550273A2C8311CB00A05441 /* Info.plist */,
5586E0842C8093DF00026733 /* third-party */,
5586E0832C8092C400026733 /* AIGrammar-Bridging-Header.h */,
55BC47472C3A380C00120A7D /* CommView */,
@ -249,6 +253,7 @@
559E6D7D2C35355200C971B9 /* LogManager.swift */,
55E4E8F42C60CFFC00988503 /* CommFunc.swift */,
5586E0872C80AD2D00026733 /* TTSManager.swift */,
5550273B2C8322F800A05441 /* PushHandler.swift */,
);
path = lib;
sourceTree = "<group>";
@ -559,6 +564,7 @@
5500A3C42BB40AC40065A1D3 /* WordsView.swift in Sources */,
5509CEF12BB54DD10056C5C2 /* Config.swift in Sources */,
55BB127B2BBD653100D2BEA4 /* ShareSheet.swift in Sources */,
5550273C2C8322F800A05441 /* PushHandler.swift in Sources */,
5500A3C82BB40ADE0065A1D3 /* SettingsView.swift in Sources */,
55E4E8F52C60CFFC00988503 /* CommFunc.swift in Sources */,
55DAC6552BBA959500BDD4C8 /* ResultView.swift in Sources */,
@ -750,6 +756,7 @@
"$(PROJECT_DIR)/AIGrammar/third-party/ios-arm64_i386_x86_64-simulator",
);
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = AIGrammar/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = EasyGrammar;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
INFOPLIST_KEY_NSCameraUsageDescription = "Camera access is required to capture text for grammar and spelling correction.";
@ -873,6 +880,7 @@
"$(PROJECT_DIR)/AIGrammar/third-party/ios-arm64_i386_x86_64-simulator",
);
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = AIGrammar/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = EasyGrammar;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
INFOPLIST_KEY_NSCameraUsageDescription = "Camera access is required to capture text for grammar and spelling correction.";

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>

View File

@ -8,15 +8,24 @@
import SwiftUI
import TrustDecision
import Combine
import Firebase
import FirebaseAnalytics
import FirebaseCrashlytics
@main
struct AIGrammarApp: App {
let persistenceController = PersistenceController.shared
// AppDelegate
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let persistenceController = PersistenceController.shared
@StateObject private var appState = NofifyState()
@State private var urlToOpen: String?
init() {
// AppDelegate
appDelegate.app = self
//
setupLogging()
_ = InitApp.shared
@ -33,12 +42,68 @@ struct AIGrammarApp: App {
var body: some Scene {
WindowGroup {
AllTabView()
AllTabView(selectedTab: $appState.selectedTab, showPromotion: $appState.showPromotion, promotionMode: $appState.promotionMode)
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(IAPManager()) // IAPManager
.environmentObject(globalEnvironment) // IAPManager
.environmentObject(appState)
.preferredColorScheme(.light) // 使
.sheet(isPresented: $appState.showPromotion) {
switch appState.promotionMode {
case .halfScreen:
PromotionView() //
.presentationDetents([.medium])
case .fullScreen:
PromotionView() //
.edgesIgnoringSafeArea(.all)
default:
EmptyView()
}
}
.onChange(of: urlToOpen) { url in
if let url = url, let link = URL(string: url) {
UIApplication.shared.open(link)
}
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
handlePushNotification()
}
}
}
// app
func openURL(urlString: String) {
urlToOpen = urlString
}
func handlePushNotification() {
// 线
DispatchQueue.main.async {
let pushInfo = globalEnvironment.pushSettings
// tab
if pushInfo.gotoTab >= 0 && pushInfo.gotoTab < 4 {
self.appState.selectedTab = pushInfo.gotoTab
}else {
logger.info("invalid goto Tab: \(pushInfo.gotoTab)")
}
//
if pushInfo.showPage && pushInfo.page != "" {
self.appState.showPromotion = true
switch pushInfo.showMode {
case "halfScreen" :
self.appState.promotionMode = PromotionDisplayType.halfScreen
case "fullScreen" :
self.appState.promotionMode = PromotionDisplayType.fullScreen
default:
break
}
}
//
if pushInfo.showPage && pushInfo.openURL != "" {
urlToOpen = pushInfo.openURL
}
logger.info("Push Values. gotoTab: \(pushInfo.gotoTab), showPage: \(pushInfo.showPage), page: \(pushInfo.page), showMode: \(pushInfo.showMode), url: \(pushInfo.openURL)")
}
}
}

View File

@ -9,36 +9,58 @@ import SwiftUI
struct AllTabView: View {
// tab
@Binding var selectedTab: Int
@Binding var showPromotion: Bool
@Binding var promotionMode: PromotionDisplayType
var body: some View {
TabView {
TabView(selection: $selectedTab) {
GrammarCheckView()
.tabItem {
Image(systemName: "book.fill")
Text("Grammar Check")
}
.tag(0)
WordsView()
.tabItem {
Image(systemName: "text.bubble")
Text("Words")
}
.tag(1)
TranslateView()
.tabItem {
Image(systemName: "globe")
Text("Translate")
}
.tag(2)
SettingsView()
.tabItem {
Image(systemName: "gear")
Text("Settings")
}
.tag(3)
}
}
}
struct AllTabView_Preview: View{
@State private var selectedTab = 2
@State private var showPromotion = false
@State private var promotionMode: PromotionDisplayType = .halfScreen
@State private var urlToOpen: String?
var body: some View {
VStack {
AllTabView(selectedTab: $selectedTab, showPromotion: $showPromotion, promotionMode: $promotionMode)
}
}
}
#Preview {
AllTabView()
AllTabView_Preview()
}

View File

@ -83,7 +83,16 @@ class GlobalEnvironment: ObservableObject {
logger.info("baseHost: \(self.baseHost)")
// SandBox
}
// 使
@Published var pushSettings: PushInfo = PushInfo()
struct PushInfo{
var gotoTab: Int = 0
var showPage: Bool = false
var page: String = ""
var showMode: String = ""
var openURL: String = ""
var appAtFront : Bool = false
}
}
//

View File

@ -0,0 +1,118 @@
//
// PushHandler.swift
// AIGrammar
//
// Created by oscar on 2024/8/31.
//
import UIKit
import SwiftUI
import UserNotifications
enum PromotionDisplayType {
case halfScreen
case fullScreen
case urlLink(String) // URL URL
}
class NofifyState: ObservableObject {
@Published var selectedTab: Int = 0
@Published var showPromotion: Bool = false
@Published var promotionMode: PromotionDisplayType = .halfScreen
}
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
// AIGrammarApp
var app: AIGrammarApp?
var window: UIWindow?
var rootView: ContentView?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// UNUserNotificationCenter delegate
UNUserNotificationCenter.current().delegate = self
//
registerForPushNotifications()
return true
}
func registerForPushNotifications() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
logger.info("Permission granted: \(granted)")
guard granted else { return }
self.getNotificationSettings()
}
}
func getNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
logger.info("Notification settings: \(settings)")
guard settings.authorizationStatus == .authorized else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
let token = tokenParts.joined()
logger.info("Device Token: \(token)")
// token
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
logger.error("Failed to register: \(error)")
}
/*
//
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
logger.info("get push msg in willPresent")
globalEnvironment.pushSettings.appAtFront = true
//completionHandler([.sound, .banner])
}
*/
//
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
logger.info("get push msg in didReceive")
let userInfo = response.notification.request.content.userInfo
//
globalEnvironment.pushSettings.gotoTab = extractValue(from: userInfo, key: "gotoTab", defaultValue: 0, logMessage: "gotoTab not found or invalid in userInfo")
globalEnvironment.pushSettings.showPage = extractValue(from: userInfo, key: "showPage", defaultValue: 0, logMessage: "showPage not found or invalid in userInfo") != 0
globalEnvironment.pushSettings.page = extractValue(from: userInfo, key: "page", defaultValue: "", logMessage: "page not found or invalid in userInfo")
globalEnvironment.pushSettings.showMode = extractValue(from: userInfo, key: "showMode", defaultValue: "", logMessage: "showMode not found or invalid in userInfo")
globalEnvironment.pushSettings.openURL = extractValue(from: userInfo, key: "url", defaultValue: "", logMessage: "url not found or invalid in userInfo")
//
if globalEnvironment.pushSettings.appAtFront {
DispatchQueue.main.async {
self.app?.handlePushNotification()
}
globalEnvironment.pushSettings.appAtFront = false
}
completionHandler()
}
// userInfo
func extractValue<T>(from userInfo: [AnyHashable: Any], key: String, defaultValue: T, logMessage: String) -> T {
if let value = userInfo[key] as? T {
return value
} else {
logger.info(logMessage)
return defaultValue
}
}
}
struct PromotionView: View {
var body: some View {
Text("This is the Promotion View")
}
}