Commit c3147e26 authored by shigemi miura's avatar shigemi miura

BackgroundTask試作

parent 866c9797
//
// EcaTask.swift
// Sailassist
//
// Created by 三浦薫巳 on 2023/11/10.
//
import Foundation
import CoreLocation
class EcaTask: Operation {
enum OperationError: Error {
case cancelled
}
// URLSessionのバックグラウンド実行用の識別子
private let identifier: String = "get_articles_operation"
private var req_url: URL
private var dataTask: URLSessionDataTask? = nil
private var titles: [String] = []
// 処理結果を格納する変数
var result: Result<[String], Error>? = nil
init(url: URL) {
self.req_url = url
}
// 非同期処理を行う場合、isAsynchronousをオーバーライドしてtrueを返すようにする
override var isAsynchronous: Bool {
return true
}
// 非同期処理を行う場合、isExecutingのオーバーライドが必要
// 値を変更するときはKVOの変更通知を行う
private var _isExecuting: Bool = false
override var isExecuting: Bool {
get {
_isExecuting
}
set {
willChangeValue(forKey: #keyPath(isExecuting))
_isExecuting = newValue
didChangeValue(forKey: #keyPath(isExecuting))
}
}
// 非同期処理を行う場合、isFinishedのオーバーライドが必要
// 値を変更するときはKVOの変更通知を行う
private var _isFinished: Bool = false
override var isFinished: Bool {
get {
_isFinished
}
set {
willChangeValue(forKey: #keyPath(isFinished))
_isFinished = newValue
didChangeValue(forKey: #keyPath(isFinished))
}
}
// キャンセル処理
override func cancel() {
super.cancel()
if let task = dataTask {
task.cancel()
}
}
// 非同期オペレーションのエントリーポイント
override func start() {
// 実行中フラグON
isExecuting = true
if isCancelled {
completionHandler(result: .failure(OperationError.cancelled))
return
}
// バックグラウンド用セッションの作成
let configuration = URLSessionConfiguration.background(withIdentifier: identifier)
configuration.isDiscretionary = false
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let login = ReqLogin(Id: Preferences.ShipId, Password: Preferences.ShipPassword)
if let postdata = toJSON(login) {
print(debug: "タスクの実行:EcaTask 1")
// RSS XML取得
var req = URLRequest(url: req_url)
req.httpMethod = "POST"
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = postdata
req.timeoutInterval = 20
dataTask = session.dataTask(with: req)
dataTask!.resume()
}
else {
return
}
}
// JSON 文字列に変換
func toJSON<T>(_ reqparam : T) -> Data! where T : Encodable {
let Jencoder = JSONEncoder()
var postdata : Data!
do {
postdata = try Jencoder.encode(reqparam)
} catch {
return nil
}
return postdata
}
// 処理完了ハンドラ
private func completionHandler(result: Result<[String], Error>) {
guard isExecuting else {
return
}
// 実行中フラグOFF
isExecuting = false
self.result = result
self.dataTask = nil
// 終了フラグON
isFinished = true
}
}
extension EcaTask: URLSessionDelegate, URLSessionDataDelegate {
// データ受信完了
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
print(debug: "タスクの実行:EcaTask 2")
if isCancelled {
completionHandler(result: .failure(OperationError.cancelled))
return
}
let xml = String(data: data, encoding: .utf8)!
do {
var titles: [String] = []
// 正規表現でタイトル抽出
let pattern = "<title>([^<]+)</title>"
let regex = try NSRegularExpression(pattern: pattern, options: [
.anchorsMatchLines, .dotMatchesLineSeparators
])
let range = NSRange(location: 0, length: xml.count)
let results = regex.matches(in: xml, options: [], range: range)
for result in results {
let start = xml.index(xml.startIndex, offsetBy: result.range(at: 1).location)
let end = xml.index(start, offsetBy: result.range(at: 1).length)
let text = String(xml[start..<end])
titles.append(text)
}
// 1行目はRSSのタイトルなので落とす
if !titles.isEmpty {
titles.remove(at: 0)
}
self.titles = titles
print(debug: "タスクの実行:EcaTask 3")
SharingData.My.location?.latitude = 35.0
SharingData.My.location?.longitude = 135.0
var distance = 0.0
if let nowlocation = SharingData.My.location {
distance = LocationCalculation.checkPolyline(objPos: iceland, shipPos: nowlocation)
}
print(debug: "called: \(distance) \n")
}
catch {
// No Action
}
}
// タスク完了
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if isCancelled {
completionHandler(result: .failure(OperationError.cancelled))
return
}
if let error = error {
self.completionHandler(result: .failure(error))
}
else {
self.completionHandler(result: .success(titles))
}
}
}
// var sharingData = SharingData.shared
//
// override func main() {
// print(debug: "タスクの実行:EcaTask 1")
// let login = ReqLogin(Id: Preferences.ShipId, Password: Preferences.ShipPassword)
// let sessionLogin = SessionLogin.OnlyOne
// sessionLogin.RequestLogin(login)
// }
// private func checkEca() {
// print(debug: "タスクの実行:EcaTask3")
// let ecaArea = sharingData.ecaArea.map{ $0.1 }.filter{ $0.isRunning == true }
//// if let ecaArea = ecaArea[0] {
//// print(debug: "called")
// var distance = 0.0
// if let nowlocation = SharingData.My.location {
// distance = LocationCalculation.checkPolyline(objPos: iceland, shipPos: nowlocation)
// }
// print(distance)
//// }
// }
// override func main() {
// print(debug: "タスクの実行(eca)")
// print("this operation refresh id is \(self.id)")
// let location = SharingData.My.location
//
// let ecaArea = sharingData.ecaArea.map{ $0.1 }.filter{ $0.isRunning == true }
// if let ecaArea = ecaArea.first {
// print(debug: ecaArea)
// }
// }
// override func main() {
// print(debug: "called eca task")
// let ecaArea = sharingData.ecaArea.map{ $0.1 }.filter{ $0.isRunning == true }
// if let ecaArea = ecaArea.first {
// var points: [CLLocationCoordinate2D] = []
// points = ecaArea.first?.points
// let distance = LocationCalculation.checkPolyline(objPos: points, shipPos: location)
// }
// @Published var isRunning: Bool = false //ECA実行中
// @Published var name: String = "" //ECA名称
// @Published var swNotice: UInt32 = 6 //ECA通知[NM]
// @Published var swStart: UInt32 = 5 //ECA開始[NM]
// @Published var swFinish: UInt32 = 4 //ECA終了[NM]
// }
//}
......@@ -2,7 +2,17 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.sailassist.process</string>
<string>com.sailassist.refresh</string>
</array>
<key>MBXAccessToken</key>
<string>pk.eyJ1Ijoiam1hcmluZWNsb3VkIiwiYSI6ImNsbmxjbGYzZjA0dG8yaW82MDgwajQ5OTQifQ.pd8YC9qK1C4YmMUbMx6ywQ</string>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
</dict>
</plist>
......@@ -34,6 +34,7 @@ struct LoginView: View {
@State var isProgressView = false
@State var isAlert = false
@State var alertType: AlertType = .loginFailure
@State var timer :Timer?
@ObservedObject var scannerViewModel = ScannerViewModel()
@State var loginViewParam = LoginViewParam()
var sessionLogin = SessionLogin()
......@@ -137,12 +138,26 @@ struct LoginView: View {
}
func responseLogin(result: Result<Data, APIError>) {
print(debug: "calld")
print(debug: "called")
switch result {
case .success(let resultData):
let jsonstr = String(data: resultData, encoding: .utf8)!
Preferences.lastLoginDate_Int64 = DateTextLib.Date2UnixTime(date: Date())
isLogin = true
let url_string : String = HttpRequestType.RegisterLogin.rawValue
guard let req_url = URL(string : url_string) else {
return
}
timer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { _ in
print(debug: "called timer")
let ecaTask = EcaTask(url: req_url)
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
queue.addOperation(ecaTask)
}
case .failure(let errorCode):
isLogin = false
isAlert = true
......
......@@ -16,6 +16,7 @@ struct InputUserNameView: View {
@Binding var isAlert : Bool
@Binding var alertType: AlertType
@State private var isEmptyAlert = false
@State var timer :Timer?
let itemHPadding: Double = 16
let dialogBottomPadding: Double = 32
let textFieldHeight: Double = 40
......@@ -68,6 +69,7 @@ struct InputUserNameView: View {
isProgressView = true
let login = ReqLogin(Id: param.shipId, Password: param.password)
sessionLogin.RequestLogin(login, completion: responseLogin)
Preferences.UserName = param.userName
}
}, label: {
Text("Sign In")
......@@ -93,7 +95,7 @@ struct InputUserNameView: View {
}
func responseLogin(result: Result<Data, APIError>) {
print(debug: "calld")
print(debug: "called")
switch result {
case .success(let resultData):
let serverSession = ServerSession()
......@@ -104,6 +106,20 @@ struct InputUserNameView: View {
isProgressView = false
isLogin = true
isAlert = false
let url_string : String = HttpRequestType.RegisterLogin.rawValue
guard let req_url = URL(string : url_string) else {
return
}
timer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { _ in
print(debug: "called timer")
let ecaTask = EcaTask(url: req_url)
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
queue.addOperation(ecaTask)
}
case .failure(let errorCode):
isProgressView = false
isLogin = false
......
......@@ -6,6 +6,9 @@
//
import SwiftUI
import UIKit
import BackgroundTasks
import Combine
@main
struct SailassistApp: App {
......@@ -17,12 +20,120 @@ struct SailassistApp: App {
}
}
private let appRefreshIdentifier: String = "com.sailassist.refresh"
private let appProcessIdentifier: String = "com.sailassist.process"
//@UIApplicationMain
class AppDelegate: NSObject, UIApplicationDelegate{
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
print(debug: "called")
let config = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
config.delegateClass = SceneDelegate.self
return config
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print(debug: "called")
// 第一引数: Info.plistで定義したIdentifierを指定
// 第二引数: タスクを実行するキューを指定。nilの場合は、デフォルトのバックグラウンドキューが利用されます。
// 第三引数: 実行する処理
BGTaskScheduler.shared.register(forTaskWithIdentifier: appRefreshIdentifier, using: nil) { task in
print(debug: "タスクの登録(Refresh)")
print(task)
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
BGTaskScheduler.shared.register(forTaskWithIdentifier: appProcessIdentifier, using: nil) { task in
print(debug: "タスクの登録(Processing)")
print(task)
self.handleAppProcessing(task: task as! BGProcessingTask)
}
// 予約されているタスクの確認
Task {
print(debug: "タスクの確認")
let allRequests = await BGTaskScheduler.shared.pendingTaskRequests()
print(debug: allRequests)
}
return true
}
// バックグラウンド処理したい内容
private func handleAppRefresh(task: BGAppRefreshTask) {
// 1日の間、何度も実行したい場合は、1回実行するごとに新たにスケジューリングに登録します
scheduleAppRefresh()
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
// 時間内に実行完了しなかった場合は、処理を解放します
// バックグラウンドで実行する処理は、次回に回しても問題ない処理のはずなので、これでOK
task.expirationHandler = {
queue.cancelAllOperations()
}
let url_string : String = HttpRequestType.RegisterLogin.rawValue
guard let req_url = URL(string : url_string) else {
return
}
let operation = EcaTask(url: req_url)
operation.completionBlock = {
task.setTaskCompleted(success: operation.isFinished)
}
queue.addOperation(operation)
// サンプルの処理をキューに詰めます
// let array = [1, 2, 3, 4, 5]
// array.enumerated().forEach { arg in
// let (offset, value) = arg
// let operation = PrintProcessing(id: value)
// if offset == array.count - 1 {
// operation.completionBlock = {
// // 最後の処理が完了したら、必ず完了したことを伝える必要があります
// task.setTaskCompleted(success: operation.isFinished)
// }
// }
// queue.addOperation(operation)
// }
}
private func handleAppProcessing(task: BGProcessingTask) {
// 1日の間、何度も実行したい場合は、1回実行するごとに新たにスケジューリングに登録します
scheduleAppProcessing()
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
// 時間内に実行完了しなかった場合は、処理を解放します
// バックグラウンドで実行する処理は、次回に回しても問題ない処理のはずなので、これでOK
task.expirationHandler = {
queue.cancelAllOperations()
}
// let url_string : String = HttpRequestType.RegisterLogin.rawValue
// guard let req_url = URL(string : url_string) else {
// return
// }
// let operation = EcaTask(url: req_url)
// operation.completionBlock = {
// task.setTaskCompleted(success: operation.isFinished)
// }
// queue.addOperation(operation)
// サンプルの処理をキューに詰めます
let array = [1, 2, 3, 4, 5]
array.enumerated().forEach { arg in
let (offset, value) = arg
let operation = PrintOperation(id: value)
if offset == array.count - 1 {
operation.completionBlock = {
// 最後の処理が完了したら、必ず完了したことを伝える必要があります
task.setTaskCompleted(success: operation.isFinished)
}
}
queue.addOperation(operation)
}
}
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate, ObservableObject {
......@@ -33,7 +144,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, ObservableObject {
windowScene = scene as? UIWindowScene
}
func addTabBar(_ selectedTabModel: SelectedTabModel){
guard let scene = windowScene else{
return
......@@ -53,8 +163,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, ObservableObject {
self.tabWindow = tabWindow
}
}
//アプリがフォアグラウンドからバックグラウンドに移行
func sceneDidEnterBackground(_ scene: UIScene) {
print(debug: "called")
scheduleAppRefresh()
scheduleAppProcessing()
}
}
class PassThroughWindow: UIWindow{
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
......@@ -63,6 +179,85 @@ class PassThroughWindow: UIWindow{
}
}
private func scheduleAppRefresh() {
if let scheduledTime = Calendar.current.date(byAdding: .minute, value: 5, to: Date()) {
print(debug: "called")
// Info.plistで定義したIdentifierを指定
let request = BGAppRefreshTaskRequest(identifier: appRefreshIdentifier)
// 最低で、どの程度の期間を置いてから実行するか指定
// request.earliestBeginDate = Date(timeIntervalSinceNow: 1 * 60)
request.earliestBeginDate = scheduledTime
do {
// スケジューラーに実行リクエストを登録
print(debug: request)
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}
}
private func scheduleAppProcessing() {
if let scheduledTime = Calendar.current.date(byAdding: .minute, value: 5, to: Date()) {
print(debug: "called")
// Info.plistで定義したIdentifierを指定
let request = BGProcessingTaskRequest(identifier: appProcessIdentifier)
// 通信が発生するか否かを指定
// request.requiresNetworkConnectivity = false
// CPU監視の必要可否を設定
// request.requiresExternalPower = true
request.earliestBeginDate = scheduledTime
// ネットワーク接続を必要とするならtrueにする
request.requiresNetworkConnectivity = true
// 外部電源に接続されている場合にのみ実行するならtrue
request.requiresExternalPower = false
do {
// スケジューラーに実行リクエストを登録
print(debug: request)
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app processing: \(error)")
}
}
}
func periodicalFunction() {
print("update")
}
class PrintOperation: Operation {
let id: Int
init(id: Int) {
print(debug: "タスクの初期化(Refresh)")
self.id = id
}
override func main() {
print(debug: "タスクの実行(Refresh)")
print("this operation refresh id is \(self.id)")
}
}
class PrintProcessing: Operation {
let id: Int
init(id: Int) {
print(debug: "タスクの初期化(Processing)")
self.id = id
}
override func main() {
print(debug: "タスクの実行(Processing)")
print("this operation processing id is \(self.id)")
}
}
// MARK: コメント出力を消す
func print(debug: Any = "", function: String = #function, file: String = #file, line: Int = #line) {
#if DEBUG
......
//
// ServerAccessProtocol.swift
// Sailassist
//
// Created by 三浦薫巳 on 2023/11/12.
//
import WebKit
protocol ServerAccessDelegate {
func serverErrorResponse( errorCode : APIError )
func serverNormalResponse( resultData : Data )
}
......@@ -6,8 +6,10 @@
//
import Foundation
import WebKit
class ServerSession{
class ServerSession: NSObject, URLSessionDelegate, URLSessionDataDelegate {
var delegate: ServerAccessDelegate!
// JSON 文字列に変換
func toJSON<T>(_ reqparam : T) -> Data! where T : Encodable {
......@@ -78,6 +80,47 @@ class ServerSession{
}).resume()
}
// サーバ通信汎用クロージャー版
func postJson(_ req_url : URL, _ postdata : Data) {
print(debug: "タスクの実行:EcaTask 3")
// リクエストデータ作成
var req = URLRequest(url: req_url)
req.httpMethod = "POST"
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = postdata
req.timeoutInterval = 20 // タイムアウト3秒 → Amedasでタイムアウトエラー20秒に変更
let config = URLSessionConfiguration.background(withIdentifier: "get_articles_operation")
config.isDiscretionary = false
// サーバー送信
let session = URLSession(configuration: .default, delegate: nil, delegateQueue: OperationQueue.main)
// let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.dataTask(with: req, completionHandler: {(data, response, error ) in
session.finishTasksAndInvalidate()
if error != nil { // ここのエラーはクライアントサイドのエラー(ホストに接続できないなど)
self.delegate.serverErrorResponse(errorCode: error as! APIError)
return
}
guard let indata = data, let inresponse = response as? HTTPURLResponse else {
self.delegate.serverErrorResponse(errorCode: error as! APIError)
return
}
if inresponse.statusCode != 200 { // レスポンスのステータスコードが200でない場合などはサーバサイドエラー
print( debug: inresponse.statusCode )
self.delegate.serverErrorResponse(errorCode: error as! APIError)
return
}
self.delegate.serverNormalResponse(resultData: indata)
return
})
task.resume()
}
// HTTPCookieStorageに特定のCookieが保存されているかを調べる
func isCookieSet(name: String, url: URL) -> Bool {
if let cookies = HTTPCookieStorage.shared.cookies(for: url) {
......
......@@ -7,11 +7,13 @@
import Foundation
import SwiftUI
import CoreLocation
class SessionLogin : ObservableObject {
class SessionLogin : ObservableObject, ServerAccessDelegate {
@Published var status = false
// シングルトン宣言
static let OnlyOne = SessionLogin()
var sharingData = SharingData.shared
private var serverSession = ServerSession()
private var Calling : Bool = false // 通信中
......@@ -22,7 +24,8 @@ class SessionLogin : ObservableObject {
* - login: ログイン情報
*/
func RequestLogin(_ login : ReqLogin, completion: @escaping ((Result<Data, APIError>)) -> Void) {
print(debug: "calld")
print(debug: "タスクの実行:EcaTask 10")
print(debug: "called")
if Calling {
return
}
......@@ -43,4 +46,52 @@ class SessionLogin : ObservableObject {
return
}
}
func RequestLogin(_ login : ReqLogin) {
print(debug: "タスクの実行:EcaTask 2")
if Calling {
return
}
Calling = true
serverSession.delegate = self
// リクエストURLの組み立て
let url_string : String = HttpRequestType.RegisterLogin.rawValue
guard let req_url = URL(string : url_string) else {
Calling = false
return
}
if let postdata = serverSession.toJSON(login) {
serverSession.postJson(req_url, postdata)
}
else {
Calling = false
return
}
}
// エラーデリゲータ
func serverErrorResponse(errorCode: APIError) {
print(debug: "タスクの実行:EcaTask 6")
print(debug: "called: \(errorCode) \n")
Calling = false
}
// ノーマルデリゲータ
func serverNormalResponse(resultData: Data) {
print(debug: "タスクの実行:EcaTask 4")
let resjson = serverSession.fromJSON(resultData: resultData, resltType: ResLogin.self)
// let ecaArea = sharingData.ecaArea.map{ $0.1 }.filter{ $0.isRunning == true }
// if let ecaArea = ecaArea[0] {
// print(debug: "called")
SharingData.My.location?.latitude = 35.0
SharingData.My.location?.longitude = 135.0
var distance = 0.0
if let nowlocation = SharingData.My.location {
distance = LocationCalculation.checkPolyline(objPos: iceland, shipPos: nowlocation)
}
print(debug: "called: \(distance) \n")
Calling = false
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment