스플래시 페이지 추가

This commit is contained in:
Yu Sung
2023-08-09 16:52:36 +09:00
parent 058c907609
commit 84d3dd61ca
27 changed files with 932 additions and 6 deletions

View File

@@ -0,0 +1,110 @@
//
// AppDelegate.swift
// SodaLive
//
// Created by klaus on 2023/08/09.
//
import UIKit
import FirebaseCore
import FirebaseMessaging
class AppDelegate: UIResponder, UIApplicationDelegate {
private let gcmMessageIDKey = "gcm.message_id"
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
Messaging.messaging().delegate = self
// For iOS 10 display notification (sent via APNS)
UNUserNotificationCenter.current().delegate = self
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions,
completionHandler: {_, _ in })
application.registerForRemoteNotifications()
UIApplication.shared.applicationIconBadgeNumber = 0
return true
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
DEBUG_LOG("Message ID: \(messageID)")
}
// Print full message.
DEBUG_LOG("userInfo: \(userInfo)")
completionHandler(UIBackgroundFetchResult.newData)
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
UserDefaults.set(deviceToken, forKey: .devicePushToken)
Messaging.messaging().apnsToken = deviceToken
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
}
}
extension AppDelegate: MessagingDelegate {
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
if let fcmToken = fcmToken {
DEBUG_LOG("fcmToken: \(fcmToken)")
UserDefaults.set(fcmToken, forKey: .pushToken)
}
}
}
extension AppDelegate : UNUserNotificationCenterDelegate {
// Receive displayed notifications for iOS 10 devices.
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
let userInfo = notification.request.content.userInfo
// With swizzling disabled you must let Messaging know about the message, for Analytics
Messaging.messaging().appDidReceiveMessage(userInfo)
// ...
// Print full message.
DEBUG_LOG("userInfo: \(userInfo)")
// Change this to your preferred presentation option
completionHandler([.banner, .badge, .sound])
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
// With swizzling disabled you must let Messaging know about the message, for Analytics
Messaging.messaging().appDidReceiveMessage(userInfo)
let roomIdString = userInfo["suda_room_id"] as? String
let audioContentIdString = userInfo["audio_content_id"] as? String
if let roomIdString = roomIdString, let roomId = Int(roomIdString), roomId > 0 {
AppState.shared.pushRoomId = roomId
}
if let audioContentIdString = audioContentIdString, let audioContentId = Int(audioContentIdString), audioContentId > 0 {
AppState.shared.pushAudioContentId = audioContentId
}
completionHandler()
}
}

View File

@@ -0,0 +1,58 @@
//
// AppState.swift
// SodaLive
//
// Created by klaus on 2023/08/09.
//
import Foundation
class AppState: ObservableObject {
static let shared = AppState()
private var appStepBackStack = [AppStep]()
@Published private(set) var appStep: AppStep = .splash
@Published var isShowPlayer = false {
didSet {
if isShowPlayer {
}
}
}
@Published var isShowNotificationSettingsDialog = false
@Published var pushRoomId = 0
@Published var pushChannelId = 0
@Published var pushAudioContentId = 0
@Published var roomId = 0 {
didSet {
if roomId <= 0 {
isShowPlayer = false
}
}
}
func setAppStep(step: AppStep) {
switch step {
case .splash, .main:
appStepBackStack.removeAll()
default:
appStepBackStack.append(appStep)
}
DispatchQueue.main.async {
self.appStep = step
}
}
func back() {
if let step = appStepBackStack.popLast() {
self.appStep = step
} else {
self.appStep = .main
}
}
}

View File

@@ -0,0 +1,14 @@
//
// AppStep.swift
// SodaLive
//
// Created by klaus on 2023/08/09.
//
import Foundation
enum AppStep {
case splash
case main
}

View File

