Commit 8cec0939 authored by shigemi miura's avatar shigemi miura

・クラス分割

・チャットのイメージデータの先読み ・イメージデータ送信時はテンポラリを表示
parent d2befa8b
......@@ -38,9 +38,9 @@ struct NotificationView: View {
.onAppear {
let pushCount = pushHist.viewCnt
print(debug: "\(pushCount)")
DispatchQueue.main.async {
pushHist.viewCnt = 0
}
// DispatchQueue.main.async {
// pushHist.viewCnt = 0
// }
}
}
......
......@@ -9,6 +9,7 @@ import Foundation
class GetMessage {
var sessionGetMessage = SessionGetMessage()
var chatViewModel: ChatViewModel?
func start() {
print(debug: "called")
......@@ -27,6 +28,11 @@ class GetMessage {
if let msg = res.messages {
//既読マーク確認
SharingData.message.messages = msg
Task { @MainActor in
self.chatViewModel?.loadMessages()
}
self.checkUnreadMessages()
}
SharingData.message.users = []
......
......@@ -12,20 +12,21 @@ enum ImageQualityPreset {
}
}
var targetSize: CGSize {
var scale: CGFloat {
switch self {
case .low: return CGSize(width: 640, height: 480)
case .middle: return CGSize(width: 1280, height: 960)
case .high: return CGSize(width: 1920, height: 1440)
case .low: return 0.3
case .middle: return 0.6
case .high: return 1.0
}
}
}
extension UIImage {
func resized(to targetSize: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: targetSize)
func resized(to scale: CGFloat) -> UIImage? {
let newSize = CGSize(width: self.size.width * scale, height: self.size.height * scale)
let renderer = UIGraphicsImageRenderer(size: newSize)
return renderer.image { _ in
self.draw(in: CGRect(origin: .zero, size: targetSize))
self.draw(in: CGRect(origin: .zero, size: newSize))
}
}
}
......@@ -41,19 +41,46 @@ struct Imagepicker : UIViewControllerRepresentable {
}
//MARK: - Use Photo
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let image = info[.originalImage] as? UIImage {
let resizedImage = image.resized(to: parent.preset.targetSize)
if let data = resizedImage.jpegData(compressionQuality: parent.preset.compressionQuality) {
parent.image = data
//MARK: - 画像データのリサイズ
DispatchQueue.global(qos: .userInitiated).async {
// リサイズ処理
let scale: CGFloat = min(0.1, max(self.parent.preset.scale, 1.0))
if let resizedImage = image.resized(to: scale),
let data = resizedImage.jpegData(compressionQuality: self.parent.preset.compressionQuality) {
DispatchQueue.main.async {
self.parent.image = data
self.parent.show.toggle()
}
} else {
DispatchQueue.main.async {
print("Failed to resize or compress image")
self.parent.show.toggle()
}
}
}
} else if let videoURL = info[.mediaURL] as? URL {
// 動画ファイルの処理(例: Dataに変換して保存)
if let videoData = try? Data(contentsOf: videoURL) {
parent.image = videoData
//MARK: - 動画データのリサイズ
DispatchQueue.global(qos: .userInitiated).async {
if let videoData = try? Data(contentsOf: videoURL) {
DispatchQueue.main.async {
self.parent.image = videoData
self.parent.show.toggle()
}
} else {
DispatchQueue.main.async {
print("Failed to process video data")
self.parent.show.toggle()
}
}
}
} else {
DispatchQueue.main.async {
print("No valid media selected")
self.parent.show.toggle()
}
}
parent.show.toggle()
}
}
}
import SwiftUI
import Combine
class KeyboardResponder: ObservableObject {
@Published var currentHeight: CGFloat = 0
@Published var isKeyboardVisible: Bool = false
private var cancellableSet: Set<AnyCancellable> = []
init() {
let willShow = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
.compactMap { notification -> CGFloat? in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.isKeyboardVisible = true
}
return (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
}
let willHide = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ -> CGFloat in
DispatchQueue.main.async {
self.isKeyboardVisible = false
}
return 0
}
Publishers.Merge(willShow, willHide)
.receive(on: RunLoop.main)
.assign(to: \KeyboardResponder.currentHeight, on: self)
.store(in: &cancellableSet)
}
deinit {
cancellableSet.forEach { $0.cancel() }
}
}
......@@ -8,55 +8,42 @@
import SwiftUI
struct ChatUrlImageView: View {
var imageUrl = ""
init(message: ChatMessage) {
if let url = message.message {
self.imageUrl = url
}
}
let imageUrl: String
var onLoad: (() -> Void)? = nil
var body: some View {
AsyncImage(url: URL(string: imageUrl)) { phase in
if let image = phase.image {
image
.resizable(resizingMode: .stretch)
.aspectRatio(contentMode: .fit)
.frame(width: 250)
} else if phase.error != nil {
Color.gray.opacity(0.2)
.overlay(Image(systemName: "rectangle.slash"))
} else {
Color.gray.opacity(0.2)
ProgressView()
if let url = URL(string: imageUrl) {
AsyncImage(url: url) { phase in
switch phase {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 250)
.onAppear {
onLoad?()
}
case .failure(_):
VStack {
Image(systemName: "exclamationmark.triangle")
Text("Image loading failed")
.font(.caption)
}
.frame(width: 250, height: 250)
.background(Color.gray.opacity(0.2))
case .empty:
ProgressView()
.frame(width: 250, height: 250)
.background(Color.gray.opacity(0.2))
@unknown default:
EmptyView()
}
}
} else {
Color.gray.opacity(0.2)
.frame(width: 250, height: 250)
.overlay(Text("Invalid URL"))
}
.cornerRadius(16)
.frame(height: 250)
}
}
#Preview {
ChatUrlImageView(message: ChatMessage(
shipId: 10000003,
messageId: "92c2dfb5-f5ed-4943-98a3-9848d7f9a962",
type: 0,
time: "2023-10-06T01:51:01.872Z",
location: 1,
from: "はだだ",
fromId: "487420489",
mode: 0,
message: "999",
stampId: 0,
viewer: [
Viewer(
time: "2023-10-06T01:51:12.973Z",
location: 1,
id: ""),
Viewer(
time: "2023-10-06T01:51:12.973Z",
location: 2,
id: "")
]
))
}
import SwiftUI
import AVKit
struct ChatUrlVideoView: View {
@State private var player: AVPlayer?
@State private var isPlaying = false
let imageUrl: String
var onLoad: (() -> Void)? = nil
var body: some View {
ZStack {
if let url = URL(string: imageUrl) {
VideoPlayer(player: player)
.frame(height: 250)
.cornerRadius(16)
.onAppear {
player = AVPlayer(url: url)
onLoad?()
}
.onTapGesture {
if isPlaying {
player?.pause()
} else {
player?.play()
}
isPlaying.toggle()
}
} else {
Color.gray.opacity(0.2)
.frame(height: 250)
.overlay(
VStack {
Image(systemName: "video.slash")
Text("Video URL is invalid")
.font(.caption)
}
)
}
}
}
}
......@@ -13,6 +13,10 @@ struct ChatView: View {
@State var isShowMember: Bool = false
@State var isNotification = Preferences.ChatNotification
@State var isFocus: Bool = true
@State private var loadedMediaCount: Int = 0
@State private var totalMediaCount: Int = 0
@State private var isMediaLoading: Bool = false
@State private var isUploadingDialogPresented: Bool = false
var body: some View {
ZStack {
......@@ -30,12 +34,12 @@ struct ChatView: View {
ForEach(message.messages, id: \.messageId) { msg in
if msg.message != nil {
if msg.from == Preferences.UserName {
//自分のメッセージ
MyChatContentView(message: msg)
//MARK: - 自分のメッセージ
MyChatContentView(message: msg, onMediaLoaded: {handleMediaLoaded(proxy: proxy)})
.padding(.bottom, 24)
}else{
//他人のメッセージ
OtherChatContentView(message: msg)
//MARK: - 他人のメッセージ
OtherChatContentView(message: msg, onMediaLoaded: {handleMediaLoaded(proxy: proxy)})
.padding(.bottom, 24)
}
} else {
......@@ -48,27 +52,32 @@ struct ChatView: View {
.onAppear {
guard !message.messages.isEmpty,
let id = message.messages.last?.messageId else { return }
proxy.scrollTo(id, anchor: .bottom)
DispatchQueue.main.async {
message.viewCnt = 0
totalMediaCount = message.messages.reduce(0) { count, msg in
count + (msg.type >= 2 ? 1 : 0)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
proxy.scrollTo(id, anchor: .bottom)
}
}
.onChange(of: message.messages.count) { newValue in
print(debug: "ChatView: onChange { newValue: \(newValue)")
withAnimation {
if let id = message.messages.last?.messageId {
proxy.scrollTo(id, anchor: .bottom)
//Warningモードは既読は返さない
if message.messages.last?.message != nil {
if message.messages.last?.from != Preferences.UserName {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let getMessage = GetMessage()
getMessage.readNotification()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
proxy.scrollTo(id, anchor: .bottom)
//MARK: - Warningモードは既読は返さない
if message.messages.last?.message != nil {
if message.messages.last?.from != Preferences.UserName {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let getMessage = GetMessage()
getMessage.readNotification()
}
}
}
}
message.viewCnt = 0
}
}
}
......@@ -77,7 +86,6 @@ struct ChatView: View {
guard !value.isEmpty else {return}
if let id = message.messages.last?.messageId {
proxy.scrollTo(id, anchor: .bottom)
message.viewCnt = 0
}
}
}
......@@ -116,19 +124,80 @@ struct ChatView: View {
.onTapGesture {
isFocus = false
}
ChatInputView(isFocus: $isFocus)
.onAppear {
//MARK: - 画面が表示された時に実行 既読処理
if message.viewCnt > 0 {
message.viewCnt = 0
}
}
ChatInputView(isFocus: $isFocus, isUploadingDialogPresented: $isUploadingDialogPresented)
}
if !isMediaLoading {
LoadingView()
}
if isUploadingDialogPresented {
// UpLoadingView()
}
}
.background(ColorSet.BackgroundPrimary.color)
}
//MARK: - 動画・画像読み込み完了後に最新チャットに移動
private func handleMediaLoaded(proxy: ScrollViewProxy) {
guard !message.messages.isEmpty,
let id = message.messages.last?.messageId else { return }
loadedMediaCount += 1
if loadedMediaCount >= totalMediaCount {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
proxy.scrollTo(id, anchor: .bottom)
isMediaLoading = true
}
}
}
}
// MARK: - 全てのチャットのローディング画面
struct LoadingView: View {
var body: some View {
ZStack {
Color.black.opacity(1.0)
.edgesIgnoringSafeArea(.all)
ProgressView("Loading...")
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.foregroundColor(.white)
.padding()
.background(Color.gray.opacity(0.7))
.cornerRadius(10)
}
}
}
// MARK: - 動画・画像のアップロード画面
struct UpLoadingView: View {
var body: some View {
ZStack {
Color.black.opacity(0.8)
.edgesIgnoringSafeArea(.all)
ProgressView("UpLoading...")
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.foregroundColor(.white)
.padding()
.background(Color.gray.opacity(0.7))
.cornerRadius(10)
}
}
}
struct AlertChatMessage: View {
@Environment(\ .colorScheme) var colorScheme
@Environment(\.colorScheme) var colorScheme
var message : ChatMessage
var body: some View {
if message.mode == ChatMode.warningProgress.rawValue {
switch message.mode {
case ChatMode.warningProgress.rawValue:
HStack() {
Rectangle()
.fill(ColorSet.ChatDate.color)
......@@ -140,7 +209,7 @@ struct AlertChatMessage: View {
.fill(ColorSet.ChatDate.color)
.frame(width: 20, height: 1)
}
} else {
case ChatMode.normal.rawValue:
HStack() {
Rectangle()
.fill(ColorSet.ChatDate.color)
......@@ -152,6 +221,8 @@ struct AlertChatMessage: View {
.fill(ColorSet.ChatDate.color)
.frame(width: 20, height: 1)
}
default:
EmptyView()
}
}
}
......
......@@ -46,7 +46,6 @@ enum Corners{
case br
}
fileprivate struct CustomCornerRadiusModifier: ViewModifier {
let cornerRadius: CGFloat
let corners: [Corners]
......
......@@ -9,14 +9,18 @@ import SwiftUI
struct MyChatContentView: View {
var message : ChatMessage
var onMediaLoaded: (() -> Void)? = nil
var body: some View {
HStack {
Spacer()
VStack(alignment: .trailing, spacing: 6) {
Group {
if let msg = message.message {
if msg.contains("https://") {
ChatUrlImageView(message: message)
if msg.contains(".jpg") || msg.contains(".png") {
ChatUrlImageView(imageUrl: msg, onLoad: onMediaLoaded)
} else if msg.contains(".mp4") || msg.contains(".mov") {
ChatUrlVideoView(imageUrl: msg, onLoad: onMediaLoaded)
} else {
Text(msg)
.font(FontStyle.DefaultText.font)
......@@ -36,7 +40,7 @@ struct MyChatContentView: View {
}
HStack(spacing: 5){
//既読マーク
//MARK: - 既読マーク
Text(DateTextLib.ISO86012FormatText(message.time, format: "yyyy/MM/dd HH:mm", errFormat: ""))
.padding(.trailing, 8)
......
......@@ -9,6 +9,8 @@ import SwiftUI
struct OtherChatContentView: View {
var message : ChatMessage
var onMediaLoaded: (() -> Void)? = nil
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
......@@ -20,8 +22,10 @@ struct OtherChatContentView: View {
VStack(alignment: .leading, spacing: 10) {
Group {
if let msg = message.message {
if msg.contains("https://") {
ChatUrlImageView(message: message)
if msg.contains(".jpg") || msg.contains(".png") {
ChatUrlImageView(imageUrl: msg, onLoad: onMediaLoaded)
} else if msg.contains(".mp4") || msg.contains(".mov") {
ChatUrlVideoView(imageUrl: msg, onLoad: onMediaLoaded)
} else {
Text(msg)
.font(FontStyle.DefaultText.font)
......
import SwiftUI
import Combine
import Speech
enum MediaInputType {
case none
case camera
case photoLibrary
case fileImport
}
class ChatInputViewModel: ObservableObject {
@Published var failedUploadImage: ReqUploadImage? = nil
@Published var isRetryDialogPresented = false
@Published var isSignalrRestart = false
@Published var isRecording = false
@Published var isChatAlert = false
@Published var inputText = ""
@Published var textViewHeight: CGFloat = 40
@Published var tempId: String = ""
@Published var mediaInputType: MediaInputType = .none
@Published var isImagePickerPresented = false
@Published var isFileImporterPresented = false
@Published var isKeyboardFocused: Bool = false
@Published var isUploadingDialogPresented: Bool = false
@Published var isFocus: Double = 0.0
private let sessionUploadImage = SessionUploadImage()
//MARK: - メディア入力
func handleMediaInput(type: MediaInputType) {
self.mediaInputType = type
if type == .camera || type == .photoLibrary {
self.isImagePickerPresented = true
} else if type == .fileImport {
self.isFileImporterPresented = true
}
}
// MARK: - Send Chat Image
func sendChatImage(_ uploadImage: ReqUploadImage) {
Task {
do {
isUploadingDialogPresented = true
let response = try await sessionUploadImage.requestUploadImage(uploadImage)
print(debug: "Upload success: \(response)")
let serverSession = ServerSession()
_ = serverSession.fromJSON(resultData: response, resltType: ResLogin.self)
sessionUploadImage.progress = 0.0 // 完了後リセット
isUploadingDialogPresented = false
} catch {
print(debug: "Upload failed: \(error)")
sessionUploadImage.progress = 0.0 // エラー時もリセット
isUploadingDialogPresented = false
failedUploadImage = uploadImage
isRetryDialogPresented = true
}
}
}
// MARK: - Send Chat Message
func sendChatMessage() {
guard !SharingData.message.sendInf else { return }
isRecording = false
SignalR().chatMessage(message: inputText, completion: responseChatMessage)
SharingData.message.sendInf = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.restartChatMessage()
}
}
// MARK: - Chat Response
private func responseChatMessage(error: Error?) {
SharingData.message.sendInf = false
if let error = error {
print("Chat error: \(error)")
isChatAlert = true
} else {
isKeyboardFocused = false
inputText = ""
}
}
// MARK: - Restart Chat if No Response
private func restartChatMessage() {
if SharingData.message.sendInf {
SignalR().stopConnection()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.isSignalrRestart = true
}
}
}
//MARK: - テキスト高さ
func recalculateHeight() {
let textView = UITextView()
textView.text = inputText
textView.font = UIFont.preferredFont(forTextStyle: .body)
let fixedWidth = UIScreen.main.bounds.width - 40 // 適宜調整
let newSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
textViewHeight = max(newSize.height, 40) // 最低高さを40に設定
}
// MARK: - ローカル画像を一時保存してURLを取得
func saveImageToTemporaryDirectory(_ image: UIImage) -> String? {
let fileName = UUID().uuidString + ".jpg"
let tempDir = FileManager.default.temporaryDirectory
let fileURL = tempDir.appendingPathComponent(fileName)
guard let data = image.jpegData(compressionQuality: 0.8) else {
print("Image JPEG conversion failed")
return nil
}
do {
try data.write(to: fileURL)
return fileURL.absoluteString
} catch {
print("Image JPEG conversion failed: \(error.localizedDescription)")
return nil
}
}
//MARK: - 送信前に送信画像をテンポラリに入れる
func sendImageToTemporary(_ image: UIImage) {
if let localURL = saveImageToTemporaryDirectory(image),
let jpegData = image.jpegData(compressionQuality: 1.0) {
tempId = UUID().uuidString
let viewer = Viewer(
time: DateTextLib.Date2ISO8601Text(Date()),
location: 2,
id: "",
name: ""
)
let tempMessage = ChatMessage(
shipId: Preferences.shipId,
messageId: tempId,
type: 2,
time: DateTextLib.Date2ISO8601Text(Date()),
location: 2,
from: Preferences.UserName,
fromId: String(SharingData.my.id),
mode: SharingData.message.mode ? 1 : 0,
message: localURL,
stampId: 0,
viewer: [viewer]
)
SharingData.message.messages.append(tempMessage)
let uploadImage = ReqUploadImage(
shipId: Preferences.shipId,
messageId: tempId,
location: 2,
from: Preferences.UserName,
fromId: String(SharingData.my.id),
files: jpegData
)
sendChatImage(uploadImage)
}
}
}
import SwiftUI
import AVFoundation
@MainActor
class ChatViewModel: ObservableObject {
@Published var messages: [ChatMessage] = []
@Published var isMediaPrefetched: Bool = false
func loadMessages() {
messages = SharingData.message.messages
prefetchMedia()
}
private func prefetchMedia() {
let imageURLs = messages.filter { $0.type == 2 && $0.message != nil }.compactMap { $0.message }.compactMap { URL(string: $0) }
let videoURLs = messages.filter { $0.type == 3 && $0.message != nil }.compactMap { $0.message }.compactMap { URL(string: $0) }
Task {
await withTaskGroup(of: Void.self) { group in
for url in imageURLs {
group.addTask {
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 10)
_ = try? await URLSession.shared.data(for: request)
}
}
for url in videoURLs {
group.addTask {
let asset = AVURLAsset(url: url)
do {
_ = try await asset.load(.isPlayable)
} catch {
print(debug: "Video loading failure: \(url)")
}
}
}
}
isMediaPrefetched = true
}
}
}
......@@ -14,6 +14,7 @@ class LoginViewModel: ObservableObject{
struct ContentView: View {
@StateObject private var loginViewModel = LoginViewModel()
@EnvironmentObject private var sceneDelegate: SceneDelegate
@StateObject private var chatViewModel = ChatViewModel()
let selectedTabModel = SelectedTabModel()
......@@ -38,6 +39,7 @@ struct ContentView: View {
}, content: {
LoginView(isLogin: $loginViewModel.isLogin)
.environmentObject(selectedTabModel)
.environmentObject(chatViewModel)
})
}
}
......
......@@ -908,9 +908,9 @@ class LocationCalculation{
distance = rtn.xte * -1
}
if let dist = distance {
// if let dist = distance {
// print(debug: "checkPolyline \(dist)")
}
// }
return distance
}
......
......@@ -37,6 +37,7 @@ struct LoginView: View {
@ObservedObject var scannerViewModel = ScannerViewModel()
@EnvironmentObject var locationViewModel: LocationViewModel
@EnvironmentObject var selectedTabModel: SelectedTabModel
@EnvironmentObject var chatViewModel: ChatViewModel
@Binding var isLogin: Bool
@State var isQrRead: Bool = false
@State var viewMode: LoginViewMode = .SelectType
......@@ -153,9 +154,7 @@ struct LoginView: View {
}
}
/**
* QRコードでのログイン
*/
//MARK: - QRコードでのログイン
func LoginCheckQR() -> () {
isProgressView = true
loginViewParam.shipId = Preferences.Id
......@@ -167,9 +166,7 @@ struct LoginView: View {
isProgressView = false
}
/**
* Autoログイン
*/
//MARK: - Autoログイン
func LoginCheck() -> () {
let lastUnixTime = Preferences.lastLoginDate_Int64 ?? 0
let lastDate = DateTextLib.UnixTime2Date(lastUnixTime)
......@@ -211,6 +208,7 @@ struct LoginView: View {
isLogin = true
let message = GetMessage()
message.chatViewModel = chatViewModel
message.start()
timer = Timer.scheduledTimer(withTimeInterval: TimerInterval, repeats: true) { _ in
......@@ -269,4 +267,5 @@ fileprivate struct Triangle: Shape{
#Preview {
LoginView(isLogin: .constant(false))
.environmentObject(SelectedTabModel())
.environmentObject(ChatViewModel())
}
......@@ -63,7 +63,7 @@ struct MapRepresentable: UIViewControllerRepresentable {
//MARK: - 自船を画面中央に表示
if location.focusOwnShip {
mapVC.updateCamera(location: location.location, zoomlevel: 10.0)
mapVC.updateCamera(location: location.location, zoomlevel: 5.0)
}
if let mylocation = location.location {
......@@ -86,7 +86,9 @@ struct MapRepresentable: UIViewControllerRepresentable {
if SharingData.nga.editType == EditNgaType.registered {
mapVC.updateEditArea(remove: true)
SharingData.nga.editType = EditNgaType.nonEdit
DispatchQueue.main.async {
SharingData.nga.editType = EditNgaType.nonEdit
}
}
if SharingData.nga.editType == EditNgaType.deletePoint {
......
import SwiftUI
struct EcaListMainView: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
HStack{
Button(action: {
taskViewModel.viewMode = .FuelSwitching
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.EcaList.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
EcaListView(taskViewModel: taskViewModel)
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
#Preview {
EcaListMainView(taskViewModel: TaskViewModel())
}
import SwiftUI
struct EcaMenuView: View {
@ObservedObject var taskViewModel: TaskViewModel
@ObservedObject var ecaData = SharingData.eca
@Environment(\ .colorScheme) var colorScheme
@State var isDeleteAlert: Bool = false
let deleteEcaArea = DeleteEcaArea()
var eca: RegisteredEca
var body: some View {
Menu {
Text(eca.name)
Button{
taskViewModel.edittingEcaArea = eca
taskViewModel.ecaName = eca.name
taskViewModel.viewMode = .EcaSetting
} label: {
Text("Edit Notice Setting")
}
Button{
taskViewModel.edittingEcaArea = eca
taskViewModel.ecaName = eca.name
isDeleteAlert = true
} label: {
Text("Delete ECA Task")
}
} label: {
Image(systemName: "ellipsis")
.frame(width: 22, height: 22)
.opacity(eca.isRunning == true ? 0.2 : 1.0)
.foregroundColor(colorScheme == .light ? .black : .white)
}
.disabled(eca.isRunning)
.alert("Delete", isPresented: $isDeleteAlert) {
Button("Yes") {
if let ecaArea = taskViewModel.edittingEcaArea {
var newData = ecaArea
newData.isEnable = false
newData.status = EcaState.cancel
ecaData.editEcaArea(key: ecaArea.areaId, value: newData, type: EcaOperation.Delete)
deleteEcaArea.start(ecaId: ecaArea.areaId)
}
taskViewModel.edittingEcaArea = nil
}
Button("No"){}
} message: {
Text("Have you finished " + taskViewModel.ecaName + " fuel switching?")
}
}
}
import SwiftUI
struct EcaSettingMainView: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
//タイトルエリア
HStack{
Button(action: {
taskViewModel.viewMode = .FuelSwitching
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.EcaSetting.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
if let edittingEcaArea = taskViewModel.edittingEcaArea {
EcaSettingView(taskViewModel: taskViewModel, edittingEca: edittingEcaArea)
}
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
#Preview {
EcaSettingMainView(taskViewModel: TaskViewModel())
}
import SwiftUI
struct FuelSwitchingMainView: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
HStack{
Button(action: {
taskViewModel.viewMode = .MenuList
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.FuelSwitching.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
//ECAリスト
ScrollViewReader { proxy in
ScrollView(.vertical){
FuelSwitchingView(taskViewModel: taskViewModel)
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
#Preview {
FuelSwitchingMainView(taskViewModel: TaskViewModel())
}
......@@ -4,7 +4,6 @@
//
// Created by Mamoru Sugita on 2023/10/18.
//
import SwiftUI
struct FuelSwitchingView: View {
......@@ -96,59 +95,6 @@ struct FuelSwitchingView: View {
}
}
struct EcaMenuView: View {
@ObservedObject var taskViewModel: TaskViewModel
@ObservedObject var ecaData = SharingData.eca
@Environment(\ .colorScheme) var colorScheme
@State var isDeleteAlert: Bool = false
let deleteEcaArea = DeleteEcaArea()
var eca: RegisteredEca
var body: some View {
Menu {
Text(eca.name)
Button{
taskViewModel.edittingEcaArea = eca
taskViewModel.ecaName = eca.name
taskViewModel.viewMode = .EcaSetting
} label: {
Text("Edit Notice Setting")
}
Button{
taskViewModel.edittingEcaArea = eca
taskViewModel.ecaName = eca.name
isDeleteAlert = true
} label: {
Text("Delete ECA Task")
}
} label: {
Image(systemName: "ellipsis")
.frame(width: 22, height: 22)
.opacity(eca.isRunning == true ? 0.2 : 1.0)
.foregroundColor(colorScheme == .light ? .black : .white)
}
.disabled(eca.isRunning)
.alert("Delete", isPresented: $isDeleteAlert) {
Button("Yes") {
if let ecaArea = taskViewModel.edittingEcaArea {
var newData = ecaArea
newData.isEnable = false
newData.status = EcaState.cancel
ecaData.editEcaArea(key: ecaArea.areaId, value: newData, type: EcaOperation.Delete)
deleteEcaArea.start(ecaId: ecaArea.areaId)
}
taskViewModel.edittingEcaArea = nil
}
Button("No"){}
} message: {
Text("Have you finished " + taskViewModel.ecaName + " fuel switching?")
}
}
}
#Preview {
FuelSwitchingView(taskViewModel: TaskViewModel())
}
import SwiftUI
struct MenuMainView: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
VStack(spacing: 0) {
headerView
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
VStack(spacing: 16) {
Color.clear.frame(height: 1).id("top")
MenuButton(
title: TaskViewMode.FuelSwitching.title,
isEnabled: SharingData.my.isFuelSwitchTask,
action: { taskViewModel.viewMode = .FuelSwitching }
)
MenuButton(
title: TaskViewMode.NgaNotification.title,
isEnabled: SharingData.my.isNga,
action: { taskViewModel.viewMode = .NgaNotification }
)
}
.padding(.top, 16)
}
.onChange(of: taskViewModel.viewMode) { _ in
withAnimation {
proxy.scrollTo("top", anchor: .top)
}
}
}
Spacer().frame(height: 55)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
}
private var headerView: some View {
HStack {
Spacer()
Text(TaskViewMode.MenuList.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
}
}
}
struct MenuButton: View {
let title: String
let isEnabled: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(FontStyle.DefaultText.font)
.padding()
.frame(maxWidth: .infinity, minHeight: 42)
}
.foregroundColor(ColorSet.ButtonText.color)
.background(isEnabled ? ColorSet.PrimaryActiveIcon.color : ColorSet.SecondaryDisable.color)
.cornerRadius(30)
.disabled(!isEnabled)
.frame(width: 153)
}
}
#Preview {
MenuMainView(taskViewModel: TaskViewModel())
}
import SwiftUI
struct NgaMenuView: View {
@ObservedObject var taskViewModel: TaskViewModel
@ObservedObject var ngaData = SharingData.nga
@Environment(\ .colorScheme) var colorScheme
@State var isDelete: Bool = false
var nga: RegisteredNga
var body: some View {
Menu {
Text(nga.name)
Button{
ngaData.editNga = nga
taskViewModel.viewMode = .NgaSetting
ngaData.editType = EditNgaType.nonEdit
} label: {
Text("Edit NGA Setting")
}
//削除
Button{
ngaData.editNga = nga
isDelete = true
} label: {
Text("Delete NGA Task")
}.disabled(nga.isLock)
} label: {
Image(systemName: "ellipsis")
.frame(width: 22, height: 22)
.opacity(nga.isRunning ? 0.2 : 1.0)
.foregroundColor(colorScheme == .light ? .black : .white)
}
.disabled(nga.isRunning)
.alert("Delete", isPresented: $isDelete) {
Button("Yes") {
if let newData = ngaData.editNga {
ngaData.editNgaArea(value: newData, type: NgaOperation.Delete)
}
}
Button("No"){}
} message: {
if let editNga = ngaData.editNga {
Text("Delete " + editNga.name + " ?")
}
}
}
}
import SwiftUI
struct NgaNotificationMainView: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
//タイトルエリア
HStack{
Button(action: {
taskViewModel.viewMode = .MenuList
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.NgaNotification.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
NgaNotificationView(taskViewModel: taskViewModel)
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
#Preview {
NgaNotificationMainView(taskViewModel: TaskViewModel())
}
......@@ -4,7 +4,6 @@
//
// Created by Mamoru Sugita on 2023/10/18.
//
import SwiftUI
struct NgaNotificationView: View {
......@@ -87,54 +86,6 @@ struct NgaNotificationView: View {
}
}
struct NgaMenuView: View {
@ObservedObject var taskViewModel: TaskViewModel
@ObservedObject var ngaData = SharingData.nga
@Environment(\ .colorScheme) var colorScheme
@State var isDelete: Bool = false
var nga: RegisteredNga
var body: some View {
Menu {
Text(nga.name)
Button{
ngaData.editNga = nga
taskViewModel.viewMode = .NgaSetting
ngaData.editType = EditNgaType.nonEdit
} label: {
Text("Edit NGA Setting")
}
//削除
Button{
ngaData.editNga = nga
isDelete = true
} label: {
Text("Delete NGA Task")
}.disabled(nga.isLock)
} label: {
Image(systemName: "ellipsis")
.frame(width: 22, height: 22)
.opacity(nga.isRunning ? 0.2 : 1.0)
.foregroundColor(colorScheme == .light ? .black : .white)
}
.disabled(nga.isRunning)
.alert("Delete", isPresented: $isDelete) {
Button("Yes") {
if let newData = ngaData.editNga {
ngaData.editNgaArea(value: newData, type: NgaOperation.Delete)
}
}
Button("No"){}
} message: {
if let editNga = ngaData.editNga {
Text("Delete " + editNga.name + " ?")
}
}
}
}
#Preview {
NgaNotificationView(taskViewModel: TaskViewModel())
}
import SwiftUI
struct NgaSettingMainView: View {
@ObservedObject var taskViewModel = TaskViewModel()
@ObservedObject var nga = SharingData.nga
@ObservedObject var map = SharingData.map
var body: some View {
//タイトルエリア
HStack{
Button(action: {
if taskViewModel.viewMode == .NgaSetting && nga.editType == EditNgaType.movePoint {
nga.editNga = nga.moveNga
nga.editType = EditNgaType.addPoint
map.isMapFree = true
} else {
taskViewModel.viewMode = .NgaNotification
nga.editType = EditNgaType.registered
}
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.NgaSetting.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
NgaSettingView(taskViewModel: taskViewModel)
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
#Preview {
NgaSettingMainView(taskViewModel: TaskViewModel())
}
......@@ -84,6 +84,7 @@ var connection: HubConnection?
class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, MSInstallationLifecycleDelegate {
@ObservedObject var msg = SharingData.message
private var hubConnectionDelegate: HubConnectionDelegate?
private var isReconnecting = false // 再接続中かどうかを管理するフラグ
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print(debug: "called")
......@@ -105,7 +106,7 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
application.registerForRemoteNotifications()
}
//ユニークな値の生成
//MARK: - ユニークな値の生成
if Preferences.DeviceId == "" {
Preferences.DeviceId = UUID().uuidString
}
......@@ -117,32 +118,37 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
.withAutoReconnect()
// .withLogging(minLogLevel: .error)
.withLogging(minLogLevel: .debug)
.withHubConnectionOptions(configureHubConnectionOptions: {options in options.keepAliveInterval = 30})
.withHubConnectionOptions(configureHubConnectionOptions: {options in options.keepAliveInterval = 20})
.build()
if let r_connection = connection {
//Chat
r_connection.on(method: "ChatMessage", callback: { (message: ResChatMessage) in
self.handleChatMessage(message: message)
})
setupSignalRHandlers(connection: r_connection)
r_connection.start()
}
//Photo / Image
r_connection.on(method: "chatMessage", callback: { (message: ResChatMessage) in
self.handleChatMessage(message: message)
})
return true
}
r_connection.on(method: "AckMessage", callback: { (message: ResAckMessage) in
self.handleAckMessage(message: message)
})
private func setupSignalRHandlers(connection: HubConnection) {
// Chatメッセージ処理
connection.on(method: "ChatMessage", callback: { (message: ResChatMessage) in
self.handleChatMessage(message: message)
})
r_connection.on(method: "ChatMode", callback: { (message: ResChatMode) in
self.handleChatMode(message: message)
})
// Photo / Image
connection.on(method: "chatMessage", callback: { (message: ResChatMessage) in
self.handleChatMessage(message: message)
})
r_connection.start()
}
// Ackメッセージ処理
connection.on(method: "AckMessage", callback: { (message: ResAckMessage) in
self.handleAckMessage(message: message)
})
return true
// Chatモード処理
connection.on(method: "ChatMode", callback: { (message: ResChatMode) in
self.handleChatMode(message: message)
})
}
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
......@@ -162,7 +168,7 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
completionHandler(.noData)
}
// Push通知を受信した時(サイレントプッシュ)
//MARK: - Push通知を受信した時(サイレントプッシュ)
func notificationHub(_ notificationHub: MSNotificationHub, didReceivePushNotification notification: MSNotificationHubMessage) {
print(debug: "called")
// let title = notification.title ?? ""
......@@ -188,9 +194,36 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
}
private func handleChatMessage(message: ResChatMessage) {
print(debug: "called")
let ownMsg = ChatMessage(shipId: message.shipId, messageId: message.messageId, type: message.type, time: message.time, location: message.location, from: message.from, fromId: message.fromId, mode: message.mode, message: message.message, stampId: message.stampId, viewer: [])
self.msg.messages.append(ownMsg)
DispatchQueue.main.async {
print(debug: "called")
let newMsg = ChatMessage(
shipId: message.shipId,
messageId: message.messageId,
type: message.type,
time: message.time,
location: message.location,
from: message.from,
fromId: message.fromId,
mode: message.mode,
message: message.message,
stampId: message.stampId,
viewer: []
)
if let index = self.msg.messages.lastIndex(where: { $0.messageId.lowercased() == newMsg.messageId.lowercased() }) {
var updatedMsg = newMsg
let existingMsg = self.msg.messages[index]
if existingMsg.message!.hasPrefix("file://") {
updatedMsg.message = existingMsg.message
}
self.msg.messages[index] = updatedMsg
} else {
self.msg.messages.append(newMsg)
}
}
}
private func handleAckMessage(message: ResAckMessage) {
......@@ -219,7 +252,20 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
self.msg.messages.append(ownMsg)
}
//アプリ終了時
//MARK: - Signal-R再接続
func reconnect() {
guard !isReconnecting else { return } // 再接続中なら何もしない
isReconnecting = true
print(debug: "Attempting to reconnect...")
DispatchQueue.global().asyncAfter(deadline: .now() + 5.0) { [weak self] in
guard let self = self else { return }
connection?.start()
self.isReconnecting = false
}
}
//MARK: - アプリ終了時
func applicationWillTerminate(_ aNotification: UIApplication) {
if let r_connection = connection {
r_connection.stop()
......@@ -232,12 +278,14 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
func connectionDidFailToOpen(error: Error) {
print(debug: "connectionDidFailToOpen:\(error)")
reconnect()
}
func connectionDidClose(error: Error?) {
if let err = error {
print(debug: "connectionDidClose:\(err)")
}
reconnect()
}
func connectionWillReconnect(error: Error) {
......@@ -253,17 +301,15 @@ class SignalR: NSObject {
@ObservedObject var msg = SharingData.message
func chatMessage(message: String, completion: @escaping (_ error: Error?) -> Void) {
print(debug: "called")
var request = ReqMessage(shipId: Preferences.shipId, messageId: UUID().uuidString.lowercased())
request.type = 0 //0:テキスト, 1:スタンプ
request.time = DateTextLib.Date2ISO8601Text(Date())
request.location = 2 //1:Shore , 2:Ship
request.from = Preferences.UserName //投稿者名
request.fromId = String(SharingData.my.id) //ユーザーID
if SharingData.message.mode {
request.mode = 1
} else {
request.mode = 0
}
request.mode = SharingData.message.mode ? 1 : 0
request.message = message
request.stampId = 0 //スタンプ番号 0:Fire~
......@@ -299,11 +345,15 @@ class SignalR: NSObject {
let test = connection.debugDescription
print(debug: "Test: Chat Message \(test)")
connection!.stop()
guard let connection = connection else { return }
connection.stop()
}
func startConnection() {
connection!.start()
guard let connection = connection else { return }
if connection.connectionId == nil {
connection.start()
}
}
}
......@@ -355,6 +405,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
let arrCategory = arrAPS["category"] as? String ?? ""
switch arrCategory {
case "chat":
print(debug: "chat")
guard let arrAlert = arrAPS["alert"] as? [String: Any] else {
completionHandler([[.banner, .badge, .sound]])
return
......
......@@ -20,29 +20,25 @@ class SessionUploadImage : ObservableObject {
func cancelUpload() {
uploadTask?.cancel()
uploadTask = nil
isUploading = false
progress = 0.0
}
/**
* メッセージ
*/
//MARK: - 画像のアップロード
func requestUploadImage(_ uploadImage: ReqUploadImage) async throws -> Data {
print(debug: "calld")
print(debug: "called")
guard !Calling else {
throw APIError.busy
}
Calling = true
defer { Calling = false }
// リクエストURLの組み立て
guard let req_url = URL(string: HttpRequestType.UploadImage.rawValue) else {
throw APIError.invalidURL
}
let imageFileName = "itemp.jpg"
let boundary = "----------\(UUID().uuidString)"
var httpBody = Data()
func appendFormField(name: String, value: String) {
httpBody.append("--\(boundary)\r\n".data(using: .utf8)!)
......@@ -57,7 +53,7 @@ class SessionUploadImage : ObservableObject {
appendFormField(name: "FromId", value: uploadImage.fromId)
httpBody.append("--\(boundary)\r\n".data(using: .utf8)!)
httpBody.append("Content-Disposition: form-data; name=\"files\"; filename=\"\(imageFileName)\"\r\n".data(using: .utf8)!)
httpBody.append("Content-Disposition: form-data; name=\"files\"; filename=\"itemp.jpg\"\r\n".data(using: .utf8)!)
httpBody.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
httpBody.append(uploadImage.files)
httpBody.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
......@@ -65,22 +61,35 @@ class SessionUploadImage : ObservableObject {
var request = URLRequest(url: req_url)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
let session = URLSession(configuration: .default, delegate: UploadProgressDelegate(parent: self), delegateQueue: nil)
isUploading = true
uploadTask = session.uploadTask(with: request, from: httpBody)
uploadTask?.resume()
let (data, response) = try await session.upload(for: request, from: httpBody)
isUploading = false
progress = 0.0
return try await withCheckedThrowingContinuation { continuation in
self.uploadTask = session.uploadTask(with: request, from: httpBody) { data, response, error in
DispatchQueue.main.async {
self.isUploading = false
self.progress = 0.0
}
if let error = error {
continuation.resume(throwing: error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode),
let data = data else {
continuation.resume(throwing: APIError.serverError)
return
}
continuation.resume(returning: data)
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
throw APIError.serverError
self.uploadTask?.resume()
}
return data
}
func postFormAsync(boundary: String, url: URL, body: Data) async throws -> Data {
......@@ -105,8 +114,11 @@ class SessionUploadImage : ObservableObject {
self.parent = parent
}
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64,
totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
func urlSession(_ session: URLSession, task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64) {
guard totalBytesExpectedToSend > 0 else { return }
DispatchQueue.main.async {
self.parent?.progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
}
......
import SwiftUI
struct CustomTabBar: View {
@EnvironmentObject private var selectedTabModel: SelectedTabModel
@StateObject private var viewModel = CustomTabBarViewModel()
@Environment(\.openURL) var openURL
@ObservedObject var my = SharingData.my
@ObservedObject var message = SharingData.message
@ObservedObject var location = SharingData.location
@ObservedObject var pushHistory = SharingData.pushHistory
var body: some View {
VStack(spacing: 0){
Divider()
HStack(spacing: 0){
ForEach(Tab.allCases, id: \.rawValue) { tab in
VStack{
ZStack(alignment: .bottomTrailing) {
if !SharingData.my.isCommunication && tab == Tab.chat {
Image("tab_chat_Invalid")
.onTapGesture {
viewModel.handleTabSelection(
selectedTabModel: selectedTabModel,
tab: tab,
message: message,
history: pushHistory,
location: location,
)
}
} else {
Image(selectedTabModel.activeTab == tab ? tab.rawValue + "_selected" : tab.rawValue)
.onTapGesture {
viewModel.handleTabSelection(
selectedTabModel: selectedTabModel,
tab: tab,
message: message,
history: pushHistory,
location: location,
)
}
}
if tab == Tab.chat {
NotificationBadge(viewModel: viewModel.chatBadgeViewModel, font: FontStyle.VersionText.font)
}
if tab == Tab.alert {
NotificationBadge(viewModel: viewModel.alertBadgeViewModel, font: FontStyle.VersionText.font)
}
}
Text(tab.title)
.font(.caption)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
}
}
.frame(height: 50)
}
.background(ColorSet.BottomNav.color)
.modifier(ChangeModeAlertModifier(
isPresented: $selectedTabModel.isShowChangeEmrMode,
currentMode: message.mode,
onConfirm: {
var chatMode = message.mode
chatMode.toggle()
let signalRService = SignalR()
signalRService.chatMode(mode: chatMode, completion: responseChatMode)
message.sendInf = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
restartChatMode()
}
}))
.modifier(LocationAlertModifier(isPresented: $selectedTabModel.isLocationAlert))
.modifier(UpdateAlertModifier(isPresented: $my.isUpDate, appStoreURL: URL(string: HttpRequestType.AppStore.rawValue)!))
.modifier(WarningModeAlertModifier(isPresented: $viewModel.isModeAlert, isSignalrRestert: $viewModel.isSignalrRestart,
onConfirm: {
let signalRService = SignalR()
signalRService.startConnection()
viewModel.isSignalrRestart = false
}))
//MARK: - チャットTab上の既読マーク
.onChange(of: message.viewCnt) { newValue in
viewModel.chatBadgeViewModel.updateViewCnt(to: newValue)
}
//MARK: - アラートTab上の既読マーク
.onChange(of: pushHistory.viewCnt) { newValue in
viewModel.alertBadgeViewModel.updateViewCnt(to: newValue)
}
}
//MARK: - Warninngモードレスポンス
func responseChatMode(error: Error?) {
print(debug: "responseChatMode")
message.sendInf = false
if let e = error {
print(debug: "Error chat:\(e)")
viewModel.isModeAlert = true
} else {
message.changeMode()
}
}
//MARK: - モード変更レスポンスが無かった場合
func restartChatMode() {
if message.sendInf {
let signalRService = SignalR()
signalRService.stopConnection()
Thread.sleep(forTimeInterval: 1.0)
viewModel.isSignalrRestart = true
}
}
}
import SwiftUI
import Foundation
import UIKit
import Combine
class CustomTabBarViewModel: ObservableObject {
@Published var chatBadgeViewModel = NotificationBadgeViewModel()
@Published var alertBadgeViewModel = NotificationBadgeViewModel()
@Published var isMenuTaskViewVisible = false
@Published var isSignalrRestart = false
@Published var isModeAlert = false
private let messageService = GetMessage()
func handleTabSelection(
selectedTabModel: SelectedTabModel,
tab: Tab,
message: SharingData.Message,
history: SharingData.PushHistory,
location: SharingData.Location
) {
selectedTabModel.activeTab = tab
switch tab {
case .map:
selectedTabModel.isPoppver.toggle()
location.focusOwnShip = true
if UIDevice.current.userInterfaceIdiom == .pad {
isMenuTaskViewVisible.toggle()
updateMapMenuVisibility()
}
case .chat:
messageService.start()
messageService.readNotification()
messageService.checkUnreadMessages()
message.viewCnt = 0
chatBadgeViewModel.resetBadge()
resetPopoverAndMenu(selectedTabModel: selectedTabModel)
case .alert:
history.viewCnt = 0
alertBadgeViewModel.resetBadge()
resetPopoverAndMenu(selectedTabModel: selectedTabModel)
default:
resetPopoverAndMenu(selectedTabModel: selectedTabModel)
}
}
private func resetPopoverAndMenu(selectedTabModel: SelectedTabModel) {
selectedTabModel.isPoppver = false
if UIDevice.current.userInterfaceIdiom == .pad {
isMenuTaskViewVisible = false
updateMapMenuVisibility()
}
}
//MARK: - マップメニュー表示の切り替え
private func updateMapMenuVisibility() {
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
let menuWidth = min(400, screenWidth * 0.3)
let menuHeight = screenHeight * 0.8
if isMenuTaskViewVisible {
MenuWindowManager.shared.showMenuView(
MenuTaskView()
.frame(width: menuWidth, height: menuHeight)
.background(ColorSet.BackgroundSecondary.color)
.cornerRadius(15),
frame: CGRect(x: 5, y: screenHeight * 0.12, width: menuWidth, height: menuHeight)
)
} else {
MenuWindowManager.shared.hideMenuView()
}
}
}
class MenuWindowManager {
static let shared = MenuWindowManager()
private var window: UIWindow?
func showMenuView<Content: View>(_ content: Content, frame: CGRect) {
guard window == nil else { return }
let hostingController = UIHostingController(rootView: content)
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
var rFrame = frame
rFrame.size.height -= 20
let newWindow = UIWindow(windowScene: windowScene)
newWindow.rootViewController = hostingController
newWindow.windowLevel = .alert + 1
newWindow.backgroundColor = UIColor.clear
newWindow.makeKeyAndVisible()
newWindow.alpha = 0.8
newWindow.frame = rFrame
newWindow.layer.cornerRadius = 15
newWindow.layer.masksToBounds = true
self.window = newWindow
}
}
func hideMenuView() {
guard let window = window else { return }
window.isHidden = true
window.rootViewController = nil
self.window = nil
}
}
This diff is collapsed.
import SwiftUI
class NotificationBadgeViewModel: ObservableObject {
@Published private(set) var viewCnt: Int = 0
@Published private(set) var hasUnread: Bool = false
init(viewCnt: Int = 0) {
self.viewCnt = viewCnt
}
//MARK: - 未読カウントを更新
func updateViewCnt(to newValue: Int) {
viewCnt = newValue
hasUnread = newValue > 0
}
//MARK: - バッジをリセット(既読状態にする)
func resetBadge() {
viewCnt = 0
hasUnread = false
}
//MARK: - 現在の未読数を取得
func getUnreadCount() -> Int {
return viewCnt
}
}
struct NotificationBadge: View {
@ObservedObject var viewModel: NotificationBadgeViewModel
let font: Font
var body: some View {
Group {
if viewModel.getUnreadCount() > 0 {
Ellipse()
.fill(Color.red)
.frame(width: 12, height: 12)
.overlay(
Text(viewModel.getUnreadCount() < 10 ? "\(viewModel.getUnreadCount())" : "-")
.font(font)
.foregroundColor(.white)
)
}
}
}
}
//
// MainTabView.swift
// forShip
//
// Created by Mamoru Sugita on 2023/10/13.
//
import SwiftUI
enum Tab: String, CaseIterable{
case map = "tab_map"
case chat = "tab_chat"
case alert = "tab_notification"
case menu = "tab_menu"
var title: String{
switch self {
case .map: return "Map"
case .chat: return "Chat"
case .alert: return "Alert"
case .menu: return "Menu"
}
}
}
struct MainTabView: View {
@EnvironmentObject var selectedTabModel: SelectedTabModel
@EnvironmentObject private var sceneDelegate: SceneDelegate
@ObservedObject var location = SharingData.location
@State private var offset = CGSize.zero
@State var isSignout = false
var isTabWindowActive: Bool {
sceneDelegate.tabWindow != nil
}
var isTaskSel: Bool {
selectedTabModel.activeTab == .map
}
var body: some View {
Group {
if UIDevice.current.userInterfaceIdiom == .phone {
phoneView
} else {
padView
}
}
.onAppear {
SharingData.location.focusOwnShip = true
configureTabBarAppearance()
}
}
private var phoneView: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selectedTabModel.activeTab) {
mapTab
if SharingData.my.isCommunication {
ChatView().tag(Tab.chat)
}
NotificationView().tag(Tab.alert)
MenuView(isSignout: $isSignout).tag(Tab.menu)
}
.sheet(isPresented: .constant(isTaskSel && isTabWindowActive)) {
MenuTaskView()
.zIndex(0)
.presentationDragIndicator(.hidden)
.presentationDetents([.height(150), .medium, .fraction(0.99)])
.presentationCornerRadius(15)
.presentationBackgroundInteraction(.enabled(upThrough: .medium))
.presentationBackground(ColorSet.BackgroundSecondary.color)
.interactiveDismissDisabled()
}
}
}
private var padView: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selectedTabModel.activeTab) {
GeometryReader { geometry in
mapTab.padding(.bottom, geometry.safeAreaInsets.bottom + 10)
}
if SharingData.my.isCommunication {
ChatView().padding(.bottom, 50).tag(Tab.chat)
}
NotificationView().padding(.bottom, 50).tag(Tab.alert)
MenuView(isSignout: $isSignout).padding(.bottom, 50).tag(Tab.menu)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
@ViewBuilder
private var mapTab: some View {
ZStack {
MapRepresentable()
MapInformation().zIndex(1)
}
.ignoresSafeArea(edges: .top)
.tag(Tab.map)
}
private func configureTabBarAppearance() {
let appearance = UITabBarAppearance()
appearance.backgroundColor = .clear
UITabBar.appearance().scrollEdgeAppearance = appearance
UITabBar.appearance().standardAppearance = appearance
}
}
#Preview {
MainTabView()
.environmentObject(SelectedTabModel())
.environmentObject(SceneDelegate())
}
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