diff --git a/Runnect-iOS/Podfile b/Runnect-iOS/Podfile index f82d3dbf..dbe1d303 100644 --- a/Runnect-iOS/Podfile +++ b/Runnect-iOS/Podfile @@ -9,6 +9,7 @@ target 'Runnect-iOS' do pod 'Kingfisher', '~> 7.0' pod 'SnapKit', '~> 5.6.0' pod 'Moya', '~> 15.0' + pod 'Then' # Pods for Runnect-iOS diff --git a/Runnect-iOS/Podfile.lock b/Runnect-iOS/Podfile.lock index f73983c6..9bff01ba 100644 --- a/Runnect-iOS/Podfile.lock +++ b/Runnect-iOS/Podfile.lock @@ -9,12 +9,14 @@ PODS: - NMapsMap (3.16.1): - NMapsGeometry - SnapKit (5.6.0) + - Then (3.0.0) DEPENDENCIES: - Kingfisher (~> 7.0) - Moya (~> 15.0) - NMapsMap - SnapKit (~> 5.6.0) + - Then SPEC REPOS: trunk: @@ -24,6 +26,7 @@ SPEC REPOS: - NMapsGeometry - NMapsMap - SnapKit + - Then SPEC CHECKSUMS: Alamofire: 4e95d97098eacb88856099c4fc79b526a299e48c @@ -32,7 +35,8 @@ SPEC CHECKSUMS: NMapsGeometry: 53c573ead66466681cf123f99f698dc8071a4b83 NMapsMap: 926c3a303d381a24bec8da3cd6e198f50af93ae9 SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25 + Then: 844265ae87834bbe1147d91d5d41a404da2ec27d -PODFILE CHECKSUM: f23513cb80e72754bbf29355e1160abe4b35c783 +PODFILE CHECKSUM: bec9bfadf42d34524a80ccc0e46829d1fe943ddb COCOAPODS: 1.11.3 diff --git a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj index dd2eb573..301c54fc 100644 --- a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj +++ b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj @@ -13,6 +13,10 @@ CE4545CD295D7AF4003201E1 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4545CC295D7AF4003201E1 /* ViewController.swift */; }; CE4545D2295D7AF5003201E1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE4545D1295D7AF5003201E1 /* Assets.xcassets */; }; CE4545D5295D7AF5003201E1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE4545D3295D7AF5003201E1 /* LaunchScreen.storyboard */; }; + CE58759E29601476005D967E /* LoadingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE58759D29601476005D967E /* LoadingIndicator.swift */; }; + CE5875A029601500005D967E /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE58759F29601500005D967E /* Toast.swift */; }; + CE5875A2296015A2005D967E /* NetworkLoggerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5875A1296015A2005D967E /* NetworkLoggerPlugin.swift */; }; + CE5875A4296015D2005D967E /* Encodable+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5875A3296015D2005D967E /* Encodable+.swift */; }; CE6655BF295D82E200C64E12 /* .gitkeep in Resources */ = {isa = PBXBuildFile; fileRef = CE6655BE295D82E200C64E12 /* .gitkeep */; }; CE6655C8295D849F00C64E12 /* StringLiterals.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6655C7295D849F00C64E12 /* StringLiterals.swift */; }; CE6655CA295D84DD00C64E12 /* UserDefaultKeyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6655C9295D84DD00C64E12 /* UserDefaultKeyList.swift */; }; @@ -63,6 +67,10 @@ CE4545D1295D7AF5003201E1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; CE4545D4295D7AF5003201E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; CE4545D6295D7AF5003201E1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CE58759D29601476005D967E /* LoadingIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingIndicator.swift; sourceTree = ""; }; + CE58759F29601500005D967E /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = ""; }; + CE5875A1296015A2005D967E /* NetworkLoggerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkLoggerPlugin.swift; sourceTree = ""; }; + CE5875A3296015D2005D967E /* Encodable+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encodable+.swift"; sourceTree = ""; }; CE6655B8295D81C900C64E12 /* .gitkeep */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitkeep; sourceTree = ""; }; CE6655BC295D82CF00C64E12 /* .gitkeep */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitkeep; sourceTree = ""; }; CE6655BD295D82D800C64E12 /* .gitkeep */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitkeep; sourceTree = ""; }; @@ -241,6 +249,7 @@ isa = PBXGroup; children = ( CE6655BD295D82D800C64E12 /* .gitkeep */, + CE5875A1296015A2005D967E /* NetworkLoggerPlugin.swift */, ); path = Foundation; sourceTree = ""; @@ -270,6 +279,8 @@ CE66560D295D92A500C64E12 /* setStatusBarBackgroundColor.swift */, CE66560F295D92C200C64E12 /* setTextLineHeight.swift */, CE665611295D92E400C64E12 /* UserDefaultWrapper.swift */, + CE58759D29601476005D967E /* LoadingIndicator.swift */, + CE58759F29601500005D967E /* Toast.swift */, ); path = Utils; sourceTree = ""; @@ -368,6 +379,7 @@ CE6655D6295D86F900C64E12 /* String+.swift */, CE6655D8295D871B00C64E12 /* URL+.swift */, CE665609295D924A00C64E12 /* Result+.swift */, + CE5875A3296015D2005D967E /* Encodable+.swift */, ); path = "Foundation+"; sourceTree = ""; @@ -539,10 +551,13 @@ CE66560A295D924A00C64E12 /* Result+.swift in Sources */, CE66560E295D92A500C64E12 /* setStatusBarBackgroundColor.swift in Sources */, CE6655D7295D86F900C64E12 /* String+.swift in Sources */, + CE58759E29601476005D967E /* LoadingIndicator.swift in Sources */, + CE5875A2296015A2005D967E /* NetworkLoggerPlugin.swift in Sources */, CE4545C9295D7AF4003201E1 /* AppDelegate.swift in Sources */, CE6655C8295D849F00C64E12 /* StringLiterals.swift in Sources */, CE6655E0295D87D200C64E12 /* UIDevice+.swift in Sources */, CE6655E8295D889600C64E12 /* UISwitch+.swift in Sources */, + CE5875A029601500005D967E /* Toast.swift in Sources */, CE6655F6295D90B600C64E12 /* addToolBar.swift in Sources */, CE6655F0295D891B00C64E12 /* UITextView+.swift in Sources */, CE6655EE295D88E600C64E12 /* UITextField+.swift in Sources */, @@ -560,6 +575,7 @@ CE665612295D92E400C64E12 /* UserDefaultWrapper.swift in Sources */, CE665610295D92C200C64E12 /* setTextLineHeight.swift in Sources */, CE6655E2295D87EB00C64E12 /* UIImage+.swift in Sources */, + CE5875A4296015D2005D967E /* Encodable+.swift in Sources */, CE6655D2295D862A00C64E12 /* Publisher+Driver.swift in Sources */, CE6655E6295D887F00C64E12 /* UIStackView+.swift in Sources */, CE6655CA295D84DD00C64E12 /* UserDefaultKeyList.swift in Sources */, diff --git a/Runnect-iOS/Runnect-iOS/Global/Extension/Foundation+/Encodable+.swift b/Runnect-iOS/Runnect-iOS/Global/Extension/Foundation+/Encodable+.swift new file mode 100644 index 00000000..bb3ebd5d --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/Extension/Foundation+/Encodable+.swift @@ -0,0 +1,23 @@ +// +// Encodable+.swift +// Runnect-iOS +// +// Created by sejin on 2022/12/31. +// + +import Foundation + +// MARK: - Encodable Extension + +extension Encodable { + + func asParameter() throws -> [String: Any] { + let data = try JSONEncoder().encode(self) + guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) + as? [String: Any] else { + throw NSError() + } + + return dictionary + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/Utils/LoadingIndicator.swift b/Runnect-iOS/Runnect-iOS/Global/Utils/LoadingIndicator.swift new file mode 100644 index 00000000..fd9a554e --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/Utils/LoadingIndicator.swift @@ -0,0 +1,36 @@ +// +// LoadingIndicator.swift +// Runnect-iOS +// +// Created by sejin on 2022/12/31. +// + +import UIKit + +class LoadingIndicator { + static func showLoading() { + DispatchQueue.main.async { + // 최상단에 있는 window 객체 획득 + guard let window = UIApplication.shared.windows.last else { return } + + let loadingIndicatorView: UIActivityIndicatorView + if let existedView = window.subviews.first(where: { $0 is UIActivityIndicatorView }) as? UIActivityIndicatorView { + loadingIndicatorView = existedView + } else { + loadingIndicatorView = UIActivityIndicatorView(style: .large) + loadingIndicatorView.frame = window.frame + loadingIndicatorView.color = .lightGray + window.addSubview(loadingIndicatorView) + } + + loadingIndicatorView.startAnimating() + } + } + + static func hideLoading() { + DispatchQueue.main.async { + guard let window = UIApplication.shared.windows.last else { return } + window.subviews.filter({ $0 is UIActivityIndicatorView }).forEach { $0.removeFromSuperview() } + } + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/Utils/Toast.swift b/Runnect-iOS/Runnect-iOS/Global/Utils/Toast.swift new file mode 100644 index 00000000..d7836d7e --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/Utils/Toast.swift @@ -0,0 +1,61 @@ +// +// Toast.swift +// Runnect-iOS +// +// Created by sejin on 2022/12/31. +// + +import UIKit + +import SnapKit + +public extension UIViewController { + func showToast(message: String) { + Toast.show(message: message, view: self.view, safeAreaBottomInset: self.safeAreaBottomInset()) + } +} + +public class Toast { + public static func show(message: String, view: UIView, safeAreaBottomInset: CGFloat = 0) { + + let toastContainer = UIView() + let toastLabel = UILabel() + + toastContainer.backgroundColor = .lightGray + toastContainer.alpha = 1 + toastContainer.layer.cornerRadius = 9 + toastContainer.clipsToBounds = true + toastContainer.isUserInteractionEnabled = false + + toastLabel.textColor = .white + toastLabel.textAlignment = .center + toastLabel.text = message + toastLabel.clipsToBounds = true + toastLabel.numberOfLines = 0 + toastLabel.sizeToFit() + + toastContainer.addSubview(toastLabel) + view.addSubview(toastContainer) + + toastContainer.snp.makeConstraints { + $0.centerX.equalToSuperview() + $0.bottom.equalToSuperview().inset(safeAreaBottomInset+40) + $0.width.equalTo(100) + $0.height.equalTo(44) + } + + toastLabel.snp.makeConstraints { + $0.center.equalToSuperview() + } + + UIView.animate(withDuration: 0.4, delay: 0.0, options: .curveEaseIn, animations: { + toastContainer.alpha = 1.0 + }, completion: { _ in + UIView.animate(withDuration: 0.4, delay: 1.0, options: .curveEaseOut, animations: { + toastContainer.alpha = 0.0 + }, completion: {_ in + toastContainer.removeFromSuperview() + }) + }) + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/Utils/adjusted+.swift b/Runnect-iOS/Runnect-iOS/Global/Utils/adjusted+.swift index adc093d9..ebe564dc 100644 --- a/Runnect-iOS/Runnect-iOS/Global/Utils/adjusted+.swift +++ b/Runnect-iOS/Runnect-iOS/Global/Utils/adjusted+.swift @@ -18,24 +18,24 @@ import UIKit extension CGFloat { var adjusted: CGFloat { - let ratio: CGFloat = UIScreen.main.bounds.width / 375 + let ratio: CGFloat = UIScreen.main.bounds.width / 390 return self * ratio } var adjustedH: CGFloat { - let ratio: CGFloat = UIScreen.main.bounds.height / 812 + let ratio: CGFloat = UIScreen.main.bounds.height / 844 return self * ratio } } extension Double { var adjusted: Double { - let ratio: Double = Double(UIScreen.main.bounds.width / 375) + let ratio: Double = Double(UIScreen.main.bounds.width / 390) return self * ratio } var adjustedH: Double { - let ratio: Double = Double(UIScreen.main.bounds.height / 812) + let ratio: Double = Double(UIScreen.main.bounds.height / 844) return self * ratio } } diff --git a/Runnect-iOS/Runnect-iOS/Info.plist b/Runnect-iOS/Runnect-iOS/Info.plist index 0eb786dc..d633f329 100644 --- a/Runnect-iOS/Runnect-iOS/Info.plist +++ b/Runnect-iOS/Runnect-iOS/Info.plist @@ -2,6 +2,14 @@ + NSLocationWhenInUseUsageDescription + 위치 정보 권한이 필요합니다. + NSLocationAlwaysUsageDescription + 위치 정보 권한이 필요합니다. + NSLocationAlwaysAndWhenInUseUsageDescription + 위치 정보 권한이 필요합니다. + NMFClientId + 1vyblfmq7l UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/Runnect-iOS/Runnect-iOS/Network/Foundation/NetworkLoggerPlugin.swift b/Runnect-iOS/Runnect-iOS/Network/Foundation/NetworkLoggerPlugin.swift new file mode 100644 index 00000000..af05c7fa --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Network/Foundation/NetworkLoggerPlugin.swift @@ -0,0 +1,122 @@ +// +// NetworkLoggerPlugin.swift +// Runnect-iOS +// +// Created by sejin on 2022/12/31. +// + +import Foundation + +import Moya + +/// Logs network activity (outgoing requests and incoming responses). +public final class NetworkLoggerPlugin: PluginType { + fileprivate let loggerId = "Moya_Logger" + fileprivate let dateFormatString = "dd/MM/yyyy HH:mm:ss" + fileprivate let dateFormatter = DateFormatter() + fileprivate let separator = ", " + fileprivate let terminator = "\n" + fileprivate let cURLTerminator = "\\\n" + fileprivate let output: (_ separator: String, _ terminator: String, _ items: Any...) -> Void + fileprivate let requestDataFormatter: ((Data) -> (String))? + fileprivate let responseDataFormatter: ((Data) -> (Data))? + + /// A Boolean value determing whether response body data should be logged. + public let isVerbose: Bool + public let cURL: Bool + + /// Initializes a NetworkLoggerPlugin. + public init(verbose: Bool = true, cURL: Bool = false, output: ((_ separator: String, _ terminator: String, _ items: Any...) -> Void)? = nil, requestDataFormatter: ((Data) -> (String))? = nil, responseDataFormatter: ((Data) -> (Data))? = nil) { + self.cURL = cURL + self.isVerbose = verbose + self.output = output ?? NetworkLoggerPlugin.reversedPrint + self.requestDataFormatter = requestDataFormatter + self.responseDataFormatter = responseDataFormatter + } + + public func willSend(_ request: RequestType, target: TargetType) { + if let request = request as? CustomDebugStringConvertible, cURL { + output(separator, terminator, request.debugDescription) + return + } + outputItems(logNetworkRequest(request.request as URLRequest?)) + } + + public func didReceive(_ result: Result, target: TargetType) { + if case .success(let response) = result { + outputItems(logNetworkResponse(response.response, data: response.data, target: target)) + } else { + print(result) + outputItems(logNetworkResponse(nil, data: nil, target: target)) + } + } + + fileprivate func outputItems(_ items: [String]) { + if isVerbose { + items.forEach { output(separator, terminator, $0) } + } else { + output(separator, terminator, items) + } + } +} + +private extension NetworkLoggerPlugin { + + var date: String { + dateFormatter.dateFormat = dateFormatString + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + return dateFormatter.string(from: Date()) + } + + func format(_ loggerId: String, date: String, identifier: String, message: String) -> String { + return "\(loggerId): [\(date)] \(identifier): \(message)" + } + + func logNetworkRequest(_ request: URLRequest?) -> [String] { + + var output = [String]() + + output += [format(loggerId, date: date, identifier: "Request", message: request?.description ?? "(invalid request)")] + + if let headers = request?.allHTTPHeaderFields { + output += [format(loggerId, date: date, identifier: "Request Headers", message: headers.description)] + } + + if let bodyStream = request?.httpBodyStream { + output += [format(loggerId, date: date, identifier: "Request Body Stream", message: bodyStream.description)] + } + + if let httpMethod = request?.httpMethod { + output += [format(loggerId, date: date, identifier: "HTTP Request Method", message: httpMethod)] + } + + if let body = request?.httpBody, let stringOutput = requestDataFormatter?(body) ?? String(data: body, encoding: .utf8), isVerbose { + output += [format(loggerId, date: date, identifier: "Request Body", message: stringOutput)] + } + + return output + } + + func logNetworkResponse(_ response: HTTPURLResponse?, data: Data?, target: TargetType) -> [String] { + guard let response = response else { + return [format(loggerId, date: date, identifier: "Response", message: "Received empty network response for \(target).")] + } + + var output = [String]() + output += [format(loggerId, date: date, identifier: "Response", message: response.description)] + + if let data = data, let stringData = String(data: responseDataFormatter?(data) ?? data, encoding: String.Encoding.utf8), isVerbose { + output += [stringData] + } + + return output + } +} + +fileprivate extension NetworkLoggerPlugin { + static func reversedPrint(_ separator: String, terminator: String, items: Any...) { + for item in items { + print(item, separator: separator, terminator: terminator) + } + } +}