@@ -6,12 +6,81 @@
//
import SwiftUI
import AppTrackingTransparency
import FirebaseDynamicLinks
@main
struct SodaLiveApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
func handleIncomingDynamicLink(_ dynamicLink: DynamicLink) {
guard let url = dynamicLink.url else {
DEBUG_LOG("That's weired. My dynamic link object has no url")
return
}
DEBUG_LOG("incoming link parameter is \(url.absoluteString)")
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems
let roomId = queryItems?.filter({$0.name == "room_id"}).first?.value
let channelId = queryItems?.filter({$0.name == "channel_id"}).first?.value
let audioContentId = queryItems?.filter({$0.name == "audio_content_id"}).first?.value
if let roomId = roomId {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
AppState.shared.pushRoomId = Int(roomId) ?? 0
}
}
if let channelId = channelId {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
AppState.shared.pushChannelId = Int(channelId) ?? 0
}
}
if let audioContentId = audioContentId {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
AppState.shared.pushAudioContentId = Int(audioContentId) ?? 0
}
}
}
var body: some Scene {
WindowGroup {
ContentView()
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
UIApplication.shared.applicationIconBadgeNumber = 0
ATTrackingManager.requestTrackingAuthorization { _ in }
}
.onOpenURL { url in
DEBUG_LOG("I have received a URL through a custom scheme! \(url.absoluteString)")
if let scheme = url.scheme {
if scheme == "kr.co.vividnext.yozm" {
if let dynamicLink = DynamicLinks.dynamicLinks().dynamicLink(fromCustomSchemeURL: url) {
self.handleIncomingDynamicLink(dynamicLink)
} else {
DEBUG_LOG("dynamic link fail")
}
} else {
DynamicLinks.dynamicLinks().handleUniversalLink(url) { dynamicLink, error in
guard error == nil else {
DEBUG_LOG("Found an error! \(error!.localizedDescription)")
DEBUG_LOG("dynamic link fail")
return
}
if let dynamicLink = dynamicLink {
self.handleIncomingDynamicLink(dynamicLink)
} else {
DEBUG_LOG("dynamic link fail")
}
}
}
}
}
}
}
}

View File

@@ -8,14 +8,21 @@
import SwiftUI
struct ContentView: View {
@StateObject private var appState = AppState.shared
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
ZStack {
Color.black.ignoresSafeArea()
switch appState.appStep {
case .splash:
SplashView()
default:
EmptyView()
.frame(width: 0, height: 0, alignment: .topLeading)
}
}
.padding()
}
}

View File

@@ -0,0 +1,16 @@
//
// Constants.swift
// SodaLive-dev
//
// Created by klaus on 2023/08/09.
//
import Foundation
let BASE_URL = "https://test-api.sodalive.net"
let APPLY_YOZM_CREATOR = "https://forms.gle/mmhJKmijjVRnwtdZ7"
let AGORA_APP_ID = "b96574e191a9430fa54c605528aa3ef7"
let AGORA_APP_CERTIFICATE = "ae18ade3afcf4086bd4397726eb0654c"
let BOOTPAY_APP_ID = "6242a7772701800023f68b2f"

View File

@@ -0,0 +1,40 @@
//
// ColorExtension.swift
// SodaLive
//
// Created by klaus on 2023/08/09.
//
// https://seons-dev.tistory.com/174
//
import SwiftUI
extension Color {
init(hex: String) {
let scanner = Scanner(string: hex)
_ = scanner.scanString("#")
var rgb: UInt64 = 0
scanner.scanHexInt64(&rgb)
let r = Double((rgb >> 16) & 0xFF) / 255.0
let g = Double((rgb >> 8) & 0xFF) / 255.0
let b = Double((rgb >> 0) & 0xFF) / 255.0
self.init(red: r, green: g, blue: b)
}
}
extension UIColor {
convenience init(hex: String, alpha: CGFloat = 1.0) {
let scanner = Scanner(string: hex)
_ = scanner.scanString("#")
var rgb: UInt64 = 0
scanner.scanHexInt64(&rgb)
let r = Double((rgb >> 16) & 0xFF) / 255.0
let g = Double((rgb >> 8) & 0xFF) / 255.0
let b = Double((rgb >> 0) & 0xFF) / 255.0
self.init(red: r, green: g, blue: b, alpha: alpha)
}
}

View File

@@ -0,0 +1,21 @@
//
// DateExtension.swift
// yozm
//
// Created by klaus on 2022/05/27.
//
import Foundation
extension Date {
func convertDateFormat(dateFormat: String = "yyyy.MM.dd") -> String {
let formatter = DateFormatter()
formatter.dateFormat = dateFormat
formatter.locale = Locale(identifier: "ko")
return formatter.string(from: self)
}
func currentTimeMillis() -> Int64 {
return Int64(self.timeIntervalSince1970 * 1000)
}
}

View File

@@ -0,0 +1,32 @@
//
// IntExtension.swift
// yozm
//
// Created by klaus on 2022/06/21.
//
import Foundation
extension Int64 {
func durationText() -> String {
let duration = self
let convertedTime = Int(duration / 1000)
let hour = Int(convertedTime / 3600)
let minute = Int(convertedTime / 60) % 60
let second = Int(convertedTime % 60)
// update UI
var timeText = [String]()
if hour > 0 {
timeText.append(String(hour))
timeText.append(String(format: "%02d", minute))
} else {
timeText.append(String(format: "%02d", minute))
timeText.append(String(format: "%02d", second))
}
return timeText.joined(separator: ":")
}
}

View File

@@ -0,0 +1,43 @@
//
// StringExtension.swift
// yozm
//
// Created by klaus on 2022/06/03.
//
import Foundation
extension Optional where Wrapped == String {
func isNullOrBlank() -> Bool {
return self == nil || self!.trimmingCharacters(in: .whitespaces).isEmpty
}
}
extension String {
func convertDateFormat(from: String, to: String) -> String {
let fromFormatter = DateFormatter()
fromFormatter.dateFormat = from
fromFormatter.timeZone = TimeZone(identifier: TimeZone.current.identifier)
if let date = fromFormatter.date(from: self) {
return date.convertDateFormat(dateFormat: to)
} else {
return self
}
}
func substring(from: Int, to: Int) -> String {
guard from < count, to >= 0, to - from >= 0 else {
return ""
}
// Index
let startIndex = index(self.startIndex, offsetBy: from)
let endIndex = index(self.startIndex, offsetBy: to + 1) // '+1' : endIndex
//
return String(self[startIndex ..< endIndex])
// - https://ios-development.tistory.com/379
}
}

View File

@@ -0,0 +1,72 @@
//
// UserDefaultsExtension.swift
// yozm
//
// Created by klaus on 2022/05/20.
//
import Foundation
enum UserDefaultsKey: String, CaseIterable {
case auth
case role
case coin
case token
case email
case userId
case nickname
case pushToken
case profileImage
case voipPushToken
case devicePushToken
case isContentPlayLoop
case isFollowedCreatorLive
case isViewedOnboardingView
case notShowingEventPopupId
}
extension UserDefaults {
static func set(_ value: Bool, forKey key: UserDefaultsKey) {
let key = key.rawValue
UserDefaults.standard.set(value, forKey: key)
}
static func bool(forKey key: UserDefaultsKey) -> Bool {
let key = key.rawValue
return UserDefaults.standard.bool(forKey: key)
}
static func set(_ value: String, forKey key: UserDefaultsKey) {
let key = key.rawValue
UserDefaults.standard.set(value, forKey: key)
}
static func string(forKey key: UserDefaultsKey) -> String {
let key = key.rawValue
return UserDefaults.standard.string(forKey: key) ?? ""
}
static func set(_ value: Int, forKey key: UserDefaultsKey) {
let key = key.rawValue
UserDefaults.standard.set(value, forKey: key)
}
static func int(forKey key: UserDefaultsKey) -> Int {
let key = key.rawValue
return UserDefaults.standard.integer(forKey: key)
}
static func set(_ value: Data, forKey key: UserDefaultsKey) {
let key = key.rawValue
UserDefaults.standard.set(value, forKey: key)
}
static func data(forKey key: UserDefaultsKey) -> Data? {
let key = key.rawValue
return UserDefaults.standard.data(forKey: key)
}
static func reset() {
UserDefaultsKey.allCases.forEach { UserDefaults.standard.removeObject(forKey: $0.rawValue) }
}
}

View File

@@ -0,0 +1,49 @@
//
// ViewExtension.swift
// yozm
//
// Created by klaus on 2022/05/19.
//
import SwiftUI
import Combine
extension View {
func screenSize() -> CGRect {
return UIScreen.main.bounds
}
// - https://stackoverflow.com/a/58606176
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape( RoundedCorner(radius: radius, corners: corners) )
}
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
/// A backwards compatible wrapper for iOS 14 `onChange`
@ViewBuilder func valueChanged<T: Equatable>(value: T, onChange: @escaping (T) -> Void) -> some View {
if #available(iOS 14.0, *) {
self.onChange(of: value, perform: onChange)
} else {
self.onReceive(Just(value)) { (value) in
onChange(value)
}
}
}
@ViewBuilder func scrollContentBackgroundHidden() -> some View {
if #available(iOS 16.0, *) {
self.scrollContentBackground(.hidden)
}
}
@ViewBuilder func progressColor(color: Color) -> some View {
if #available(iOS 16.0, *) {
self.tint(color)
} else {
self.accentColor(color)
}
}
}

View File

@@ -0,0 +1,20 @@
//
// RoundedCorner.swift
// SodaLive
//
// Created by klaus on 2023/08/09.
//
import SwiftUI
// - https://stackoverflow.com/a/58606176
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}

View File

@@ -0,0 +1,106 @@
//
// SplashView.swift
// SodaLive
//
// Created by klaus on 2023/08/09.
//
import SwiftUI
import FirebaseRemoteConfig
struct SplashView: View {
@State private var isShowForcedUpdatePopup = false
@State private var isShowUpdatePopup = false
var body: some View {
ZStack(alignment: .top) {
LinearGradient(
gradient: Gradient(colors: [Color(hex: "a0e2ff"), Color(hex: "ecfaff")]),
startPoint: .bottom,
endPoint: .top
).ignoresSafeArea()
Image("splash_bubble")
.padding(.top, 11)
VStack(spacing: 0) {
Image("splash_text")
.padding(.top, 111)
Image("splash_logo")
.padding(.top, 77.3)
Spacer()
Image("vividnext_logo")
.padding(.bottom, 36)
}
}
.onAppear {
fetchLastestVersion()
}
}
private func fetchLastestVersion() {
let remoteConfig = RemoteConfig.remoteConfig()
let configSettings = RemoteConfigSettings()
configSettings.minimumFetchInterval = 300
remoteConfig.configSettings = configSettings
remoteConfig.fetch { status, error in
if status == .success {
remoteConfig.activate { changed, error in
checkAppVersion(latestVersion: remoteConfig["ios_latest_version"].stringValue)
}
} else {
withAnimation {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
AppState.shared.setAppStep(step: .main)
}
}
}
}
}
private func checkAppVersion(latestVersion: String?) {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
if let latestVersion = latestVersion, let version = version {
let latestVersions = latestVersion.split(separator: ".")
let versions = version.split(separator: ".")
let latestMajor = Int(latestVersions[0])!
let latestMinor = Int(latestVersions[1])!
let latestPatch = Int(latestVersions[2])!
let major = Int(versions[0])!
let minor = Int(versions[1])!
let patch = Int(versions[2])!
if latestMajor > major || (latestMajor == major && latestMinor > minor) {
self.isShowForcedUpdatePopup = true
} else if latestMajor == major && latestMinor == minor && latestPatch > patch {
self.isShowUpdatePopup = true
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
withAnimation {
AppState.shared.setAppStep(step: .main)
}
}
}
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
withAnimation {
AppState.shared.setAppStep(step: .main)
}
}
}
}
}
struct SplashView_Previews: PreviewProvider {
static var previews: some View {
SplashView()
}
}

View File

@@ -0,0 +1,16 @@
//
// Constants.swift
// SodaLive
//
// Created by klaus on 2023/08/09.
//
import Foundation
let BASE_URL = "https://api.sodalive.net"
let APPLY_YOZM_CREATOR = "https://forms.gle/mmhJKmijjVRnwtdZ7"
let AGORA_APP_ID = "e34e40046e9847baba3adfe2b8ffb4f6"
let AGORA_APP_CERTIFICATE = "15cadeea4ba94ff7b091c9a10f4bf4a6"
let BOOTPAY_APP_ID = "64c35be1d25985001dc50c88"

View File

@@ -0,0 +1,23 @@
//
// Log.swift
// yozm
//
// Created by klaus on 2022/05/20.
//
import Foundation
func DEBUG_LOG(_ msg: String, file: String = #file, function: String = #function, line: Int = #line) {
#if DEBUG
let filename = file.split(separator: "/").last ?? ""
let funcName = function.split(separator: "(").first ?? ""
print("👻 [\(filename)] \(funcName)(\(line)): \(msg)")
#endif
}
func ERROR_LOG(_ msg: String, file: String = #file, function: String = #function, line: Int = #line) {
let filename = file.split(separator: "/").last ?? ""
let funcName = function.split(separator: "(").first ?? ""
print("🤯😡 [\(filename)] \(funcName)(\(line)): \(msg)")
}