From ca09b328b3a0983b2bf7d5f58de44a1e888abf8b Mon Sep 17 00:00:00 2001 From: Hyunjun Date: Wed, 4 Dec 2024 16:56:15 +0900 Subject: [PATCH 1/8] =?UTF-8?q?chore=20gitconfig,=20pre-push,=20swiftforma?= =?UTF-8?q?t=20=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitconfig | 2 +- .githooks/pre-push | 2 +- .swiftformat | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitconfig b/.gitconfig index 10fa6f89..7f0d55c1 100644 --- a/.gitconfig +++ b/.gitconfig @@ -1,2 +1,2 @@ [core] - hooksPath = /Users/hyunjun/Desktop/iOS08-Shook/.githooks \ No newline at end of file + hooksPath = .githooks \ No newline at end of file diff --git a/.githooks/pre-push b/.githooks/pre-push index 69489172..87976576 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -13,7 +13,7 @@ else fi # 포맷팅 결과 , -RESULT=$($FORMAT ./IOS08Shook --config .swiftformat) +RESULT=$($FORMAT ./Projects --config .swiftformat) if [ "$RESULT" == '' ]; then git add . diff --git a/.swiftformat b/.swiftformat index 3ad01524..e64c22bf 100644 --- a/.swiftformat +++ b/.swiftformat @@ -6,6 +6,7 @@ --exclude /Tuist # rules +--disable trailingCommas --enable blockComments --enable markTypes --enable noExplicitOwnership From f74162902ec16878b93bcc141a731ac3c9b946cf Mon Sep 17 00:00:00 2001 From: Hyunjun Date: Wed, 4 Dec 2024 16:56:46 +0900 Subject: [PATCH 2/8] =?UTF-8?q?style=20swiftformat=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/SampleHandler.swift | 35 ++-- Projects/App/Project.swift | 4 +- Projects/App/Sources/AppDelegate.swift | 16 +- Projects/App/Sources/DIContainer.swift | 10 +- .../MockFetchChannelListUsecaseImpl.swift | 21 ++- Projects/App/Sources/SceneDelegate.swift | 40 +++-- .../App/Sources/Splash/EmptyViewModel.swift | 6 +- .../Sources/Splash/SplashGradientView.swift | 34 ++-- .../Sources/Splash/SplashViewController.swift | 29 ++-- .../BaseDomain/Sources/BaseRepository.swift | 10 +- .../BaseDomain/Sources/Utils/Secrets.swift | 2 + .../Entity/BroadcastInfoEntity.swift | 2 +- .../Sources/Endpoint/BroadcastEndpoint.swift | 17 +- .../Repository/BroadcastRepositoryImpl.swift | 4 +- .../Usecase/DeleteBroadcastUsecaseImpl.swift | 4 +- .../FetchAllBroadcastUsecaseImpl.swift | 4 +- .../Usecase/MakeBroadcastUsecaseImpl.swift | 4 +- .../Sources/Endpoint/ChatEndpoint.swift | 35 ++-- .../Repository/ChatRepositoryImpl.swift | 4 +- .../Usecase/DeleteChatRoomUseCaseImpl.swift | 6 +- .../Usecase/MakeChatRoomUseCaseImpl.swift | 6 +- .../Interface/Entity/BroadcastEntity.swift | 2 +- .../Interface/Entity/ChannelEntity.swift | 4 +- .../Interface/Entity/ChannelInfoEntity.swift | 2 +- .../Interface/Entity/VideoEntity.swift | 2 +- .../DTO/Response/BroadcastResponseDTO.swift | 10 +- .../DTO/Response/ChannelInfoResponseDTO.swift | 4 +- .../DTO/Response/ChannelResponseDTO.swift | 10 +- .../DTO/Response/ThumbnailResponseDTO.swift | 10 +- .../DTO/Response/VideoListResponseDTO.swift | 10 +- .../Endpoint/LiveStationEndpoint.swift | 49 +++--- .../LiveStationRepositoryImpl.swift | 24 +-- .../UseCase/CreateChannelUsecaseImpl.swift | 4 +- .../UseCase/DeleteChannelUsecaseImpl.swift | 4 +- .../UseCase/FetchChannelInfoUsecaseImpl.swift | 4 +- .../UseCase/FetchChannelListUsecaseImpl.swift | 12 +- .../UseCase/FetchVideoListUsecaseImpl.swift | 4 +- .../Demo/Sources/AppDelegate.swift | 4 +- .../Sources/SignUpGradientView.swift | 34 ++-- .../Sources/SignUpViewController.swift | 164 ++++++++++-------- .../AuthFeature/Sources/SignUpViewModel.swift | 23 +-- .../Demo/Sources/AppDelegate.swift | 4 +- .../BaseFeature/Interface/ViewLifeCycle.swift | 2 +- .../BaseFeature/Interface/ViewModel.swift | 2 +- .../Sources/BaseCollectionViewCell.swift | 31 ++-- .../Sources/BaseNavigationController.swift | 2 +- .../Sources/BaseTableViewCell.swift | 33 ++-- .../BaseFeature/Sources/BaseView.swift | 31 ++-- .../Sources/BaseViewController.swift | 44 ++--- .../Demo/Sources/AppDelegate.swift | 6 +- .../Sources/Chating/Models/ChatInfo.swift | 10 +- .../Sources/Chating/Views/ChatEmptyView.swift | 22 +-- .../Chating/Views/ChatInputField.swift | 60 ++++--- .../Sources/Chating/Views/ChattingCell.swift | 18 +- .../Chating/Views/ChattingListView.swift | 74 ++++---- .../Chating/Views/SystemAlarmCell.swift | 10 +- .../LiveStreamViewControllerFactoryImpl.swift | 4 +- .../LiveStreamViewController.swift | 146 ++++++++-------- .../ViewModels/LiveStreamViewModel.swift | 52 +++--- .../Player/Views/LiveStreamInfoView.swift | 16 +- .../Player/Views/PlayerControlView.swift | 91 +++++----- .../Player/Views/ShookPlayerView.swift | 127 ++++++++------ .../Player/Views/TimeControlView.swift | 36 ++-- .../SampleHandler.swift | 3 +- .../Demo/Sources/AppDelegate.swift | 6 +- .../MockDeleteBroadcastUsecaseImpl.swift | 2 +- .../MockDeleteChannelUsecaseImpl.swift | 2 +- .../MockFetchChannelListUsecaseImpl.swift | 21 ++- .../MockLiveStreamViewControllerFactory.swift | 8 +- .../MockLiveStreamingViewController.swift | 40 +++-- .../MockMakeBroadcastUsecaseImpl.swift | 2 +- .../Demo/Sources/MockShookPlayerView.swift | 30 ++-- Projects/Features/MainFeature/Project.swift | 2 +- .../BroadcastViewControllerFactoryImpl.swift | 6 +- .../SettingViewControllerFactoryImpl.swift | 6 +- .../MainFeature/Sources/Models/Channel.swift | 2 +- .../CollectionViewCellTransitioning.swift | 84 ++++----- .../BroadcastCollectionViewController.swift | 127 +++++++------- .../BroadcastViewController.swift | 48 ++--- .../SettingUIViewController.swift | 88 +++++----- .../BroadcastCollectionViewModel.swift | 16 +- .../Sources/ViewModels/SettingViewModel.swift | 30 ++-- .../Views/BroadcastCollectionLoadView.swift | 50 +++--- .../EmptyBroadcastCollectionViewCell.swift | 24 +-- .../LargeBroadcastCollectionViewCell.swift | 28 +-- .../SmallBroadcastCollectionViewCell.swift | 34 ++-- .../Views/BroadcastThumbnailView.swift | 49 +++--- .../Sources/Views/PaddingLabel.swift | 2 +- .../Sources/Views/SettingTableViewCell.swift | 45 ++--- .../Demo/Sources/AppDelegate.swift | 4 +- .../Sources/Message/ChatMessage.swift | 2 +- .../Sources/SoketTestViewController.swift | 64 +++---- .../ChatSoketModule/Sources/WebSocket.swift | 94 +++++----- .../EasyLayout/Demo/Sources/AppDelegate.swift | 4 +- .../Demo/Sources/ViewController.swift | 22 +-- .../Modules/EasyLayout/Sources/Anchor.swift | 22 +-- .../EasyLayout/Sources/EasyConstraint.swift | 47 ++--- .../EasyLayout/Sources/EasyLayout.swift | 12 +- .../Sources/Protocol/Anchorable.swift | 10 +- .../Demo/Sources/AppDelegate.swift | 4 +- .../Sources/Client/NetworkClient.swift | 34 ++-- .../Sources/Client/Requestable.swift | 2 +- .../FastNetwork/Sources/Endpoint.swift | 4 +- .../FastNetwork/Sources/Error/HTTPError.swift | 12 +- .../Sources/Error/NetworkError.swift | 4 +- .../Sources/Extensions/URL+Extension.swift | 4 +- .../DefaultLoggingInterceptor.swift | 149 ++++++++-------- .../Sources/Interceptor/Interceptor.swift | 8 +- .../Request/Components/HTTPMethod.swift | 2 +- .../Request/Components/RequestTask.swift | 14 +- .../Encoding/ParamterJSONEncoder.swift | 2 + .../Encoding/RequestParameterEncodable.swift | 2 + .../Request/Encoding/URLQueryEncoder.swift | 6 +- .../FastNetwork/Testing/MockData.swift | 46 ++--- .../FastNetwork/Testing/MockURLProtocol.swift | 16 +- .../FastNetwork/Tests/MockEndpoint.swift | 24 +-- .../FastNetwork/Tests/NetworkClientTest.swift | 21 ++- .../Tests/NetworkEncoderTests.swift | 17 +- .../DesignSystem/Sources/SHFontSystem.swift | 55 +++--- .../DesignSystem/Sources/SHLoadingView.swift | 23 +-- .../Sources/SHRefreshControl.swift | 21 +-- 121 files changed, 1600 insertions(+), 1400 deletions(-) diff --git a/Projects/App/BroadcastUploadExtension/Sources/SampleHandler.swift b/Projects/App/BroadcastUploadExtension/Sources/SampleHandler.swift index 02d1bb01..3b8dc1a7 100644 --- a/Projects/App/BroadcastUploadExtension/Sources/SampleHandler.swift +++ b/Projects/App/BroadcastUploadExtension/Sources/SampleHandler.swift @@ -4,63 +4,66 @@ import HaishinKit final class SampleHandler: RPBroadcastSampleHandler { // MARK: - App group + private let sharedDefaults = UserDefaults(suiteName: "group.kr.codesquad.boostcamp9.Shook")! private let isStreamingKey = "IS_STREAMING" - + // MARK: - HaishinKit + private let mixer = MediaMixer() private let connection = RTMPConnection() private lazy var stream = RTMPStream(connection: connection) private let rotator = VideoRotator() - + // MARK: - RTMP Service URL and Streaming key + private let rtmp = "RTMP_SEVICE_URL" private let streamKey = "STREAMING_KEY" - - override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) { + + override func broadcastStarted(withSetupInfo _: [String: NSObject]?) { Task { var videoSettings = VideoCodecSettings() videoSettings.videoSize = CGSize(width: 1280, height: 720) videoSettings.scalingMode = .letterbox - + await stream.setVideoSettings(videoSettings) await mixer.addOutput(stream) - + guard let rtmpURL = sharedDefaults.string(forKey: rtmp), let streamKey = sharedDefaults.string(forKey: streamKey) else { return } - + _ = try await connection.connect(rtmpURL) _ = try await stream.publish(streamKey) } - - sharedDefaults.set(true, forKey: self.isStreamingKey) + + sharedDefaults.set(true, forKey: isStreamingKey) } - + override func broadcastFinished() { Task { _ = try await stream.close() try await connection.close() } - + sharedDefaults.set(false, forKey: isStreamingKey) } - + override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) { Task { switch sampleBufferType { case .video: - if case .success(let rotatedBuffer) = rotator?.rotate(buffer: sampleBuffer) { + if case let .success(rotatedBuffer) = rotator?.rotate(buffer: sampleBuffer) { await mixer.append(rotatedBuffer) } else { await mixer.append(sampleBuffer) } - + case .audioApp: await mixer.append(sampleBuffer, track: 0) - + case .audioMic: await mixer.append(sampleBuffer, track: 1) - + default: break } } diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index 580d3d69..86596f77 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -1,6 +1,6 @@ -import ProjectDescription import ConfigurationPlugin -import TemplatePlugin +import ProjectDescription import ProjectDescriptionHelpers +import TemplatePlugin let project = Project.project diff --git a/Projects/App/Sources/AppDelegate.swift b/Projects/App/Sources/AppDelegate.swift index adba3558..18d6a16e 100644 --- a/Projects/App/Sources/AppDelegate.swift +++ b/Projects/App/Sources/AppDelegate.swift @@ -3,22 +3,22 @@ import UIKit @main final class AppDelegate: UIResponder, UIApplicationDelegate { func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - return true + true } func application( - _ application: UIApplication, + _: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions + options _: UIScene.ConnectionOptions ) -> UISceneConfiguration { - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } func application( - _ application: UIApplication, - didDiscardSceneSessions sceneSessions: Set + _: UIApplication, + didDiscardSceneSessions _: Set ) {} } diff --git a/Projects/App/Sources/DIContainer.swift b/Projects/App/Sources/DIContainer.swift index cd511e74..b14723fa 100644 --- a/Projects/App/Sources/DIContainer.swift +++ b/Projects/App/Sources/DIContainer.swift @@ -2,16 +2,16 @@ import Foundation final class DIContainer { static let shared = DIContainer() - + private var dependencies: [String: Any] = [:] - - private init() { } - + + private init() {} + func register(_ type: T.Type, dependency: T) { let key = String(describing: type) dependencies[key] = dependency } - + func resolve(_ type: T.Type) -> T { let key = String(describing: type) guard let dependency = dependencies[key] as? T else { diff --git a/Projects/App/Sources/MockFetchChannelListUsecaseImpl.swift b/Projects/App/Sources/MockFetchChannelListUsecaseImpl.swift index 008a7124..b2dc9d6e 100644 --- a/Projects/App/Sources/MockFetchChannelListUsecaseImpl.swift +++ b/Projects/App/Sources/MockFetchChannelListUsecaseImpl.swift @@ -3,6 +3,8 @@ import UIKit import LiveStationDomainInterface +// MARK: - MockFetchChannelListUsecaseImpl + struct MockFetchChannelListUsecaseImpl: FetchChannelListUsecase { func execute() -> AnyPublisher<[ChannelEntity], any Error> { let fetcher = MockChannelListFetcher() @@ -17,16 +19,17 @@ struct MockFetchChannelListUsecaseImpl: FetchChannelListUsecase { } } +// MARK: - MockChannelListFetcher + final class MockChannelListFetcher { enum Image { case ratio16x9 case ratio4x3 func fetch() async -> UIImage? { - let size: (width: Int, height: Int) - switch self { - case .ratio16x9: size = (1920, 1080) - case .ratio4x3: size = (1440, 1080) + let size: (width: Int, height: Int) = switch self { + case .ratio16x9: (1920, 1080) + case .ratio4x3: (1440, 1080) } return await fetchImage(width: size.width, height: size.height) } @@ -34,18 +37,18 @@ final class MockChannelListFetcher { private func fetchImage(width: Int, height: Int) async -> UIImage? { guard let url = URL(string: "https://picsum.photos/\(width)/\(height)") else { return nil } - let data = (try? await URLSession.shared.data(from: url).0) ?? Data() + let data = await (try? URLSession.shared.data(from: url).0) ?? Data() return UIImage(data: data) } } func fetch() async -> [ChannelEntity] { - let random = Int.random(in: 3...7) + let random = Int.random(in: 3 ... 7) var channels: [ChannelEntity] = [] - for _ in 0..) {} + + func scene(_: UIScene, openURLContexts _: Set) {} // MARK: - Handling UniversalLink - func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {} + + func scene(_: UIScene, continue _: NSUserActivity) {} } extension SceneDelegate { @@ -49,16 +53,16 @@ extension SceneDelegate { let liveStationRepository = LiveStationRepositoryImpl() let fetchChannelListUsecaseImpl = FetchChannelListUsecaseImpl(repository: liveStationRepository) DIContainer.shared.register(FetchChannelListUsecase.self, dependency: fetchChannelListUsecaseImpl) - + let createChannelUsecaseImpl = CreateChannelUsecaseImpl(repository: liveStationRepository) DIContainer.shared.register(CreateChannelUsecase.self, dependency: createChannelUsecaseImpl) - + let deleteChannelUsecaseImpl = DeleteChannelUsecaseImpl(repository: liveStationRepository) DIContainer.shared.register(DeleteChannelUsecase.self, dependency: deleteChannelUsecaseImpl) - + let fetchChannelInfoUsecaseImpl: any FetchChannelInfoUsecase = FetchChannelInfoUsecaseImpl(repository: liveStationRepository) DIContainer.shared.register(FetchChannelInfoUsecase.self, dependency: fetchChannelInfoUsecaseImpl) - + let broadcastRepository = BroadcastRepositoryImpl() let makeBroadcastUsecaseImpl: any MakeBroadcastUsecase = MakeBroadcastUsecaseImpl(repository: broadcastRepository) let fetchAllBroadcastUsecaseImpl = FetchAllBroadcastUsecaseImpl(repository: broadcastRepository) @@ -70,14 +74,14 @@ extension SceneDelegate { let fetchBroadcastUseCase: any FetchVideoListUsecase = FetchVideoListUsecaseImpl(repository: liveStationRepository) let liveStreamFactoryImpl = LiveStreamViewControllerFactoryImpl(fetchBroadcastUseCase: fetchBroadcastUseCase) DIContainer.shared.register(LiveStreamViewControllerFactory.self, dependency: liveStreamFactoryImpl) - + let settingFactoryImpl = SettingViewControllerFactoryImpl( fetchChannelInfoUsecase: fetchChannelInfoUsecaseImpl, makeBroadcastUsecase: makeBroadcastUsecaseImpl, deleteBroadCastUsecase: deleteBroadCastUsecaseImpl ) DIContainer.shared.register(SettingViewControllerFactory.self, dependency: settingFactoryImpl) - + let broadcastViewControllerFactory = BroadcastViewControllerFactoryImpl( fetchChannelInfoUsecase: fetchChannelInfoUsecaseImpl, makeBroadcastUsecase: makeBroadcastUsecaseImpl, diff --git a/Projects/App/Sources/Splash/EmptyViewModel.swift b/Projects/App/Sources/Splash/EmptyViewModel.swift index 44f6ab2a..1ade24d9 100644 --- a/Projects/App/Sources/Splash/EmptyViewModel.swift +++ b/Projects/App/Sources/Splash/EmptyViewModel.swift @@ -3,8 +3,8 @@ import BaseFeatureInterface public class EmptyViewModel: ViewModel { public struct Input {} public struct Output {} - - public func transform(input: Input) -> Output { - return Output() + + public func transform(input _: Input) -> Output { + Output() } } diff --git a/Projects/App/Sources/Splash/SplashGradientView.swift b/Projects/App/Sources/Splash/SplashGradientView.swift index 17640e13..755275a9 100644 --- a/Projects/App/Sources/Splash/SplashGradientView.swift +++ b/Projects/App/Sources/Splash/SplashGradientView.swift @@ -2,45 +2,45 @@ import UIKit final class SplashGradientView: UIView { private let gradientLayer = CAGradientLayer() - + private let colors: [[CGColor]] = [ - [CGColor(red: 31/255, green: 52/255, blue: 55/255, alpha: 1), - CGColor(red: 0/255, green: 0/255, blue: 0/255, alpha: 1)], - - [CGColor(red: 31/255, green: 52/255, blue: 55/255, alpha: 1), - CGColor(red: 22/255, green: 23/255, blue: 31/255, alpha: 1)], - - [CGColor(red: 22/255, green: 23/255, blue: 31/255, alpha: 1), - CGColor(red: 31/255, green: 52/255, blue: 55/255, alpha: 1)], - - [CGColor(red: 0/255, green: 0/255, blue: 0/255, alpha: 1), - CGColor(red: 31/255, green: 52/255, blue: 55/255, alpha: 1)] + [CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1), + CGColor(red: 0 / 255, green: 0 / 255, blue: 0 / 255, alpha: 1)], + + [CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1), + CGColor(red: 22 / 255, green: 23 / 255, blue: 31 / 255, alpha: 1)], + + [CGColor(red: 22 / 255, green: 23 / 255, blue: 31 / 255, alpha: 1), + CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1)], + + [CGColor(red: 0 / 255, green: 0 / 255, blue: 0 / 255, alpha: 1), + CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1)] ] - + override init(frame: CGRect) { super.init(frame: frame) setupGradientLayer() startDynamicColorAnimation() } - + required init?(coder: NSCoder) { super.init(coder: coder) setupGradientLayer() startDynamicColorAnimation() } - + override func layoutSubviews() { super.layoutSubviews() gradientLayer.frame = bounds } - + private func setupGradientLayer() { gradientLayer.colors = colors.first gradientLayer.startPoint = CGPoint(x: 0, y: 1) gradientLayer.endPoint = CGPoint(x: 1, y: 0) layer.insertSublayer(gradientLayer, at: 0) } - + private func startDynamicColorAnimation() { let colorAnimation = CAKeyframeAnimation(keyPath: "colors") colorAnimation.values = colors diff --git a/Projects/App/Sources/Splash/SplashViewController.swift b/Projects/App/Sources/Splash/SplashViewController.swift index 20930599..da2a20e2 100644 --- a/Projects/App/Sources/Splash/SplashViewController.swift +++ b/Projects/App/Sources/Splash/SplashViewController.swift @@ -13,32 +13,34 @@ import Lottie import MainFeature import MainFeatureInterface +// MARK: - SplashViewController + public final class SplashViewController: BaseViewController { private let splashGradientView = SplashGradientView() private let splashAnimationView = LottieAnimationView(name: "splash", bundle: Bundle(for: DesignSystemResources.self)) - - public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return .portrait + + override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { + .portrait } - - public override func viewDidAppear(_ animated: Bool) { + + override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - + splashAnimationView.play { [weak self] _ in self?.moveToMainView() } } - - public override func setupViews() { + + override public func setupViews() { view.addSubview(splashGradientView) view.addSubview(splashAnimationView) } - - public override func setupLayouts() { + + override public func setupLayouts() { splashGradientView.ezl.makeConstraint { $0.diagonal(to: view) } - + splashAnimationView.ezl.makeConstraint { $0.diagonal(to: view) } @@ -46,6 +48,7 @@ public final class SplashViewController: BaseViewController { } // MARK: - View Transition + extension SplashViewController { private func moveToMainView() { let fetchChannelListUsecase = DIContainer.shared.resolve(FetchChannelListUsecase.self) @@ -66,10 +69,10 @@ extension SplashViewController { let singUpViewModel = SignUpViewModel(createChannelUsecase: createChannelUsecase) navigationController.viewControllers.append(SignUpViewController(viewModel: singUpViewModel)) } - + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let window = windowScene.windows.first else { return } - + UIView.transition(with: window, duration: 0.4, options: .transitionCrossDissolve) { window.rootViewController = navigationController } diff --git a/Projects/Domains/BaseDomain/Sources/BaseRepository.swift b/Projects/Domains/BaseDomain/Sources/BaseRepository.swift index def39525..77225f37 100644 --- a/Projects/Domains/BaseDomain/Sources/BaseRepository.swift +++ b/Projects/Domains/BaseDomain/Sources/BaseRepository.swift @@ -3,18 +3,20 @@ import Foundation import FastNetwork +// MARK: - BaseRepository + open class BaseRepository { - private let decoder: JSONDecoder = JSONDecoder() + private let decoder: JSONDecoder = .init() private let client: NetworkClient - + public init() { var interceptors: [any Interceptor] = [] #if DEBUG - interceptors.append(DefaultLoggingInterceptor()) + interceptors.append(DefaultLoggingInterceptor()) #endif client = NetworkClient(interceptors: interceptors) } - + public final func request(_ endpoint: E, type: T.Type) -> AnyPublisher where T: Decodable { performRequest(endpoint) .map(\.data) diff --git a/Projects/Domains/BaseDomain/Sources/Utils/Secrets.swift b/Projects/Domains/BaseDomain/Sources/Utils/Secrets.swift index bf4bd21e..ee5b5c55 100644 --- a/Projects/Domains/BaseDomain/Sources/Utils/Secrets.swift +++ b/Projects/Domains/BaseDomain/Sources/Utils/Secrets.swift @@ -1,5 +1,7 @@ import Foundation +// MARK: - KeyKind + public enum KeyKind: String { case secretKey = "SECRET_KEY" case accessKey = "ACCESS_KEY" diff --git a/Projects/Domains/BroadcastDomain/Interface/Entity/BroadcastInfoEntity.swift b/Projects/Domains/BroadcastDomain/Interface/Entity/BroadcastInfoEntity.swift index 090ddcd7..8a31580d 100644 --- a/Projects/Domains/BroadcastDomain/Interface/Entity/BroadcastInfoEntity.swift +++ b/Projects/Domains/BroadcastDomain/Interface/Entity/BroadcastInfoEntity.swift @@ -3,7 +3,7 @@ public struct BroadcastInfoEntity { public let title: String public let owner: String public let description: String - + public init(id: String, title: String, owner: String, description: String) { self.id = id self.title = title diff --git a/Projects/Domains/BroadcastDomain/Sources/Endpoint/BroadcastEndpoint.swift b/Projects/Domains/BroadcastDomain/Sources/Endpoint/BroadcastEndpoint.swift index f64c5e9e..e1aef6c4 100644 --- a/Projects/Domains/BroadcastDomain/Sources/Endpoint/BroadcastEndpoint.swift +++ b/Projects/Domains/BroadcastDomain/Sources/Endpoint/BroadcastEndpoint.swift @@ -3,12 +3,16 @@ import Foundation import BaseDomain import FastNetwork +// MARK: - BroadcastEndpoint + public enum BroadcastEndpoint { case make(id: String, title: String, owner: String, description: String) case fetchAll case delete(id: String) } +// MARK: Endpoint + extension BroadcastEndpoint: Endpoint { public var method: FastNetwork.HTTPMethod { switch self { @@ -17,25 +21,25 @@ extension BroadcastEndpoint: Endpoint { case .delete: .delete } } - + public var header: [String: String]? { [ "Content-Type": "application/json" ] } - + public var scheme: String { "http" } - + public var host: String { config(key: .host) } - + public var port: Int? { Int(config(key: .port)) ?? 0 } - + public var path: String { switch self { case .make: "/broadcast" @@ -43,7 +47,7 @@ extension BroadcastEndpoint: Endpoint { case let .delete(id): "/broadcast/delete/\(id)" } } - + public var requestTask: FastNetwork.RequestTask { switch self { case let .make(id, title, owner, description): .withObject( @@ -58,5 +62,4 @@ extension BroadcastEndpoint: Endpoint { case .delete: .empty } } - } diff --git a/Projects/Domains/BroadcastDomain/Sources/Repository/BroadcastRepositoryImpl.swift b/Projects/Domains/BroadcastDomain/Sources/Repository/BroadcastRepositoryImpl.swift index 1930625d..cdee7992 100644 --- a/Projects/Domains/BroadcastDomain/Sources/Repository/BroadcastRepositoryImpl.swift +++ b/Projects/Domains/BroadcastDomain/Sources/Repository/BroadcastRepositoryImpl.swift @@ -9,13 +9,13 @@ public final class BroadcastRepositoryImpl: BaseRepository, B .map { _ in () } .eraseToAnyPublisher() } - + public func fetchAllBroadcast() -> AnyPublisher<[BroadcastInfoEntity], any Error> { request(.fetchAll, type: [BroadcastDTO].self) .map { $0.map { BroadcastInfoEntity(id: $0.id, title: $0.title, owner: $0.owner, description: $0.description) } } .eraseToAnyPublisher() } - + public func deleteBroadcast(id: String) -> AnyPublisher { request(.delete(id: id), type: BaseDTO.self) .map { _ in () } diff --git a/Projects/Domains/BroadcastDomain/Sources/Usecase/DeleteBroadcastUsecaseImpl.swift b/Projects/Domains/BroadcastDomain/Sources/Usecase/DeleteBroadcastUsecaseImpl.swift index 36c48f46..91e4bc13 100644 --- a/Projects/Domains/BroadcastDomain/Sources/Usecase/DeleteBroadcastUsecaseImpl.swift +++ b/Projects/Domains/BroadcastDomain/Sources/Usecase/DeleteBroadcastUsecaseImpl.swift @@ -5,11 +5,11 @@ import BroadcastDomainInterface public struct DeleteBroadcastUsecaseImpl: DeleteBroadcastUsecase { private let repository: any BroadcastRepository - + public init(repository: any BroadcastRepository) { self.repository = repository } - + public func execute(id: String) -> AnyPublisher { repository.deleteBroadcast(id: id) } diff --git a/Projects/Domains/BroadcastDomain/Sources/Usecase/FetchAllBroadcastUsecaseImpl.swift b/Projects/Domains/BroadcastDomain/Sources/Usecase/FetchAllBroadcastUsecaseImpl.swift index eada1304..a61442ef 100644 --- a/Projects/Domains/BroadcastDomain/Sources/Usecase/FetchAllBroadcastUsecaseImpl.swift +++ b/Projects/Domains/BroadcastDomain/Sources/Usecase/FetchAllBroadcastUsecaseImpl.swift @@ -5,11 +5,11 @@ import BroadcastDomainInterface public struct FetchAllBroadcastUsecaseImpl: FetchAllBroadcastUsecase { private let repository: any BroadcastRepository - + public init(repository: any BroadcastRepository) { self.repository = repository } - + public func execute() -> AnyPublisher<[BroadcastInfoEntity], any Error> { repository.fetchAllBroadcast() } diff --git a/Projects/Domains/BroadcastDomain/Sources/Usecase/MakeBroadcastUsecaseImpl.swift b/Projects/Domains/BroadcastDomain/Sources/Usecase/MakeBroadcastUsecaseImpl.swift index d04ee9f6..212b2dca 100644 --- a/Projects/Domains/BroadcastDomain/Sources/Usecase/MakeBroadcastUsecaseImpl.swift +++ b/Projects/Domains/BroadcastDomain/Sources/Usecase/MakeBroadcastUsecaseImpl.swift @@ -5,11 +5,11 @@ import BroadcastDomainInterface public struct MakeBroadcastUsecaseImpl: MakeBroadcastUsecase { private let repository: any BroadcastRepository - + public init(repository: any BroadcastRepository) { self.repository = repository } - + public func execute(id: String, title: String, owner: String, description: String) -> AnyPublisher { repository.makeBroadcast(id: id, title: title, owner: owner, description: description) } diff --git a/Projects/Domains/ChattingDomain/Sources/Endpoint/ChatEndpoint.swift b/Projects/Domains/ChattingDomain/Sources/Endpoint/ChatEndpoint.swift index 36f1e43f..26b2c64f 100644 --- a/Projects/Domains/ChattingDomain/Sources/Endpoint/ChatEndpoint.swift +++ b/Projects/Domains/ChattingDomain/Sources/Endpoint/ChatEndpoint.swift @@ -4,58 +4,61 @@ import Foundation import BaseDomain import NetworkModule +// MARK: - ChatEndpoint + public enum ChatEndpoint { case makeRoom(String) case deleteRoom(String) } +// MARK: Endpoint + extension ChatEndpoint: Endpoint { public var method: NetworkModule.HTTPMethod { switch self { case .makeRoom: - return .post - + .post + case .deleteRoom: - return .delete + .delete } } - + public var header: [String: String]? { [ "Content-Type": "application/json" ] } - + public var scheme: String { "http" } - + public var host: String { config(key: .host) } - + public var port: Int? { Int(config(key: .port)) ?? 0 } - + public var path: String { switch self { case .makeRoom: - return "/chat" - + "/chat" + case let .deleteRoom(id): - return "/chat/delete/\(id)" + "/chat/delete/\(id)" } } - + public var requestTask: NetworkModule.RequestTask { switch self { case let .makeRoom(id): - return .withObject(body: MakeRoomRequestDTO(id: id)) - + .withObject(body: MakeRoomRequestDTO(id: id)) + case .deleteRoom: - return .empty + .empty } } - } diff --git a/Projects/Domains/ChattingDomain/Sources/Repository/ChatRepositoryImpl.swift b/Projects/Domains/ChattingDomain/Sources/Repository/ChatRepositoryImpl.swift index 0cd517ce..f9ed9fba 100644 --- a/Projects/Domains/ChattingDomain/Sources/Repository/ChatRepositoryImpl.swift +++ b/Projects/Domains/ChattingDomain/Sources/Repository/ChatRepositoryImpl.swift @@ -4,14 +4,12 @@ import BaseDomain import ChattingDomainInterface public final class ChatRepositoryImpl: BaseRepository, ChatRepository { - public func makeChatRoom(_ id: String) -> AnyPublisher { request(.makeRoom(id), type: BaseChatDTO.self) .map { _ in () } .eraseToAnyPublisher() - } - + public func deleteChatRoom(_ id: String) -> AnyPublisher { request(.deleteRoom(id), type: BaseChatDTO.self) .map { _ in () } diff --git a/Projects/Domains/ChattingDomain/Sources/Usecase/DeleteChatRoomUseCaseImpl.swift b/Projects/Domains/ChattingDomain/Sources/Usecase/DeleteChatRoomUseCaseImpl.swift index 598aeb2e..bd7c541f 100644 --- a/Projects/Domains/ChattingDomain/Sources/Usecase/DeleteChatRoomUseCaseImpl.swift +++ b/Projects/Domains/ChattingDomain/Sources/Usecase/DeleteChatRoomUseCaseImpl.swift @@ -4,15 +4,13 @@ import Foundation import ChattingDomainInterface public struct DeleteChatRoomUseCaseImpl: DeleteChatRoomUseCase { - private let repository: any ChatRepository - + public init(repository: any ChatRepository) { self.repository = repository } - + public func execute(id: String) -> AnyPublisher { repository.deleteChatRoom(id) } - } diff --git a/Projects/Domains/ChattingDomain/Sources/Usecase/MakeChatRoomUseCaseImpl.swift b/Projects/Domains/ChattingDomain/Sources/Usecase/MakeChatRoomUseCaseImpl.swift index a5a63d8b..2517c5d3 100644 --- a/Projects/Domains/ChattingDomain/Sources/Usecase/MakeChatRoomUseCaseImpl.swift +++ b/Projects/Domains/ChattingDomain/Sources/Usecase/MakeChatRoomUseCaseImpl.swift @@ -4,15 +4,13 @@ import Foundation import ChattingDomainInterface public struct MakeChatRoomUseCaseImpl: MakeChatRoomUseCase { - private let repository: any ChatRepository - + public init(repository: any ChatRepository) { self.repository = repository } - + public func execute(id: String) -> AnyPublisher { repository.makeChatRoom(id) } - } diff --git a/Projects/Domains/LiveStationDomain/Interface/Entity/BroadcastEntity.swift b/Projects/Domains/LiveStationDomain/Interface/Entity/BroadcastEntity.swift index 466e5dfe..692aebd4 100644 --- a/Projects/Domains/LiveStationDomain/Interface/Entity/BroadcastEntity.swift +++ b/Projects/Domains/LiveStationDomain/Interface/Entity/BroadcastEntity.swift @@ -1,7 +1,7 @@ public struct BroadcastEntity { public let name: String public let urlString: String - + public init(name: String, urlString: String) { self.name = name self.urlString = urlString diff --git a/Projects/Domains/LiveStationDomain/Interface/Entity/ChannelEntity.swift b/Projects/Domains/LiveStationDomain/Interface/Entity/ChannelEntity.swift index c1020307..c6c0d8c1 100644 --- a/Projects/Domains/LiveStationDomain/Interface/Entity/ChannelEntity.swift +++ b/Projects/Domains/LiveStationDomain/Interface/Entity/ChannelEntity.swift @@ -4,10 +4,10 @@ public struct ChannelEntity { public let id: String public let name: String public var imageURLString: String - + public init(id: String, name: String) { self.id = id self.name = name - self.imageURLString = "" + imageURLString = "" } } diff --git a/Projects/Domains/LiveStationDomain/Interface/Entity/ChannelInfoEntity.swift b/Projects/Domains/LiveStationDomain/Interface/Entity/ChannelInfoEntity.swift index 6ddb946e..8bd3d321 100644 --- a/Projects/Domains/LiveStationDomain/Interface/Entity/ChannelInfoEntity.swift +++ b/Projects/Domains/LiveStationDomain/Interface/Entity/ChannelInfoEntity.swift @@ -3,7 +3,7 @@ public struct ChannelInfoEntity { public let name: String public let streamKey: String public let rtmpUrl: String - + public init(id: String, name: String, streamKey: String, rtmpUrl: String) { self.id = id self.name = name diff --git a/Projects/Domains/LiveStationDomain/Interface/Entity/VideoEntity.swift b/Projects/Domains/LiveStationDomain/Interface/Entity/VideoEntity.swift index 2cdc233c..b8a72365 100644 --- a/Projects/Domains/LiveStationDomain/Interface/Entity/VideoEntity.swift +++ b/Projects/Domains/LiveStationDomain/Interface/Entity/VideoEntity.swift @@ -1,7 +1,7 @@ public struct VideoEntity { public let name: String public let videoURLString: String - + public init(name: String, videoURLString: String) { self.name = name self.videoURLString = videoURLString diff --git a/Projects/Domains/LiveStationDomain/Sources/DTO/Response/BroadcastResponseDTO.swift b/Projects/Domains/LiveStationDomain/Sources/DTO/Response/BroadcastResponseDTO.swift index 28baf44f..945b017e 100644 --- a/Projects/Domains/LiveStationDomain/Sources/DTO/Response/BroadcastResponseDTO.swift +++ b/Projects/Domains/LiveStationDomain/Sources/DTO/Response/BroadcastResponseDTO.swift @@ -1,15 +1,19 @@ import LiveStationDomainInterface +// MARK: - BroadcastResponseDTO + public struct BroadcastResponseDTO: Decodable { let content: [BroadcastResponse] } +// MARK: - BroadcastResponse + public struct BroadcastResponse: Decodable { let name, url, resolution, videoBitrate, audioBitrate: String } -extension BroadcastResponse { - public func toDomain() -> BroadcastEntity { - BroadcastEntity(name: self.name, urlString: self.url) +public extension BroadcastResponse { + func toDomain() -> BroadcastEntity { + BroadcastEntity(name: name, urlString: url) } } diff --git a/Projects/Domains/LiveStationDomain/Sources/DTO/Response/ChannelInfoResponseDTO.swift b/Projects/Domains/LiveStationDomain/Sources/DTO/Response/ChannelInfoResponseDTO.swift index 247cee8c..27df8a8a 100644 --- a/Projects/Domains/LiveStationDomain/Sources/DTO/Response/ChannelInfoResponseDTO.swift +++ b/Projects/Domains/LiveStationDomain/Sources/DTO/Response/ChannelInfoResponseDTO.swift @@ -7,9 +7,9 @@ struct ChannelInfoResponseDTO: Decodable { let streamKey: String let publishUrl: String } - + let content: ContentResponseDTO - + var toDomain: ChannelInfoEntity { ChannelInfoEntity( id: content.channelId, diff --git a/Projects/Domains/LiveStationDomain/Sources/DTO/Response/ChannelResponseDTO.swift b/Projects/Domains/LiveStationDomain/Sources/DTO/Response/ChannelResponseDTO.swift index df5fa063..62bbfe5a 100644 --- a/Projects/Domains/LiveStationDomain/Sources/DTO/Response/ChannelResponseDTO.swift +++ b/Projects/Domains/LiveStationDomain/Sources/DTO/Response/ChannelResponseDTO.swift @@ -1,16 +1,20 @@ import LiveStationDomainInterface +// MARK: - ChannelResponseDTO + public struct ChannelResponseDTO: Decodable { let content: ChannelContentResponseDTO } +// MARK: - ChannelContentResponseDTO + public struct ChannelContentResponseDTO: Decodable { let channelId: String let channelName: String } -extension ChannelContentResponseDTO { - public func toDomain() -> ChannelEntity { - ChannelEntity(id: self.channelId, name: self.channelName) +public extension ChannelContentResponseDTO { + func toDomain() -> ChannelEntity { + ChannelEntity(id: channelId, name: channelName) } } diff --git a/Projects/Domains/LiveStationDomain/Sources/DTO/Response/ThumbnailResponseDTO.swift b/Projects/Domains/LiveStationDomain/Sources/DTO/Response/ThumbnailResponseDTO.swift index 081d0c28..5cd0a5f9 100644 --- a/Projects/Domains/LiveStationDomain/Sources/DTO/Response/ThumbnailResponseDTO.swift +++ b/Projects/Domains/LiveStationDomain/Sources/DTO/Response/ThumbnailResponseDTO.swift @@ -1,13 +1,17 @@ +// MARK: - ThumbnailResponseDTO + public struct ThumbnailResponseDTO: Decodable { let content: [ThumbnailResponse] } +// MARK: - ThumbnailResponse + public struct ThumbnailResponse: Decodable { let name, url: String } -extension ThumbnailResponse { - public func toDomain() -> String { - self.url +public extension ThumbnailResponse { + func toDomain() -> String { + url } } diff --git a/Projects/Domains/LiveStationDomain/Sources/DTO/Response/VideoListResponseDTO.swift b/Projects/Domains/LiveStationDomain/Sources/DTO/Response/VideoListResponseDTO.swift index b8bb8763..0a6c337d 100644 --- a/Projects/Domains/LiveStationDomain/Sources/DTO/Response/VideoListResponseDTO.swift +++ b/Projects/Domains/LiveStationDomain/Sources/DTO/Response/VideoListResponseDTO.swift @@ -1,15 +1,19 @@ import LiveStationDomainInterface +// MARK: - VideoListResponseDTO + public struct VideoListResponseDTO: Decodable { let content: [VideoResponse] } +// MARK: - VideoResponse + public struct VideoResponse: Decodable { let name, url: String } -extension VideoResponse { - public func toDomain() -> VideoEntity { - VideoEntity(name: self.name, videoURLString: self.url) +public extension VideoResponse { + func toDomain() -> VideoEntity { + VideoEntity(name: name, videoURLString: url) } } diff --git a/Projects/Domains/LiveStationDomain/Sources/Endpoint/LiveStationEndpoint.swift b/Projects/Domains/LiveStationDomain/Sources/Endpoint/LiveStationEndpoint.swift index 1043a5ad..b74dd324 100644 --- a/Projects/Domains/LiveStationDomain/Sources/Endpoint/LiveStationEndpoint.swift +++ b/Projects/Domains/LiveStationDomain/Sources/Endpoint/LiveStationEndpoint.swift @@ -4,6 +4,8 @@ import Foundation import BaseDomain import FastNetwork +// MARK: - LiveStationEndpoint + public enum LiveStationEndpoint { case fetchChannelList case receiveBroadcast(channelId: String) @@ -13,15 +15,17 @@ public enum LiveStationEndpoint { case fetchChannelInfo(channelId: String) } +// MARK: Endpoint + extension LiveStationEndpoint: Endpoint { public var method: FastNetwork.HTTPMethod { switch self { - case .fetchChannelList, .receiveBroadcast, .fetchThumbnail, .fetchChannelInfo: .get + case .fetchChannelInfo, .fetchChannelList, .fetchThumbnail, .receiveBroadcast: .get case .makeChannel: .post case .deleteChannel: .delete } } - + public var header: [String: String]? { let timestamp = String(Int(Date().timeIntervalSince1970 * 1000)) return [ @@ -31,38 +35,38 @@ extension LiveStationEndpoint: Endpoint { "x-ncp-region_code": "KR" ] } - + public var host: String { "livestation.apigw.ntruss.com" } - + public var path: String { switch self { case .fetchChannelList, .makeChannel: "/api/v2/channels" - case let .receiveBroadcast(channelId), let .fetchThumbnail(channelId): "/api/v2/channels/\(channelId)/serviceUrls" + case let .fetchThumbnail(channelId), let .receiveBroadcast(channelId): "/api/v2/channels/\(channelId)/serviceUrls" case let .deleteChannel(channelId), let .fetchChannelInfo(channelId): "/api/v2/channels/\(channelId)" } } - + public var requestTask: FastNetwork.RequestTask { switch self { case .fetchChannelList: - return .withParameters( + .withParameters( query: ["channelStatus": "PUBLISHING"] ) - + case .receiveBroadcast: - return .withParameters( + .withParameters( query: ["serviceUrlType": ServiceUrlType.timemachine.rawValue] ) - + case .fetchThumbnail: - return .withParameters( + .withParameters( query: ["serviceUrlType": ServiceUrlType.thumbnail.rawValue] ) - + case let .makeChannel(channelName): - return .withParameters( + .withParameters( body: [ "channelName": channelName, "cdn": [ @@ -83,17 +87,17 @@ extension LiveStationEndpoint: Endpoint { "timemachineMin": 360 ] ) - - case .deleteChannel, .fetchChannelInfo: return .empty + + case .deleteChannel, .fetchChannelInfo: .empty } } } private extension LiveStationEndpoint { func makeQueryString(with query: Parameters) -> String { - return "?" + query.map { "\($0.key)=\($0.value)" }.joined(separator: "&") + "?" + query.map { "\($0.key)=\($0.value)" }.joined(separator: "&") } - + func makeSignature(with timestamp: String) -> String { let space = " " let newLine = "\n" @@ -101,13 +105,13 @@ private extension LiveStationEndpoint { let accessKey = config(key: .accessKey) let secretKey = config(key: .secretKey) let timestamp = timestamp - + var url = path switch requestTask { case .empty: break - - case let .withParameters(_, query, _, _), let .withObject(_, query, _): + + case let .withObject(_, query, _), let .withParameters(_, query, _, _): if let query { let queryString = makeQueryString(with: query) url.append(queryString) @@ -119,7 +123,8 @@ private extension LiveStationEndpoint { // HMAC SHA256으로 서명 생성 guard let keyData = secretKey.data(using: .utf8), - let messageData = message.data(using: .utf8) else { + let messageData = message.data(using: .utf8) + else { return "" } @@ -132,7 +137,7 @@ private extension LiveStationEndpoint { let hmacData = Data(hmac) let base64Signature = hmacData.base64EncodedString() - + return base64Signature } } diff --git a/Projects/Domains/LiveStationDomain/Sources/Repository/LiveStationRepositoryImpl.swift b/Projects/Domains/LiveStationDomain/Sources/Repository/LiveStationRepositoryImpl.swift index 3c1ba338..5a7b4bb2 100644 --- a/Projects/Domains/LiveStationDomain/Sources/Repository/LiveStationRepositoryImpl.swift +++ b/Projects/Domains/LiveStationDomain/Sources/Repository/LiveStationRepositoryImpl.swift @@ -5,38 +5,38 @@ import LiveStationDomainInterface public final class LiveStationRepositoryImpl: BaseRepository, LiveStationRepository { public func fetchChannelList() -> AnyPublisher<[ChannelEntity], any Error> { - return request(.fetchChannelList, type: ChannelListResponseDTO.self) + request(.fetchChannelList, type: ChannelListResponseDTO.self) .map { $0.content.map { $0.toDomain() }} .eraseToAnyPublisher() } - + public func fetchThumbnail(channelId: String) -> AnyPublisher { - return request(.fetchThumbnail(channelId: channelId), type: ThumbnailResponseDTO.self) + request(.fetchThumbnail(channelId: channelId), type: ThumbnailResponseDTO.self) .compactMap { $0.content.first?.toDomain() } .eraseToAnyPublisher() } - + public func fetchBroadcast(channelId: String) -> AnyPublisher<[VideoEntity], any Error> { - return request(.receiveBroadcast(channelId: channelId), type: VideoListResponseDTO.self) + request(.receiveBroadcast(channelId: channelId), type: VideoListResponseDTO.self) .map { $0.content.map { $0.toDomain() }} .eraseToAnyPublisher() } - + public func createChannel(name: String) -> AnyPublisher { - return request(.makeChannel(channelName: name), type: ChannelResponseDTO.self) + request(.makeChannel(channelName: name), type: ChannelResponseDTO.self) .map { $0.content.toDomain() } .eraseToAnyPublisher() } - + public func deleteChannel(id: String) -> AnyPublisher { - return request(.deleteChannel(channelId: id), type: ChannelResponseDTO.self) + request(.deleteChannel(channelId: id), type: ChannelResponseDTO.self) .map { $0.content.toDomain() } .eraseToAnyPublisher() } - + public func fetchChannelInfo(id: String) -> AnyPublisher { - return request(.fetchChannelInfo(channelId: id), type: ChannelInfoResponseDTO.self) - .map { $0.toDomain } + request(.fetchChannelInfo(channelId: id), type: ChannelInfoResponseDTO.self) + .map(\.toDomain) .eraseToAnyPublisher() } } diff --git a/Projects/Domains/LiveStationDomain/Sources/UseCase/CreateChannelUsecaseImpl.swift b/Projects/Domains/LiveStationDomain/Sources/UseCase/CreateChannelUsecaseImpl.swift index d2affba2..f8146548 100644 --- a/Projects/Domains/LiveStationDomain/Sources/UseCase/CreateChannelUsecaseImpl.swift +++ b/Projects/Domains/LiveStationDomain/Sources/UseCase/CreateChannelUsecaseImpl.swift @@ -4,11 +4,11 @@ import LiveStationDomainInterface public struct CreateChannelUsecaseImpl: CreateChannelUsecase { private let repository: LiveStationRepository - + public init(repository: LiveStationRepository) { self.repository = repository } - + public func execute(name: String) -> AnyPublisher { repository.createChannel(name: name) } diff --git a/Projects/Domains/LiveStationDomain/Sources/UseCase/DeleteChannelUsecaseImpl.swift b/Projects/Domains/LiveStationDomain/Sources/UseCase/DeleteChannelUsecaseImpl.swift index 148f1f0d..bffd2a55 100644 --- a/Projects/Domains/LiveStationDomain/Sources/UseCase/DeleteChannelUsecaseImpl.swift +++ b/Projects/Domains/LiveStationDomain/Sources/UseCase/DeleteChannelUsecaseImpl.swift @@ -4,11 +4,11 @@ import LiveStationDomainInterface public struct DeleteChannelUsecaseImpl: DeleteChannelUsecase { private let repository: LiveStationRepository - + public init(repository: LiveStationRepository) { self.repository = repository } - + public func execute(channelID: String) -> AnyPublisher { repository.deleteChannel(id: channelID) .map { _ in () } diff --git a/Projects/Domains/LiveStationDomain/Sources/UseCase/FetchChannelInfoUsecaseImpl.swift b/Projects/Domains/LiveStationDomain/Sources/UseCase/FetchChannelInfoUsecaseImpl.swift index e60ddf80..619c095b 100644 --- a/Projects/Domains/LiveStationDomain/Sources/UseCase/FetchChannelInfoUsecaseImpl.swift +++ b/Projects/Domains/LiveStationDomain/Sources/UseCase/FetchChannelInfoUsecaseImpl.swift @@ -4,11 +4,11 @@ import LiveStationDomainInterface public struct FetchChannelInfoUsecaseImpl: FetchChannelInfoUsecase { private let repository: any LiveStationRepository - + public init(repository: any LiveStationRepository) { self.repository = repository } - + public func execute(channelID: String) -> AnyPublisher { repository.fetchChannelInfo(id: channelID) } diff --git a/Projects/Domains/LiveStationDomain/Sources/UseCase/FetchChannelListUsecaseImpl.swift b/Projects/Domains/LiveStationDomain/Sources/UseCase/FetchChannelListUsecaseImpl.swift index 6ea5da91..869a4afe 100644 --- a/Projects/Domains/LiveStationDomain/Sources/UseCase/FetchChannelListUsecaseImpl.swift +++ b/Projects/Domains/LiveStationDomain/Sources/UseCase/FetchChannelListUsecaseImpl.swift @@ -4,17 +4,17 @@ import LiveStationDomainInterface public struct FetchChannelListUsecaseImpl: FetchChannelListUsecase { private let repository: any LiveStationRepository - + public init(repository: any LiveStationRepository) { self.repository = repository } - + public func execute() -> AnyPublisher<[ChannelEntity], any Error> { repository.fetchChannelList() - .flatMap(processChannelEntities) - .eraseToAnyPublisher() + .flatMap(processChannelEntities) + .eraseToAnyPublisher() } - + private func processChannelEntities(_ channelEntities: [ChannelEntity]) -> AnyPublisher<[ChannelEntity], any Error> { let channels = channelEntities.map { channel in repository.fetchThumbnail(channelId: channel.id) @@ -25,7 +25,7 @@ public struct FetchChannelListUsecaseImpl: FetchChannelListUsecase { } .eraseToAnyPublisher() } - + return Publishers.MergeMany(channels) .collect() .eraseToAnyPublisher() diff --git a/Projects/Domains/LiveStationDomain/Sources/UseCase/FetchVideoListUsecaseImpl.swift b/Projects/Domains/LiveStationDomain/Sources/UseCase/FetchVideoListUsecaseImpl.swift index ab9c0931..2469846e 100644 --- a/Projects/Domains/LiveStationDomain/Sources/UseCase/FetchVideoListUsecaseImpl.swift +++ b/Projects/Domains/LiveStationDomain/Sources/UseCase/FetchVideoListUsecaseImpl.swift @@ -4,11 +4,11 @@ import LiveStationDomainInterface public struct FetchVideoListUsecaseImpl: FetchVideoListUsecase { private let repository: any LiveStationRepository - + public init(repository: any LiveStationRepository) { self.repository = repository } - + public func execute(channelID: String) -> AnyPublisher<[VideoEntity], any Error> { repository.fetchBroadcast(channelId: channelID) } diff --git a/Projects/Features/AuthFeature/Demo/Sources/AppDelegate.swift b/Projects/Features/AuthFeature/Demo/Sources/AppDelegate.swift index 4e993edc..e38243f5 100644 --- a/Projects/Features/AuthFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Features/AuthFeature/Demo/Sources/AppDelegate.swift @@ -7,8 +7,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let mockCreateChannelUsecase = MockCreateChannelUsecaseImpl() diff --git a/Projects/Features/AuthFeature/Sources/SignUpGradientView.swift b/Projects/Features/AuthFeature/Sources/SignUpGradientView.swift index 6a9b23ba..e8319d4e 100644 --- a/Projects/Features/AuthFeature/Sources/SignUpGradientView.swift +++ b/Projects/Features/AuthFeature/Sources/SignUpGradientView.swift @@ -2,45 +2,45 @@ import UIKit final class SignUpGradientView: UIView { private let gradientLayer = CAGradientLayer() - + private let colors: [[CGColor]] = [ - [CGColor(red: 31/255, green: 52/255, blue: 55/255, alpha: 1), - CGColor(red: 0/255, green: 0/255, blue: 0/255, alpha: 1)], - - [CGColor(red: 31/255, green: 52/255, blue: 55/255, alpha: 1), - CGColor(red: 22/255, green: 23/255, blue: 31/255, alpha: 1)], - - [CGColor(red: 22/255, green: 23/255, blue: 31/255, alpha: 1), - CGColor(red: 31/255, green: 52/255, blue: 55/255, alpha: 1)], - - [CGColor(red: 0/255, green: 0/255, blue: 0/255, alpha: 1), - CGColor(red: 31/255, green: 52/255, blue: 55/255, alpha: 1)] + [CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1), + CGColor(red: 0 / 255, green: 0 / 255, blue: 0 / 255, alpha: 1)], + + [CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1), + CGColor(red: 22 / 255, green: 23 / 255, blue: 31 / 255, alpha: 1)], + + [CGColor(red: 22 / 255, green: 23 / 255, blue: 31 / 255, alpha: 1), + CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1)], + + [CGColor(red: 0 / 255, green: 0 / 255, blue: 0 / 255, alpha: 1), + CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1)] ] - + override init(frame: CGRect) { super.init(frame: frame) setupGradientLayer() startDynamicColorAnimation() } - + required init?(coder: NSCoder) { super.init(coder: coder) setupGradientLayer() startDynamicColorAnimation() } - + override func layoutSubviews() { super.layoutSubviews() gradientLayer.frame = bounds } - + private func setupGradientLayer() { gradientLayer.colors = colors.first gradientLayer.startPoint = CGPoint(x: 0, y: 1) gradientLayer.endPoint = CGPoint(x: 1, y: 0) layer.insertSublayer(gradientLayer, at: 0) } - + private func startDynamicColorAnimation() { let colorAnimation = CAKeyframeAnimation(keyPath: "colors") colorAnimation.values = colors diff --git a/Projects/Features/AuthFeature/Sources/SignUpViewController.swift b/Projects/Features/AuthFeature/Sources/SignUpViewController.swift index 545fabba..b079fa39 100644 --- a/Projects/Features/AuthFeature/Sources/SignUpViewController.swift +++ b/Projects/Features/AuthFeature/Sources/SignUpViewController.swift @@ -6,53 +6,55 @@ import DesignSystem import EasyLayout import Lottie +// MARK: - SignUpViewController + public class SignUpViewController: BaseViewController { private let greetStackView = UIStackView() private let textFieldContainerView = UIView() private let signUpGradientView = SignUpGradientView() - + private let welcomeLabel = UILabel() private let greetLabel = UILabel() private let guideLabel = UILabel() private let validateLabel = UILabel() - + private let textField = UITextField() - + private let button = UIButton() - + private let input = SignUpViewModel.Input() private var cancellables = Set() - + private let confettiAnimationView = LottieAnimationView(name: "confetti", bundle: Bundle(for: DesignSystemResources.self)) private let shookAnimationView = LottieAnimationView(name: "shook", bundle: Bundle(for: DesignSystemResources.self)) private lazy var loadingView = SHLoadingView(message: "닉네임 등록 중") - - public override func viewDidAppear(_ animated: Bool) { + + override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) confettiAnimationView.play() animateViews() shookAnimationView.play() - + generateContinuousHapticFeedback() - + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in self?.textField.becomeFirstResponder() } } - - public override func viewWillAppear(_ animated: Bool) { + + override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(true, animated: false) } - public override func viewWillDisappear(_ animated: Bool) { + override public func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationController?.setNavigationBarHidden(false, animated: true) } - - public override func setupBind() { + + override public func setupBind() { let output = viewModel.transform(input: input) - + output.isValid .sink { [weak self] isValid in self?.animateTextFieldContainerView(by: isValid) @@ -60,7 +62,7 @@ public class SignUpViewController: BaseViewController { self?.animateValidateLabel(by: isValid) } .store(in: &cancellables) - + output.isSaved .receive(on: DispatchQueue.main) .sink { [weak self] isSaved in @@ -74,22 +76,22 @@ public class SignUpViewController: BaseViewController { } .store(in: &cancellables) } - - public override func setupViews() { + + override public func setupViews() { welcomeLabel.text = "환영합니다" greetLabel.text = "에 처음이시군요!" guideLabel.text = "닉네임을 입력해주세요" textField.placeholder = "닉네임" validateLabel.text = "닉네임은 2자리 이상 10자리 이하 문자, 숫자만 입력해주세요" - + greetStackView.addArrangedSubview(shookAnimationView) greetStackView.addArrangedSubview(greetLabel) - + textFieldContainerView.addSubview(textField) textField.delegate = self - + button.isEnabled = false - + view.addSubview(signUpGradientView) view.addSubview(confettiAnimationView) view.addSubview(welcomeLabel) @@ -99,89 +101,89 @@ public class SignUpViewController: BaseViewController { view.addSubview(button) view.addSubview(validateLabel) } - - public override func setupStyles() { + + override public func setupStyles() { welcomeLabel.font = .systemFont(ofSize: 32, weight: .bold) welcomeLabel.alpha = 0 - + guideLabel.alpha = 0 - + greetStackView.spacing = 4 greetStackView.alpha = 0 - + textFieldContainerView.layer.borderColor = DesignSystemColors.Color.white.cgColor textFieldContainerView.layer.borderWidth = 1 textFieldContainerView.layer.cornerRadius = 24 textFieldContainerView.alpha = 0 - + validateLabel.font = .setFont(.caption1()) validateLabel.textColor = .red validateLabel.alpha = 0 - + button.setTitle("시작하기", for: .normal) button.setTitleColor(DesignSystemAsset.Color.mainBlack.color, for: .normal) button.layer.cornerRadius = 16 button.titleLabel?.font = .setFont(.body1()) button.backgroundColor = .gray - + confettiAnimationView.contentMode = .scaleAspectFit confettiAnimationView.loopMode = .loop confettiAnimationView.animationSpeed = 0.5 - + shookAnimationView.contentMode = .scaleAspectFit shookAnimationView.loopMode = .loop shookAnimationView.animationSpeed = 0.5 } - - public override func setupLayouts() { + + override public func setupLayouts() { signUpGradientView.ezl.makeConstraint { $0.diagonal(to: view) } - + welcomeLabel.ezl.makeConstraint { $0.top(to: view.safeAreaLayoutGuide, offset: 80) .centerX(to: view) } - + confettiAnimationView.ezl.makeConstraint { $0.top(to: view, offset: -(view.frame.size.height / 3.3)) .horizontal(to: view) } - + greetStackView.ezl.makeConstraint { $0.top(to: welcomeLabel.ezl.bottom, offset: 48) .centerX(to: view) } - + guideLabel.ezl.makeConstraint { $0.top(to: greetStackView.ezl.bottom, offset: 16) .centerX(to: view) } - + textFieldContainerView.ezl.makeConstraint { $0.top(to: guideLabel.ezl.bottom, offset: 56) .horizontal(to: view, padding: 40) .height(48) } - + textField.ezl.makeConstraint { $0.vertical(to: textFieldContainerView, padding: 4) .horizontal(to: textFieldContainerView, padding: 24) } - + validateLabel.ezl.makeConstraint { $0.top(to: textFieldContainerView.ezl.bottom, offset: 8) .leading(to: textFieldContainerView, offset: 24) } - + button.ezl.makeConstraint { $0.height(56) .bottom(to: view.keyboardLayoutGuide.ezl.top, offset: -16) .horizontal(to: view, padding: 20) } } - - public override func setupActions() { + + override public func setupActions() { button.addAction(UIAction { [weak self] _ in UIImpactFeedbackGenerator(style: .light).impactOccurred() self?.input.saveUserName.send(self?.textField.text) @@ -191,78 +193,83 @@ public class SignUpViewController: BaseViewController { } } -// MARK: - TextFieldDelegate +// MARK: UITextFieldDelegate + extension SignUpViewController: UITextFieldDelegate { - public func textFieldDidChangeSelection(_ textField: UITextField) { + public func textFieldDidChangeSelection(_: UITextField) { updateTextFieldBorderColor() } - + private func updateTextFieldBorderColor() { input.didWriteUserName.send(textField.text) } } // MARK: - Animation + extension SignUpViewController { private func animateViews() { UIView.animate( withDuration: 1, delay: 0.2, - options: [.curveEaseInOut]) { [weak self] in - self?.welcomeLabel.transform = CGAffineTransform(translationX: 0, y: -5) - self?.welcomeLabel.alpha = 1 - } - + options: [.curveEaseInOut] + ) { [weak self] in + self?.welcomeLabel.transform = CGAffineTransform(translationX: 0, y: -5) + self?.welcomeLabel.alpha = 1 + } + UIView.animate( withDuration: 1, delay: 0.4, - options: [.curveEaseInOut]) { [weak self] in - self?.greetStackView.transform = CGAffineTransform(translationX: 0, y: -5) - self?.greetStackView.alpha = 1 - - self?.guideLabel.transform = CGAffineTransform(translationX: 0, y: -5) - self?.guideLabel.alpha = 1 - } - + options: [.curveEaseInOut] + ) { [weak self] in + self?.greetStackView.transform = CGAffineTransform(translationX: 0, y: -5) + self?.greetStackView.alpha = 1 + + self?.guideLabel.transform = CGAffineTransform(translationX: 0, y: -5) + self?.guideLabel.alpha = 1 + } + UIView.animate( withDuration: 1, delay: 0.6, - options: [.curveEaseInOut]) { [weak self] in - self?.textFieldContainerView.transform = CGAffineTransform(translationX: 0, y: -5) - self?.textFieldContainerView.alpha = 1 - } + options: [.curveEaseInOut] + ) { [weak self] in + self?.textFieldContainerView.transform = CGAffineTransform(translationX: 0, y: -5) + self?.textFieldContainerView.alpha = 1 + } } - + private func animateTextFieldContainerView(by isValid: Bool) { UIView.animate(withDuration: 0.5) { [weak self] in self?.textFieldContainerView.layer.borderColor = isValid ? DesignSystemAsset.Color.mainGreen.color.cgColor : DesignSystemColors.Color.white.cgColor } } - + private func animateValidateLabel(by isValid: Bool) { guard let text = textField.text else { return } - + UIView.animateKeyframes(withDuration: 0.4, delay: 0) { [weak self] in self?.validateLabel.alpha = (isValid || text.isEmpty) ? 0 : 1 - + if !isValid { UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.2) { self?.validateLabel.transform = CGAffineTransform(translationX: -2, y: 0) } - + UIView.addKeyframe(withRelativeStartTime: 0.2, relativeDuration: 0.2) { self?.validateLabel.transform = CGAffineTransform(translationX: 2, y: 0) } - + UIView.addKeyframe(withRelativeStartTime: 0.4, relativeDuration: 0.2) { self?.validateLabel.transform = .identity } } } } - + private func animateButton(by isValid: Bool) { - if !button.isEnabled && isValid { + if !button.isEnabled, isValid { UIView.animate(withDuration: 0.1) { self.button.transform = CGAffineTransform(scaleX: 0.98, y: 0.98) } completion: { _ in @@ -277,7 +284,7 @@ extension SignUpViewController { } } } - + UIView.animate(withDuration: 0.5) { [weak self] in if isValid { self?.enableButton() @@ -289,6 +296,7 @@ extension SignUpViewController { } // MARK: - ViewTransition + extension SignUpViewController { private func dismissWithAnimation() { UIView.animate( @@ -306,15 +314,16 @@ extension SignUpViewController { } // MARK: - Haptic + extension SignUpViewController { private func generateContinuousHapticFeedback() { let feedbackGenerator = UIImpactFeedbackGenerator(style: .medium) feedbackGenerator.prepare() - + var iteration = 0 let maxIterations = 6 let interval = 0.2 - + Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { timer in feedbackGenerator.impactOccurred() iteration += 1 @@ -326,6 +335,7 @@ extension SignUpViewController { } // MARK: - Loading + extension SignUpViewController { private func addLoadingView() { loadingView.backgroundColor = .black.withAlphaComponent(0.2) @@ -334,16 +344,16 @@ extension SignUpViewController { $0.diagonal(to: view) } } - + private func removeLoadingView() { loadingView.removeFromSuperview() } - + private func disableButton() { button.isEnabled = false button.backgroundColor = .gray } - + private func enableButton() { button.isEnabled = true button.backgroundColor = DesignSystemAsset.Color.mainGreen.color diff --git a/Projects/Features/AuthFeature/Sources/SignUpViewModel.swift b/Projects/Features/AuthFeature/Sources/SignUpViewModel.swift index 1097c5a3..b51d2841 100644 --- a/Projects/Features/AuthFeature/Sources/SignUpViewModel.swift +++ b/Projects/Features/AuthFeature/Sources/SignUpViewModel.swift @@ -9,16 +9,17 @@ public class SignUpViewModel: ViewModel { let didWriteUserName: PassthroughSubject = .init() let saveUserName: PassthroughSubject = .init() } + public struct Output { let isValid: PassthroughSubject = .init() let isSaved: PassthroughSubject = .init() } - + private let output = Output() private var cancellables = Set() - + private let createChannelUsecase: any CreateChannelUsecase - + public func transform(input: Input) -> Output { input.didWriteUserName .sink { [weak self] name in @@ -27,38 +28,38 @@ public class SignUpViewModel: ViewModel { } } .store(in: &cancellables) - + input.saveUserName .debounce(for: .seconds(1), scheduler: RunLoop.main) .sink { [weak self] name in self?.save(for: name) } .store(in: &cancellables) - + return output } - + public init(createChannelUsecase: CreateChannelUsecase) { self.createChannelUsecase = createChannelUsecase } - + private func validate(with name: String?) -> Bool { guard let name else { return false } return name.count >= 2 && name.count <= 10 && name.allSatisfy { $0.isLetter || $0.isNumber } } - + private func save(for name: String?) { guard let name else { return } UserDefaults.standard.set(name, forKey: "USER_NAME") - + let savedName = UserDefaults.standard.string(forKey: "USER_NAME") - + createChannelUsecase.execute(name: "Guest") .sink { _ in } receiveValue: { [weak self] channelEntity in UserDefaults.standard.set(channelEntity.id, forKey: "CHANNEL_ID") let savedID = UserDefaults.standard.string(forKey: "CHANNEL_ID") - + self?.output.isSaved.send(savedName == name && savedID != nil) } .store(in: &cancellables) diff --git a/Projects/Features/BaseFeature/Demo/Sources/AppDelegate.swift b/Projects/Features/BaseFeature/Demo/Sources/AppDelegate.swift index ef2bae04..41a1c74f 100644 --- a/Projects/Features/BaseFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Features/BaseFeature/Demo/Sources/AppDelegate.swift @@ -5,8 +5,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let viewController = UIViewController() diff --git a/Projects/Features/BaseFeature/Interface/ViewLifeCycle.swift b/Projects/Features/BaseFeature/Interface/ViewLifeCycle.swift index 8756091a..16fb0aab 100644 --- a/Projects/Features/BaseFeature/Interface/ViewLifeCycle.swift +++ b/Projects/Features/BaseFeature/Interface/ViewLifeCycle.swift @@ -21,7 +21,7 @@ public protocol ViewLifeCycle { /// 스타일을 업데이트하는 메서드입니다. /// 테마나 외관 설정이 변경되었을 때 이 메서드를 호출하여 변경 사항을 반영하세요. func updateStyles() - + /// 액션을 초기화하는 메서드입니다. /// 이 메서드에서 버튼 클릭 이벤트, 제스처, 사용자 상호작용에 대한 동작을 설정하세요. func setupActions() diff --git a/Projects/Features/BaseFeature/Interface/ViewModel.swift b/Projects/Features/BaseFeature/Interface/ViewModel.swift index 22cb8b2f..2f6cfa25 100644 --- a/Projects/Features/BaseFeature/Interface/ViewModel.swift +++ b/Projects/Features/BaseFeature/Interface/ViewModel.swift @@ -1,6 +1,6 @@ public protocol ViewModel { associatedtype Input associatedtype Output - + func transform(input: Input) -> Output } diff --git a/Projects/Features/BaseFeature/Sources/BaseCollectionViewCell.swift b/Projects/Features/BaseFeature/Sources/BaseCollectionViewCell.swift index 4f0af604..2b752866 100644 --- a/Projects/Features/BaseFeature/Sources/BaseCollectionViewCell.swift +++ b/Projects/Features/BaseFeature/Sources/BaseCollectionViewCell.swift @@ -6,16 +6,16 @@ open class BaseCollectionViewCell: UICollectionViewCell, ViewLifeCycle { public static var identifier: String { String(describing: Self.self) } - - public override init(frame: CGRect) { + + override public init(frame: CGRect) { super.init(frame: frame) setupViews() setupStyles() setupLayouts() setupActions() } - - required public init?(coder: NSCoder) { + + public required init?(coder: NSCoder) { super.init(coder: coder) setupViews() setupStyles() @@ -24,15 +24,16 @@ open class BaseCollectionViewCell: UICollectionViewCell, ViewLifeCycle { } // MARK: - View Life Cycle - open func setupViews() { } - - open func setupStyles() { } - - open func updateStyles() { } - - open func setupLayouts() { } - - open func updateLayouts() { } - - open func setupActions() { } + + open func setupViews() {} + + open func setupStyles() {} + + open func updateStyles() {} + + open func setupLayouts() {} + + open func updateLayouts() {} + + open func setupActions() {} } diff --git a/Projects/Features/BaseFeature/Sources/BaseNavigationController.swift b/Projects/Features/BaseFeature/Sources/BaseNavigationController.swift index ecc16c1e..b5561e26 100644 --- a/Projects/Features/BaseFeature/Sources/BaseNavigationController.swift +++ b/Projects/Features/BaseFeature/Sources/BaseNavigationController.swift @@ -1,7 +1,7 @@ import UIKit open class BaseNavigationController: UINavigationController { - open override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + override open var supportedInterfaceOrientations: UIInterfaceOrientationMask { topViewController?.supportedInterfaceOrientations ?? .portrait } } diff --git a/Projects/Features/BaseFeature/Sources/BaseTableViewCell.swift b/Projects/Features/BaseFeature/Sources/BaseTableViewCell.swift index 8327ab14..1794c745 100644 --- a/Projects/Features/BaseFeature/Sources/BaseTableViewCell.swift +++ b/Projects/Features/BaseFeature/Sources/BaseTableViewCell.swift @@ -6,33 +6,34 @@ open class BaseTableViewCell: UITableViewCell, ViewLifeCycle { public static var identifier: String { String(describing: Self.self) } - - public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupViews() setupStyles() setupLayouts() setupActions() } - - required public init?(coder: NSCoder) { + + public required init?(coder: NSCoder) { super.init(coder: coder) setupViews() setupStyles() setupLayouts() setupActions() } - + // MARK: - View Life Cycle - open func setupViews() { } - - open func setupLayouts() { } - - open func updateLayouts() { } - - open func setupStyles() { } - - open func updateStyles() { } - - open func setupActions() { } + + open func setupViews() {} + + open func setupLayouts() {} + + open func updateLayouts() {} + + open func setupStyles() {} + + open func updateStyles() {} + + open func setupActions() {} } diff --git a/Projects/Features/BaseFeature/Sources/BaseView.swift b/Projects/Features/BaseFeature/Sources/BaseView.swift index 895f5827..9a359871 100644 --- a/Projects/Features/BaseFeature/Sources/BaseView.swift +++ b/Projects/Features/BaseFeature/Sources/BaseView.swift @@ -10,15 +10,15 @@ open class BaseView: UIView, ViewLifeCycle { setupLayouts() setupActions() } - - public override init(frame: CGRect) { + + override public init(frame: CGRect) { super.init(frame: frame) setupViews() setupStyles() setupLayouts() setupActions() } - + public required init?(coder: NSCoder) { super.init(coder: coder) setupViews() @@ -26,17 +26,18 @@ open class BaseView: UIView, ViewLifeCycle { setupLayouts() setupActions() } - + // MARK: - View Life Cycle - open func setupViews() { } - - open func setupStyles() { } - - open func updateStyles() { } - - open func setupLayouts() { } - - open func updateLayouts() { } - - open func setupActions() { } + + open func setupViews() {} + + open func setupStyles() {} + + open func updateStyles() {} + + open func setupLayouts() {} + + open func updateLayouts() {} + + open func setupActions() {} } diff --git a/Projects/Features/BaseFeature/Sources/BaseViewController.swift b/Projects/Features/BaseFeature/Sources/BaseViewController.swift index 658be25a..5d6dfd58 100644 --- a/Projects/Features/BaseFeature/Sources/BaseViewController.swift +++ b/Projects/Features/BaseFeature/Sources/BaseViewController.swift @@ -4,17 +4,18 @@ import BaseFeatureInterface open class BaseViewController: UIViewController, ViewLifeCycle { public var viewModel: VM - + public init(viewModel: VM) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } - - required public init?(coder: NSCoder) { + + @available(*, unavailable) + public required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - - open override func viewDidLoad() { + + override open func viewDidLoad() { super.viewDidLoad() setupViews() setupStyles() @@ -22,23 +23,24 @@ open class BaseViewController: UIViewController, ViewLifeCycle { setupActions() setupBind() } - - open override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + + override open var supportedInterfaceOrientations: UIInterfaceOrientationMask { .portrait } - + // MARK: - View Life Cycle - open func setupBind() { } - - open func setupViews() { } - - open func setupStyles() { } - - open func updateStyles() { } - - open func setupLayouts() { } - - open func updateLayouts() { } - - open func setupActions() { } + + open func setupBind() {} + + open func setupViews() {} + + open func setupStyles() {} + + open func updateStyles() {} + + open func setupLayouts() {} + + open func updateLayouts() {} + + open func setupActions() {} } diff --git a/Projects/Features/LiveStreamFeature/Demo/Sources/AppDelegate.swift b/Projects/Features/LiveStreamFeature/Demo/Sources/AppDelegate.swift index 624fca31..14c66851 100644 --- a/Projects/Features/LiveStreamFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Features/LiveStreamFeature/Demo/Sources/AppDelegate.swift @@ -3,10 +3,10 @@ import UIKit @main final class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - + func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let viewController = UIViewController() diff --git a/Projects/Features/LiveStreamFeature/Sources/Chating/Models/ChatInfo.swift b/Projects/Features/LiveStreamFeature/Sources/Chating/Models/ChatInfo.swift index 60222a5d..fbdda0b2 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Chating/Models/ChatInfo.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Chating/Models/ChatInfo.swift @@ -1,19 +1,23 @@ import Foundation +// MARK: - ChatInfo + struct ChatInfo: Hashable { let id = UUID() let owner: ChatOwner let message: String } +// MARK: - ChatOwner + enum ChatOwner: Hashable { case user(name: String) case system - + var name: String { switch self { - case let .user(name): return name - case .system: return "System" + case let .user(name): name + case .system: "System" } } } diff --git a/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChatEmptyView.swift b/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChatEmptyView.swift index ae1f24b1..26bfb137 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChatEmptyView.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChatEmptyView.swift @@ -8,35 +8,35 @@ final class ChatEmptyView: BaseView { private let imageView = UIImageView() private let titleLabel = UILabel() private let subtitleLabel = UILabel() - + private lazy var textStackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]) private lazy var stackView = UIStackView(arrangedSubviews: [imageView, textStackView]) - + override func setupViews() { addSubview(stackView) - + imageView.image = DesignSystemAsset.Image.chat48.image - + titleLabel.text = "여기는 지금 조용하네요!" - + subtitleLabel.text = "첫 댓글로 스트리머와 소통을 시작해보세요!" } - - override func setupStyles() { + + override func setupStyles() { titleLabel.font = .setFont(.body1()) - + subtitleLabel.textColor = .gray subtitleLabel.font = .setFont(.caption1()) - + stackView.axis = .vertical stackView.spacing = 13 stackView.alignment = .center - + textStackView.axis = .vertical textStackView.spacing = 11 textStackView.alignment = .center } - + override func setupLayouts() { stackView.ezl.makeConstraint { $0.center(to: self) diff --git a/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChatInputField.swift b/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChatInputField.swift index 0d3ba13b..2a309882 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChatInputField.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChatInputField.swift @@ -5,32 +5,34 @@ import BaseFeature import DesignSystem import EasyLayout +// MARK: - ChatInputField + final class ChatInputField: BaseView { private let heartButton = UIButton() private let inputField = UITextView() private let sendButton = UIButton() private let placeholder = UILabel() - + private let clipView = UIView() - + private var inputFieldHeightConstraint: NSLayoutConstraint! private let heartLayer = CAEmitterLayer() private var heartEmitterCell = CAEmitterCell() - + @Published var sendButtonDidTapPublisher: ChatInfo? override func setupViews() { addSubview(heartButton) addSubview(clipView) - + clipView.addSubview(inputField) clipView.addSubview(sendButton) - + heartLayer.emitterCells = [heartEmitterCell] - + heartButton.setImage(DesignSystemAsset.Image.heart24.image, for: .normal) heartButton.setContentHuggingPriority(.required, for: .horizontal) - + heartEmitterCell.contents = DesignSystemAsset.Image.heart24.image.cgImage heartEmitterCell.lifetime = 4 heartEmitterCell.birthRate = 1 @@ -41,11 +43,11 @@ final class ChatInputField: BaseView { heartEmitterCell.velocity = 50 /// 1초 기준 속도 heartEmitterCell.velocityRange = 30 heartEmitterCell.yAcceleration = -50 - + inputField.addSubview(placeholder) inputField.textContainerInset = .zero inputField.delegate = self - + placeholder.text = "재미있는 이야기를 시작해 보세요!" sendButton.setContentHuggingPriority(.required, for: .horizontal) @@ -59,68 +61,68 @@ final class ChatInputField: BaseView { ) sendButton.setContentCompressionResistancePriority(.required, for: .horizontal) } - + override func setupStyles() { backgroundColor = DesignSystemAsset.Color.darkGray.color - + clipView.layer.cornerRadius = 20 clipView.clipsToBounds = true clipView.layer.borderWidth = 1 clipView.layer.borderColor = UIColor.white.cgColor - + inputField.font = .setFont(.body2()) inputField.backgroundColor = .clear inputField.textColor = .white inputField.autocorrectionType = .no inputField.spellCheckingType = .no - + placeholder.font = .setFont(.body2()) placeholder.textColor = DesignSystemAsset.Color.gray.color placeholder.alpha = 0.5 - + sendButton.isEnabled = false } - + override func setupLayouts() { heartButton.ezl.makeConstraint { $0.leading(to: self, offset: 20) .bottom(to: self, offset: -16) } - + clipView.ezl.makeConstraint { $0.vertical(to: self, padding: 10) .leading(to: heartButton.ezl.trailing, offset: 10) .trailing(to: self, offset: -20) .height(min: 40) } - + inputField.ezl.makeConstraint { $0.vertical(to: clipView, padding: 10) .leading(to: clipView, offset: 16) .trailing(to: sendButton.ezl.leading, offset: -12) .height(max: 100) } - + inputFieldHeightConstraint = inputField.heightAnchor.constraint(equalToConstant: 20) inputFieldHeightConstraint.priority = .defaultLow inputFieldHeightConstraint.isActive = true - + placeholder.ezl.makeConstraint { $0.top(to: inputField) .leading(to: inputField, offset: 5) } - + sendButton.ezl.makeConstraint { $0.trailing(to: clipView, offset: -15) .bottom(to: clipView, offset: -8) } } - + override func setupActions() { sendButton.addAction( UIAction { [weak self] _ in guard let self, - let userName = UserDefaults.standard.string(forKey: "USER_NAME") else { return } + let userName = UserDefaults.standard.string(forKey: "USER_NAME") else { return } sendButtonDidTapPublisher = ChatInfo( owner: .user(name: userName), message: inputField.text @@ -132,22 +134,22 @@ final class ChatInputField: BaseView { ) heartButton.addTarget(self, action: #selector(didTapHeartButton), for: .touchUpInside) } - + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if heartButton.bounds.insetBy(dx: -30, dy: -30).contains(point) { return heartButton } return super.hitTest(point, with: event) } - + @objc private func didTapHeartButton() { let generator = UIImpactFeedbackGenerator(style: .light) generator.impactOccurred() - + heartLayer.emitterPosition = CGPoint(x: heartButton.frame.midX, y: heartButton.frame.midY) heartLayer.birthRate = 1 - + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { [weak self] in self?.heartLayer.birthRate = 0 } @@ -155,6 +157,8 @@ final class ChatInputField: BaseView { } } +// MARK: UITextViewDelegate + extension ChatInputField: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { if textView.text.isEmpty { @@ -166,9 +170,9 @@ extension ChatInputField: UITextViewDelegate { sendButton.isEnabled = true placeholder.isHidden = true } - + inputFieldHeightConstraint.constant = textView.contentSize.height - + layoutIfNeeded() } } diff --git a/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChattingCell.swift b/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChattingCell.swift index 9545db35..ebb20a82 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChattingCell.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChattingCell.swift @@ -4,39 +4,39 @@ import BaseFeature import DesignSystem import EasyLayout -final class ChattingCell: BaseTableViewCell { +final class ChattingCell: BaseTableViewCell { private let nameLabel = UILabel() private let detailLabel = UILabel() - + override func setupViews() { contentView.addSubview(nameLabel) contentView.addSubview(detailLabel) - - nameLabel.setContentHuggingPriority(.required, for: .horizontal) + + nameLabel.setContentHuggingPriority(.required, for: .horizontal) } - + override func setupStyles() { backgroundColor = .clear - + nameLabel.font = .setFont(.caption1(weight: .bold)) detailLabel.font = .setFont(.caption1()) detailLabel.numberOfLines = 0 } - + override func setupLayouts() { nameLabel.ezl.makeConstraint { $0.leading(to: contentView, offset: 20) .vertical(to: contentView, padding: 6) } - + detailLabel.ezl.makeConstraint { $0.leading(to: nameLabel.ezl.trailing, offset: 15) .vertical(to: contentView, padding: 6) .trailing(to: contentView, offset: -20) } } - + func configure(chat: ChatInfo) { nameLabel.text = chat.owner.name detailLabel.text = chat.message diff --git a/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChattingListView.swift b/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChattingListView.swift index 67d6af16..e1fbfc78 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChattingListView.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChattingListView.swift @@ -5,10 +5,14 @@ import BaseFeature import DesignSystem import EasyLayout +// MARK: - ChatInputFieldAction + protocol ChatInputFieldAction { var sendButtonDidTap: AnyPublisher { get } } +// MARK: - ChattingListView + final class ChattingListView: BaseView { private let chattingContainerView = UIView() private let titleLabel = UILabel() @@ -16,13 +20,13 @@ final class ChattingListView: BaseView { private let chatEmptyView = ChatEmptyView() private let chatInputField = ChatInputField() private let recentChatButton = UIButton() - + @Published private var isScrollFixed = true private var isAnimating = false - + private var recentChatButtonShowConstraints: [NSLayoutConstraint] = [] private var recentChatButtonHideConstraints: [NSLayoutConstraint] = [] - + private var subscription = Set() private lazy var dataSource = UITableViewDiffableDataSource( @@ -34,95 +38,95 @@ final class ChattingListView: BaseView { withIdentifier: ChattingCell.identifier, for: indexPath ) as? ChattingCell ?? ChattingCell() - + cell.configure(chat: chatInfo) return cell - + case .system: let cell = tableView.dequeueReusableCell( withIdentifier: SystemAlarmCell.identifier, for: indexPath ) as? SystemAlarmCell ?? SystemAlarmCell() - + cell.configure(content: chatInfo.message) return cell } } - + override func setupViews() { addSubview(chattingContainerView) chattingContainerView.addSubview(titleLabel) chattingContainerView.addSubview(chatListView) addSubview(recentChatButton) addSubview(chatInputField) - + titleLabel.text = "실시간 채팅" - + chatListView.delegate = self chatListView.register(ChattingCell.self, forCellReuseIdentifier: ChattingCell.identifier) chatListView.register(SystemAlarmCell.self, forCellReuseIdentifier: SystemAlarmCell.identifier) chatListView.backgroundView = chatEmptyView } - + override func setupStyles() { titleLabel.font = .setFont(.body1()) - + chatListView.backgroundColor = .clear chatListView.allowsSelection = false chatListView.separatorStyle = .none - + var configure = UIButton.Configuration.filled() configure.title = "최근 채팅으로 이동" configure.baseBackgroundColor = DesignSystemAsset.Color.mainGreen.color configure.baseForegroundColor = .white recentChatButton.configuration = configure } - + override func setupLayouts() { chattingContainerView.ezl.makeConstraint { $0.horizontal(to: self) .top(to: self) .bottom(to: chatInputField.ezl.top) } - + titleLabel.ezl.makeConstraint { $0.top(to: chattingContainerView, offset: 24) .leading(to: chattingContainerView, offset: 20) } - + chatListView.ezl.makeConstraint { $0.horizontal(to: chattingContainerView) .top(to: titleLabel.ezl.bottom, offset: 21) .bottom(to: chattingContainerView) } - + chatInputField.ezl.makeConstraint { $0.horizontal(to: self) .bottom(to: self) } - + recentChatButton.translatesAutoresizingMaskIntoConstraints = false recentChatButtonShowConstraints = [ - recentChatButton.centerXAnchor.constraint(equalTo: self.centerXAnchor), + recentChatButton.centerXAnchor.constraint(equalTo: centerXAnchor), recentChatButton.bottomAnchor.constraint(equalTo: chatInputField.topAnchor, constant: -8) ] recentChatButtonHideConstraints = [ - recentChatButton.centerXAnchor.constraint(equalTo: self.centerXAnchor), + recentChatButton.centerXAnchor.constraint(equalTo: centerXAnchor), recentChatButton.bottomAnchor.constraint(equalTo: chatInputField.bottomAnchor, constant: 0) ] NSLayoutConstraint.activate(recentChatButtonHideConstraints) } - + override func setupActions() { - let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) chattingContainerView.addGestureRecognizer(tapGesture) - + $isScrollFixed .sink { [weak self] in self?.updateRecentChatButtonConstraint(isHidden: $0) } .store(in: &subscription) - + recentChatButton.addAction( UIAction { [weak self] _ in self?.scrollToBottom() @@ -130,26 +134,26 @@ final class ChattingListView: BaseView { }, for: .touchUpInside ) - + sendButtonDidTap .sink { [weak self] _ in self?.isScrollFixed = true } .store(in: &subscription) } - + private func lastIndexPath() -> IndexPath? { let lastRowIndex = chatListView.numberOfRows(inSection: 0) - 1 guard lastRowIndex >= 0 else { return nil } return IndexPath(row: lastRowIndex, section: 0) } - + private func scrollToBottom() { guard let indexPath = lastIndexPath() else { return } isAnimating = true chatListView.scrollToRow(at: indexPath, at: .bottom, animated: true) } - + private func updateRecentChatButtonConstraint(isHidden: Bool) { UIView.animate(withDuration: 0.2) { if isHidden { @@ -162,7 +166,7 @@ final class ChattingListView: BaseView { self.layoutIfNeeded() } } - + @objc private func dismissKeyboard() { endEditing(true) @@ -172,31 +176,35 @@ final class ChattingListView: BaseView { extension ChattingListView { func updateList(_ chatList: [ChatInfo]) { chatEmptyView.isHidden = !chatList.isEmpty - + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([0]) snapshot.appendItems(chatList) - self.dataSource.apply(snapshot, animatingDifferences: false) + dataSource.apply(snapshot, animatingDifferences: false) if isScrollFixed { scrollToBottom() } } } +// MARK: ChatInputFieldAction + extension ChattingListView: ChatInputFieldAction { var sendButtonDidTap: AnyPublisher { chatInputField.$sendButtonDidTapPublisher.dropFirst().eraseToAnyPublisher() } } +// MARK: UITableViewDelegate + extension ChattingListView: UITableViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { guard !isAnimating else { return } let offsetMaxY = scrollView.contentSize.height - scrollView.bounds.height - isScrollFixed = (offsetMaxY - 50...offsetMaxY) ~= scrollView.contentOffset.y || offsetMaxY < scrollView.contentOffset.y + isScrollFixed = (offsetMaxY - 50 ... offsetMaxY) ~= scrollView.contentOffset.y || offsetMaxY < scrollView.contentOffset.y } - - func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { + + func scrollViewDidEndScrollingAnimation(_: UIScrollView) { isAnimating = false } } diff --git a/Projects/Features/LiveStreamFeature/Sources/Chating/Views/SystemAlarmCell.swift b/Projects/Features/LiveStreamFeature/Sources/Chating/Views/SystemAlarmCell.swift index dd42dffc..656a464f 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Chating/Views/SystemAlarmCell.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Chating/Views/SystemAlarmCell.swift @@ -6,25 +6,25 @@ import EasyLayout final class SystemAlarmCell: BaseTableViewCell { private let contentLabel = UILabel() - + override func setupViews() { contentView.addSubview(contentLabel) } - + override func setupStyles() { backgroundColor = .clear - + contentLabel.textAlignment = .center contentLabel.textColor = DesignSystemAsset.Color.gray.color contentLabel.font = .setFont(.caption1()) } - + override func setupLayouts() { contentLabel.ezl.makeConstraint { $0.diagonal(to: contentView) } } - + func configure(content: String) { contentLabel.text = content } diff --git a/Projects/Features/LiveStreamFeature/Sources/Factory/LiveStreamViewControllerFactoryImpl.swift b/Projects/Features/LiveStreamFeature/Sources/Factory/LiveStreamViewControllerFactoryImpl.swift index 90a21009..b81886ea 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Factory/LiveStreamViewControllerFactoryImpl.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Factory/LiveStreamViewControllerFactoryImpl.swift @@ -6,13 +6,13 @@ import LiveStreamFeatureInterface public struct LiveStreamViewControllerFactoryImpl: LiveStreamViewControllerFactory { private let fetchBroadcastUseCase: any FetchVideoListUsecase - + public init( fetchBroadcastUseCase: any FetchVideoListUsecase ) { self.fetchBroadcastUseCase = fetchBroadcastUseCase } - + public func make(channelID: String, title: String, owner: String, description: String) -> UIViewController { let viewModel = LiveStreamViewModel(channelID: channelID, fetchVideoListUsecase: fetchBroadcastUseCase) return LiveStreamViewController(title: title, owner: owner, description: description, viewModel: viewModel) diff --git a/Projects/Features/LiveStreamFeature/Sources/Player/ViewControllers/LiveStreamViewController.swift b/Projects/Features/LiveStreamFeature/Sources/Player/ViewControllers/LiveStreamViewController.swift index c6e8254f..57c525d2 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Player/ViewControllers/LiveStreamViewController.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Player/ViewControllers/LiveStreamViewController.swift @@ -5,23 +5,25 @@ import BaseFeature import DesignSystem import EasyLayout +// MARK: - LiveStreamViewController + public final class LiveStreamViewController: BaseViewController { private let chattingList = ChattingListView() - private let playerView: ShookPlayerView = ShookPlayerView() - private let infoView: LiveStreamInfoView = LiveStreamInfoView() + private let playerView: ShookPlayerView = .init() + private let infoView: LiveStreamInfoView = .init() private let bottomGuideView = UIView() private let _title: String private let _owner: String private let _description: String - + private var shrinkConstraints: [NSLayoutConstraint] = [] private var expandConstraints: [NSLayoutConstraint] = [] private var unfoldedConstraint: NSLayoutConstraint? private var foldedConstraint: NSLayoutConstraint? private var subscription = Set() - + private let viewDidLoadPublisher = PassthroughSubject() - + private lazy var input = LiveStreamViewModel.Input( expandButtonDidTap: playerView.playerControlView.expandButtonDidTap.eraseToAnyPublisher(), sliderValueDidChange: playerView.playerControlView.timeControlView.valueDidChanged.eraseToAnyPublisher(), @@ -32,36 +34,35 @@ public final class LiveStreamViewController: BaseViewController 0 { let scale = max(1 - translation.y / dragScalingFactor, minViewScale) view.transform = CGAffineTransform(scaleX: scale, y: scale) view.layer.cornerRadius = min(translation.y, maxDraggingCornerRadius) - + if translation.y > 72 { dismiss(animated: true) } } - - case .ended, .cancelled: + + case .cancelled, .ended: UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut) { self.view.transform = .identity self.view.layer.cornerRadius = 0 } - + default: break } } diff --git a/Projects/Features/LiveStreamFeature/Sources/Player/ViewModels/LiveStreamViewModel.swift b/Projects/Features/LiveStreamFeature/Sources/Player/ViewModels/LiveStreamViewModel.swift index af506cc5..1ee68f74 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Player/ViewModels/LiveStreamViewModel.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Player/ViewModels/LiveStreamViewModel.swift @@ -5,14 +5,15 @@ import BaseFeatureInterface import ChatSoketModule import LiveStationDomainInterface +// MARK: - LiveStreamViewModel + public final class LiveStreamViewModel: ViewModel { - private var subscription = Set() - + private let chattingSocket: WebSocket private let channelID: String private let fetchVideoListUsecase: any FetchVideoListUsecase - + public struct Input { let expandButtonDidTap: AnyPublisher let sliderValueDidChange: AnyPublisher @@ -24,7 +25,7 @@ public final class LiveStreamViewModel: ViewModel { let autoDissmissDidRegister: PassthroughSubject = .init() let viewDidLoad: AnyPublisher } - + public struct Output { let isExpanded: CurrentValueSubject = .init(false) let isPlaying: CurrentValueSubject = .init(false) @@ -36,7 +37,7 @@ public final class LiveStreamViewModel: ViewModel { let videoURLString: PassthroughSubject = .init() let showAlert: PassthroughSubject = .init() } - + public init( channelID: String, chattingSocket: WebSocket = .shared, @@ -46,14 +47,14 @@ public final class LiveStreamViewModel: ViewModel { self.fetchVideoListUsecase = fetchVideoListUsecase self.chattingSocket = chattingSocket } - + deinit { chattingSocket.closeWebSocket() } - + public func transform(input: Input) -> Output { let output = Output() - + input.expandButtonDidTap .compactMap { $0 } .sink { @@ -63,7 +64,7 @@ public final class LiveStreamViewModel: ViewModel { output.isShowedInfoView.send(false) } .store(in: &subscription) - + input.sliderValueDidChange .compactMap { $0 } .map { Double($0) } @@ -72,24 +73,24 @@ public final class LiveStreamViewModel: ViewModel { output.time.send($0) } .store(in: &subscription) - + input.playerStateDidChange .compactMap { $0 } .sink { flag in output.isPlaying.send(flag) } .store(in: &subscription) - + input.playerGestureDidTap .compactMap { $0 } .sink { _ in let nextValue1 = !output.isShowedPlayerControl.value output.isShowedPlayerControl.send(nextValue1) - + if nextValue1 { input.autoDissmissDidRegister.send() } - + if output.isExpanded.value { output.isShowedInfoView.send(false) } else { @@ -97,7 +98,7 @@ public final class LiveStreamViewModel: ViewModel { } } .store(in: &subscription) - + input.playButtonDidTap .compactMap { $0 } .sink { _ in @@ -105,13 +106,13 @@ public final class LiveStreamViewModel: ViewModel { output.isPlaying.send(!output.isPlaying.value) } .store(in: &subscription) - + input.dismissButtonDidTap .sink { _ in output.dismiss.send() } .store(in: &subscription) - + input.viewDidLoad .sink { [weak self] _ in guard let self else { return } @@ -121,7 +122,7 @@ public final class LiveStreamViewModel: ViewModel { receciveChatMessage(output: output) } .store(in: &subscription) - + input.chattingSendButtonDidTap .sink { [weak self] chatInfo in guard let chatInfo, @@ -136,7 +137,7 @@ public final class LiveStreamViewModel: ViewModel { ) } .store(in: &subscription) - + input.autoDissmissDidRegister .debounce(for: .seconds(3), scheduler: DispatchQueue.main) .sink { _ in @@ -144,10 +145,10 @@ public final class LiveStreamViewModel: ViewModel { output.isShowedInfoView.send(false) } .store(in: &subscription) - + return output } - + private func fetchVideoData(output: Output, channelID: String) { fetchVideoListUsecase.execute(channelID: channelID) .sink( @@ -155,7 +156,7 @@ public final class LiveStreamViewModel: ViewModel { switch commpletion { case .failure: output.showAlert.send(()) - + case .finished: break } @@ -167,13 +168,14 @@ public final class LiveStreamViewModel: ViewModel { } else if let lowResolution = entityList.first?.videoURLString { return output.videoURLString.send(lowResolution) } - }) + } + ) .store(in: &subscription) } } private extension LiveStreamViewModel { - func openChattingSocket(output: Output) { + func openChattingSocket(output _: Output) { do { try chattingSocket.openWebSocket() } catch { @@ -186,7 +188,7 @@ private extension LiveStreamViewModel { ) } } - + func sendEntryMessage() { guard let userName = UserDefaults.standard.string(forKey: "USER_NAME") else { return } chattingSocket.send( @@ -198,7 +200,7 @@ private extension LiveStreamViewModel { ) ) } - + func receciveChatMessage(output: Output) { chattingSocket.receive { chatMessage in guard let chatMessage else { return } diff --git a/Projects/Features/LiveStreamFeature/Sources/Player/Views/LiveStreamInfoView.swift b/Projects/Features/LiveStreamFeature/Sources/Player/Views/LiveStreamInfoView.swift index 1a2916f4..4a195a3b 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Player/Views/LiveStreamInfoView.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Player/Views/LiveStreamInfoView.swift @@ -4,10 +4,12 @@ import BaseFeature import DesignSystem import EasyLayout +// MARK: - LiveStreamInfoView + final class LiveStreamInfoView: BaseView { private let titleLabel = UILabel() private let descriptionLabel = UILabel() - + private lazy var stackView: UIStackView = { let stackView = UIStackView() stackView.addArrangedSubview(titleLabel) @@ -17,23 +19,23 @@ final class LiveStreamInfoView: BaseView { stackView.spacing = 10 return stackView }() - + override func setupViews() { - self.addSubview(stackView) - self.backgroundColor = DesignSystemAsset.Color.darkGray.color + addSubview(stackView) + backgroundColor = DesignSystemAsset.Color.darkGray.color } - + override func setupLayouts() { stackView.ezl.makeConstraint { $0.diagonal(to: self, padding: 20) } } - + override func setupStyles() { titleLabel.font = .setFont(.body2()) titleLabel.numberOfLines = 0 titleLabel.textColor = .white - + descriptionLabel.font = .setFont(.caption1()) descriptionLabel.textColor = .white descriptionLabel.numberOfLines = 1 diff --git a/Projects/Features/LiveStreamFeature/Sources/Player/Views/PlayerControlView.swift b/Projects/Features/LiveStreamFeature/Sources/Player/Views/PlayerControlView.swift index 5fccfceb..c9c4af42 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Player/Views/PlayerControlView.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Player/Views/PlayerControlView.swift @@ -5,109 +5,115 @@ import UIKit import BaseFeature import DesignSystem +// MARK: - PlayerControlViewAction + public protocol PlayerControlViewAction { var playButtonDidTap: AnyPublisher { get } var expandButtonDidTap: AnyPublisher { get } var dismissButtonDidTap: AnyPublisher { get } } +// MARK: - ImageConstants + private enum ImageConstants { case play case pause case zoomIn case zoomOut case dismiss - + var image: UIImage { switch self { case .zoomIn: - return DesignSystemAsset.Image.zoomIn24.image - + DesignSystemAsset.Image.zoomIn24.image + case .zoomOut: - return DesignSystemAsset.Image.zoomOut24.image - + DesignSystemAsset.Image.zoomOut24.image + case .play: - return DesignSystemAsset.Image.play48.image - + DesignSystemAsset.Image.play48.image + case .pause: - return DesignSystemAsset.Image.pause48.image - + DesignSystemAsset.Image.pause48.image + case .dismiss: - return DesignSystemAsset.Image.chevronDown24.image + DesignSystemAsset.Image.chevronDown24.image } } } +// MARK: - PlayerControlView + final class PlayerControlView: BaseView { - private let playButton: UIButton = UIButton() - private let expandButton: UIButton = UIButton() - private let dismissButton: UIButton = UIButton() - var timeControlView: TimeControlView = TimeControlView() - + private let playButton: UIButton = .init() + private let expandButton: UIButton = .init() + private let dismissButton: UIButton = .init() + var timeControlView: TimeControlView = .init() + @Published private var playButtonTapPublisher: Void? @Published private var sliderValuePublisher: Double? @Published private var expandButtonTapPublisher: Void? @Published private var dismissButtonTapPublisher: Void? - + override func setupViews() { - self.addSubview(playButton) - self.addSubview(expandButton) - self.addSubview(timeControlView) - self.addSubview(dismissButton) + addSubview(playButton) + addSubview(expandButton) + addSubview(timeControlView) + addSubview(dismissButton) } - + override func setupLayouts() { playButton.ezl.makeConstraint { $0.center(to: self) } - + timeControlView.ezl.makeConstraint { $0.height(10) .horizontal(to: self, padding: 15) .bottom(to: self, offset: -20) } - + expandButton.ezl.makeConstraint { $0.trailing(to: self, offset: -13) .top(to: self, offset: 16) } - + dismissButton.ezl.makeConstraint { $0.leading(to: self, offset: 13) .top(to: self, offset: 16) } } - + override func setupStyles() { - self.backgroundColor = .black.withAlphaComponent(0.5) - + backgroundColor = .black.withAlphaComponent(0.5) + var playButtonConfig = UIButton.Configuration.plain() playButtonConfig.image = ImageConstants.play.image playButton.configuration = playButtonConfig - + var expandButtonConfig = UIButton.Configuration.plain() expandButtonConfig.image = ImageConstants.zoomIn.image expandButton.configuration = expandButtonConfig - + var dismissButtonConfig = UIButton.Configuration.plain() dismissButtonConfig.image = ImageConstants.dismiss.image dismissButton.configuration = dismissButtonConfig } - + override func setupActions() { playButton.addAction(UIAction { [weak self] _ in guard let self else { return } - self.playButtonTapPublisher = () + playButtonTapPublisher = () }, for: .touchUpInside) - + expandButton.addAction(UIAction { [weak self] _ in guard let self else { return } - self.expandButtonTapPublisher = () + expandButtonTapPublisher = () }, for: .touchUpInside) - + dismissButton.addAction(UIAction { [weak self] _ in guard let self else { return } - self.dismissButtonTapPublisher = () + dismissButtonTapPublisher = () }, for: .touchUpInside) } } @@ -117,34 +123,37 @@ extension PlayerControlView { expandButton.configuration?.image = expanded ? ImageConstants.zoomOut.image : ImageConstants.zoomIn.image dismissButton.configuration?.image = expanded ? nil : ImageConstants.dismiss.image } - + func togglePlayerButtonAnimation(_ isPlaying: Bool) { playButton.transform = CGAffineTransform(scaleX: .zero, y: .zero) - + UIView.animate(withDuration: 0.7, delay: .zero, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.4, - options: .allowUserInteraction) { + options: .allowUserInteraction) + { if isPlaying { self.playButton.configuration?.image = ImageConstants.pause.image } else { self.playButton.configuration?.image = ImageConstants.play.image } - self.playButton.transform = .identity + self.playButton.transform = .identity } } } +// MARK: PlayerControlViewAction + extension PlayerControlView: PlayerControlViewAction { var expandButtonDidTap: AnyPublisher { $expandButtonTapPublisher.eraseToAnyPublisher() } - + var playButtonDidTap: AnyPublisher { $playButtonTapPublisher.eraseToAnyPublisher() } - + var dismissButtonDidTap: AnyPublisher { $dismissButtonTapPublisher.eraseToAnyPublisher() } diff --git a/Projects/Features/LiveStreamFeature/Sources/Player/Views/ShookPlayerView.swift b/Projects/Features/LiveStreamFeature/Sources/Player/Views/ShookPlayerView.swift index 8cc7486c..80a13181 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Player/Views/ShookPlayerView.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Player/Views/ShookPlayerView.swift @@ -6,61 +6,74 @@ import BaseFeature import DesignSystem import EasyLayout +// MARK: - ShhokPlayerViewState + protocol ShhokPlayerViewState { func updataePlayState(_ isPlaying: Bool) } +// MARK: - ShookPlayerViewAciton + protocol ShookPlayerViewAciton { var playerStateDidChange: AnyPublisher { get } var playerGestureDidTap: AnyPublisher { get } } +// MARK: - Constants + private enum Constants: CGFloat { - case indicatorSize = 50 + case indicatorSize = 50 } +// MARK: - BufferStateConstants + private enum BufferStateConstants: String { case playbackBufferEmpty case playbackLikelyToKeepUp case playbackBufferFull } +// MARK: - ShookPlayerView + final class ShookPlayerView: BaseView { - private let player: AVPlayer = AVPlayer() + private let player: AVPlayer = .init() private var playerItem: AVPlayerItem? - - private let indicatorView: UIActivityIndicatorView = UIActivityIndicatorView() + + private let indicatorView: UIActivityIndicatorView = .init() private var timeObserverToken: Any? private var subscription: Set = .init() private var isRegisterdObsever: Bool = false - + // MARK: - @Published + @Published private var playingStateChangedPublisher: Bool? @Published private var playerGestureTapPublisher: Void? - + // MARK: - lazy var + private lazy var playerLayer: AVPlayerLayer = { let layer = AVPlayerLayer(player: player) layer.videoGravity = .resizeAspectFill return layer }() + private lazy var videoContainerView: UIView = { let view = UIView() view.layer.addSublayer(playerLayer) return view }() - - public let playerControlView: PlayerControlView = PlayerControlView() - + + public let playerControlView: PlayerControlView = .init() + override init() { super.init(frame: .zero) } - + func stopPlayback() { player.pause() player.replaceCurrentItem(with: nil) } - + func fetchVideo(m3u8URL: URL) { playerItem = AVPlayerItem(url: m3u8URL) player.replaceCurrentItem(with: playerItem) @@ -68,11 +81,12 @@ final class ShookPlayerView: BaseView { player.play() isRegisterdObsever = true } - - required init?(coder: NSCoder) { + + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - + deinit { if isRegisterdObsever { removeObserver() @@ -80,11 +94,11 @@ final class ShookPlayerView: BaseView { } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { - guard let keyPath = keyPath else { + guard let keyPath else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) return } - + if let playerItem = object as? AVPlayerItem { handlePlayItemStatus(playerItem.status) hanldePlayItemBufferString(keyPath) @@ -92,43 +106,43 @@ final class ShookPlayerView: BaseView { handlePlayerTimeControlStatus(player.timeControlStatus) } } - + override func setupViews() { super.setupViews() - self.addSubview(videoContainerView) + addSubview(videoContainerView) videoContainerView.addSubview(playerControlView) videoContainerView.addSubview(indicatorView) } - + override func setupLayouts() { super.setupLayouts() videoContainerView.ezl.makeConstraint { $0.diagonal(to: self) } - + indicatorView.ezl.makeConstraint { $0.width(Constants.indicatorSize.rawValue).height(Constants.indicatorSize.rawValue).center(to: self) } - + playerControlView.ezl.makeConstraint { $0.diagonal(to: self) } } - + override func setupStyles() { backgroundColor = .systemBackground - + playerControlView.alpha = .zero - + indicatorView.color = DesignSystemAsset.Color.mainGreen.color indicatorView.hidesWhenStopped = true } - + override func setupActions() { - let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(toggleControlPannel)) + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toggleControlPannel)) videoContainerView.addGestureRecognizer(tapGesture) } - + override func layoutSubviews() { super.layoutSubviews() playerLayer.frame = videoContainerView.bounds @@ -137,94 +151,97 @@ final class ShookPlayerView: BaseView { extension ShookPlayerView { // MARK: - register / remove observer + private func addObserver() { addObserverPlayerItem() addObserverPlayer() } - + private func addObserverPlayerItem() { playerItem?.addObserver(self, - forKeyPath: #keyPath(AVPlayerItem.status), - options: [.old, .new], - context: nil) // 동일한 객체를 여러 키 경로에서 관찰할 때 구분하기 위한 식별자 - + forKeyPath: #keyPath(AVPlayerItem.status), + options: [.old, .new], + context: nil) // 동일한 객체를 여러 키 경로에서 관찰할 때 구분하기 위한 식별자 + playerItem?.addObserver(self, forKeyPath: BufferStateConstants.playbackBufferEmpty.rawValue, options: .new, context: nil) playerItem?.addObserver(self, forKeyPath: BufferStateConstants.playbackLikelyToKeepUp.rawValue, options: .new, context: nil) playerItem?.addObserver(self, forKeyPath: BufferStateConstants.playbackBufferFull.rawValue, options: .new, context: nil) } - + private func addObserverPlayer() { player.addObserver(self, forKeyPath: "timeControlStatus", context: nil) - + let interval = CMTimeMakeWithSeconds(1, preferredTimescale: Int32(NSEC_PER_SEC)) - + timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] cmtTime in guard let self else { return } let floatSecond = CMTimeGetSeconds(cmtTime) playerControlView.timeControlView.updateSlider(to: Float(floatSecond)) } } - + private func removeObserver() { if let token = timeObserverToken { player.removeTimeObserver(token) timeObserverToken = nil } - + player.removeObserver(self, forKeyPath: "timeControlStatus") - + playerItem?.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.status)) playerItem?.removeObserver(self, forKeyPath: BufferStateConstants.playbackBufferEmpty.rawValue) playerItem?.removeObserver(self, forKeyPath: BufferStateConstants.playbackLikelyToKeepUp.rawValue) playerItem?.removeObserver(self, forKeyPath: BufferStateConstants.playbackBufferFull.rawValue) } - + // MARK: - observeValue Handler + private func handlePlayItemStatus(_ status: AVPlayerItem.Status) { switch status { case .readyToPlay: // 성공 guard let item = player.currentItem else { return } - + let seekableDuration = item.seekableTimeRanges.last?.timeRangeValue.end.seconds ?? 0.0 playerControlView.timeControlView.maxValue = Float(seekableDuration) // HLS 사용 시 seek 가능한 영역 갱신 - case.failed, .unknown: + case .failed, .unknown: break - + @unknown default: break } } - + private func hanldePlayItemBufferString(_ bufferString: String) { switch bufferString { case "playbackBufferEmpty": indicatorView.startAnimating() - + default: indicatorView.stopAnimating() } } - + private func handlePlayerTimeControlStatus(_ status: AVPlayer.TimeControlStatus) { switch status { case .playing: playingStateChangedPublisher = true indicatorView.stopAnimating() - - case.paused: + + case .paused: playingStateChangedPublisher = false - + case .waitingToPlayAtSpecifiedRate: break - + @unknown default: break } } - + // MARK: - @objc + @objc func toggleControlPannel() { playerGestureTapPublisher = () } @@ -232,9 +249,9 @@ extension ShookPlayerView { extension ShookPlayerView { func seek(to newValue: Double) { - self.player.seek(to: CMTime(seconds: newValue, preferredTimescale: Int32(NSEC_PER_SEC))) + player.seek(to: CMTime(seconds: newValue, preferredTimescale: Int32(NSEC_PER_SEC))) } - + // - MARK: animation func playerControlViewAlphaAnimalation(_ isShowed: Bool) { UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) { @@ -247,6 +264,8 @@ extension ShookPlayerView { } } +// MARK: ShhokPlayerViewState + extension ShookPlayerView: ShhokPlayerViewState { func updataePlayState(_ isPlaying: Bool) { if isPlaying { @@ -258,11 +277,13 @@ extension ShookPlayerView: ShhokPlayerViewState { } } +// MARK: ShookPlayerViewAciton + extension ShookPlayerView: ShookPlayerViewAciton { var playerStateDidChange: AnyPublisher { $playingStateChangedPublisher.eraseToAnyPublisher() } - + var playerGestureDidTap: AnyPublisher { $playerGestureTapPublisher.eraseToAnyPublisher() } diff --git a/Projects/Features/LiveStreamFeature/Sources/Player/Views/TimeControlView.swift b/Projects/Features/LiveStreamFeature/Sources/Player/Views/TimeControlView.swift index 787b6e2a..307fadce 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Player/Views/TimeControlView.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Player/Views/TimeControlView.swift @@ -5,54 +5,60 @@ import BaseFeature import DesignSystem import EasyLayout +// MARK: - TimeControlState + private protocol TimeControlState { func updateSlider(to time: Float) } +// MARK: - TimeControlAction + private protocol TimeControlAction { var valueDidChanged: AnyPublisher { get } } +// MARK: - TimeControlView + final class TimeControlView: BaseView { - private let liveStringLabel: UILabel = UILabel() - private let slider: UISlider = UISlider() - + private let liveStringLabel: UILabel = .init() + private let slider: UISlider = .init() + var maxValue: Float = 0 { willSet { slider.maximumValue = newValue } } - + @Published private var currentValue: Float? - + override func setupViews() { - self.addSubview(liveStringLabel) - self.addSubview(slider) + addSubview(liveStringLabel) + addSubview(slider) } - + override func setupLayouts() { liveStringLabel.ezl.makeConstraint { $0.leading(to: self) .centerY(to: slider) } - + slider.ezl.makeConstraint { $0.leading(to: liveStringLabel.ezl.trailing, offset: 5) .trailing(to: self) .vertical(to: self) } } - + override func setupStyles() { liveStringLabel.textColor = DesignSystemAsset.Color.white.color liveStringLabel.font = .setFont(.caption2()) liveStringLabel.text = "live" - + slider.setThumbImage(renderThumbImage(size: CGSize(width: 10, height: 10)), for: .normal) slider.minimumTrackTintColor = DesignSystemAsset.Color.mainGreen.color slider.maximumTrackTintColor = DesignSystemAsset.Color.gray.color } - + override func setupActions() { slider.addTarget(self, action: #selector(changedValue), for: .valueChanged) } @@ -68,18 +74,22 @@ extension TimeControlView { path.fill() } } - + @objc private func changedValue() { currentValue = slider.value } } +// MARK: TimeControlState + extension TimeControlView: TimeControlState { func updateSlider(to time: Float) { slider.setValue(time, animated: false) } } +// MARK: TimeControlAction + extension TimeControlView: TimeControlAction { var valueDidChanged: AnyPublisher { $currentValue.eraseToAnyPublisher() diff --git a/Projects/Features/MainFeature/BroadcastUploadExtension/SampleHandler.swift b/Projects/Features/MainFeature/BroadcastUploadExtension/SampleHandler.swift index a71e3d2c..40b4c2a9 100644 --- a/Projects/Features/MainFeature/BroadcastUploadExtension/SampleHandler.swift +++ b/Projects/Features/MainFeature/BroadcastUploadExtension/SampleHandler.swift @@ -1,6 +1,5 @@ import ReplayKit final class SampleHandler: RPBroadcastSampleHandler, @unchecked Sendable { - override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) { - } + override func broadcastStarted(withSetupInfo _: [String: NSObject]?) {} } diff --git a/Projects/Features/MainFeature/Demo/Sources/AppDelegate.swift b/Projects/Features/MainFeature/Demo/Sources/AppDelegate.swift index 7f11f77f..d11803d2 100644 --- a/Projects/Features/MainFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Features/MainFeature/Demo/Sources/AppDelegate.swift @@ -8,8 +8,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let mockFetchChannelListUsecase = MockFetchChannelListUsecaseImpl() @@ -26,7 +26,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { let navigationController = UINavigationController(rootViewController: viewController) window?.rootViewController = navigationController window?.makeKeyAndVisible() - + return true } } diff --git a/Projects/Features/MainFeature/Demo/Sources/MockDeleteBroadcastUsecaseImpl.swift b/Projects/Features/MainFeature/Demo/Sources/MockDeleteBroadcastUsecaseImpl.swift index 45ca4aec..76146b16 100644 --- a/Projects/Features/MainFeature/Demo/Sources/MockDeleteBroadcastUsecaseImpl.swift +++ b/Projects/Features/MainFeature/Demo/Sources/MockDeleteBroadcastUsecaseImpl.swift @@ -3,7 +3,7 @@ import Combine import BroadcastDomainInterface final class MockDeleteBroadcastUsecaseImpl: DeleteBroadcastUsecase { - func execute(id: String) -> AnyPublisher { + func execute(id _: String) -> AnyPublisher { Future { promise in promise(.success(())) }.eraseToAnyPublisher() diff --git a/Projects/Features/MainFeature/Demo/Sources/MockDeleteChannelUsecaseImpl.swift b/Projects/Features/MainFeature/Demo/Sources/MockDeleteChannelUsecaseImpl.swift index a08cbb91..0383458e 100644 --- a/Projects/Features/MainFeature/Demo/Sources/MockDeleteChannelUsecaseImpl.swift +++ b/Projects/Features/MainFeature/Demo/Sources/MockDeleteChannelUsecaseImpl.swift @@ -3,7 +3,7 @@ import Combine import LiveStationDomainInterface final class MockDeleteChannelUsecaseImpl: DeleteChannelUsecase { - func execute(channelID: String) -> AnyPublisher { + func execute(channelID _: String) -> AnyPublisher { Future { promise in promise(.success(())) }.eraseToAnyPublisher() diff --git a/Projects/Features/MainFeature/Demo/Sources/MockFetchChannelListUsecaseImpl.swift b/Projects/Features/MainFeature/Demo/Sources/MockFetchChannelListUsecaseImpl.swift index 008a7124..b2dc9d6e 100644 --- a/Projects/Features/MainFeature/Demo/Sources/MockFetchChannelListUsecaseImpl.swift +++ b/Projects/Features/MainFeature/Demo/Sources/MockFetchChannelListUsecaseImpl.swift @@ -3,6 +3,8 @@ import UIKit import LiveStationDomainInterface +// MARK: - MockFetchChannelListUsecaseImpl + struct MockFetchChannelListUsecaseImpl: FetchChannelListUsecase { func execute() -> AnyPublisher<[ChannelEntity], any Error> { let fetcher = MockChannelListFetcher() @@ -17,16 +19,17 @@ struct MockFetchChannelListUsecaseImpl: FetchChannelListUsecase { } } +// MARK: - MockChannelListFetcher + final class MockChannelListFetcher { enum Image { case ratio16x9 case ratio4x3 func fetch() async -> UIImage? { - let size: (width: Int, height: Int) - switch self { - case .ratio16x9: size = (1920, 1080) - case .ratio4x3: size = (1440, 1080) + let size: (width: Int, height: Int) = switch self { + case .ratio16x9: (1920, 1080) + case .ratio4x3: (1440, 1080) } return await fetchImage(width: size.width, height: size.height) } @@ -34,18 +37,18 @@ final class MockChannelListFetcher { private func fetchImage(width: Int, height: Int) async -> UIImage? { guard let url = URL(string: "https://picsum.photos/\(width)/\(height)") else { return nil } - let data = (try? await URLSession.shared.data(from: url).0) ?? Data() + let data = await (try? URLSession.shared.data(from: url).0) ?? Data() return UIImage(data: data) } } func fetch() async -> [ChannelEntity] { - let random = Int.random(in: 3...7) + let random = Int.random(in: 3 ... 7) var channels: [ChannelEntity] = [] - for _ in 0.. UIViewController { +public struct MockLiveStreamViewControllerFractoryImpl: LiveStreamViewControllerFactory { + public init() {} + + public func make(channelID _: String, title _: String, owner _: String, description _: String) -> UIViewController { let viewModel = MockLiveStreamViewModel() return MockLiveStreamViewController(viewModel: viewModel) } diff --git a/Projects/Features/MainFeature/Demo/Sources/MockLiveStreamingViewController.swift b/Projects/Features/MainFeature/Demo/Sources/MockLiveStreamingViewController.swift index f8c1e0a1..ee6f121e 100644 --- a/Projects/Features/MainFeature/Demo/Sources/MockLiveStreamingViewController.swift +++ b/Projects/Features/MainFeature/Demo/Sources/MockLiveStreamingViewController.swift @@ -4,13 +4,17 @@ import BaseFeature import BaseFeatureInterface import EasyLayout +// MARK: - MockLiveStreamViewModel + public final class MockLiveStreamViewModel: ViewModel { - public struct Input { } - public struct Output { } - public init() { } - public func transform(input: Input) -> Output { return Output() } + public struct Input {} + public struct Output {} + public init() {} + public func transform(input _: Input) -> Output { Output() } } +// MARK: - MockLiveStreamViewController + public final class MockLiveStreamViewController: BaseViewController { public let playerView = MockShookPlayerView(with: URL(string: "https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/m3u8s/11331.m3u8")!) private let dismissButton: UIButton = { @@ -20,32 +24,32 @@ public final class MockLiveStreamViewController: BaseViewController 0 { let scale = max(1 - translation.y / 320, 0.75) view.transform = CGAffineTransform(scaleX: scale, y: scale) view.layer.cornerRadius = min(translation.y, 36) - + if translation.y > 56 { dismiss(animated: true) } } - - case .ended, .cancelled: + + case .cancelled, .ended: UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut) { self.view.transform = .identity self.view.layer.cornerRadius = 0 } - + default: break } } diff --git a/Projects/Features/MainFeature/Demo/Sources/MockMakeBroadcastUsecaseImpl.swift b/Projects/Features/MainFeature/Demo/Sources/MockMakeBroadcastUsecaseImpl.swift index a831c338..84ce9c25 100644 --- a/Projects/Features/MainFeature/Demo/Sources/MockMakeBroadcastUsecaseImpl.swift +++ b/Projects/Features/MainFeature/Demo/Sources/MockMakeBroadcastUsecaseImpl.swift @@ -3,7 +3,7 @@ import Combine import BroadcastDomainInterface final class MockMakeBroadcastUsecaseImpl: MakeBroadcastUsecase { - func execute(id: String, title: String, owner: String, description: String) -> AnyPublisher { + func execute(id _: String, title _: String, owner _: String, description _: String) -> AnyPublisher { Future { promise in promise(.success(())) }.eraseToAnyPublisher() diff --git a/Projects/Features/MainFeature/Demo/Sources/MockShookPlayerView.swift b/Projects/Features/MainFeature/Demo/Sources/MockShookPlayerView.swift index 1d5da56b..faaddc53 100644 --- a/Projects/Features/MainFeature/Demo/Sources/MockShookPlayerView.swift +++ b/Projects/Features/MainFeature/Demo/Sources/MockShookPlayerView.swift @@ -5,20 +5,21 @@ import BaseFeature import EasyLayout public final class MockShookPlayerView: BaseView { - private let player: AVPlayer = AVPlayer() + private let player: AVPlayer = .init() private var playerItem: AVPlayerItem - + private lazy var playerLayer: AVPlayerLayer = { let layer = AVPlayerLayer(player: player) layer.videoGravity = .resizeAspectFill return layer }() + private lazy var videoContainerView: UIView = { let view = UIView() view.layer.addSublayer(playerLayer) return view }() - + init(with url: URL) { playerItem = AVPlayerItem(url: url) player.replaceCurrentItem(with: playerItem) @@ -26,26 +27,27 @@ public final class MockShookPlayerView: BaseView { super.init(frame: .zero) videoContainerView.backgroundColor = .darkGray } - - required init?(coder: NSCoder) { + + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - - public override func setupViews() { + + override public func setupViews() { super.setupViews() - self.backgroundColor = .systemBackground - self.addSubview(videoContainerView) + backgroundColor = .systemBackground + addSubview(videoContainerView) } - - public override func setupLayouts() { + + override public func setupLayouts() { super.setupLayouts() - + videoContainerView.ezl.makeConstraint { $0.diagonal(to: self) } } - - public override func layoutSubviews() { + + override public func layoutSubviews() { super.layoutSubviews() playerLayer.frame = videoContainerView.bounds } diff --git a/Projects/Features/MainFeature/Project.swift b/Projects/Features/MainFeature/Project.swift index 9b4b1c05..cf8dd615 100644 --- a/Projects/Features/MainFeature/Project.swift +++ b/Projects/Features/MainFeature/Project.swift @@ -1,7 +1,7 @@ import DependencyPlugin +import EnvironmentPlugin import ProjectDescription import ProjectDescriptionHelpers -import EnvironmentPlugin let project = Project.module( name: ModulePaths.Feature.MainFeature.rawValue, diff --git a/Projects/Features/MainFeature/Sources/Factory/BroadcastViewControllerFactoryImpl.swift b/Projects/Features/MainFeature/Sources/Factory/BroadcastViewControllerFactoryImpl.swift index 92e4a204..576ed555 100644 --- a/Projects/Features/MainFeature/Sources/Factory/BroadcastViewControllerFactoryImpl.swift +++ b/Projects/Features/MainFeature/Sources/Factory/BroadcastViewControllerFactoryImpl.swift @@ -8,20 +8,20 @@ public struct BroadcastViewControllerFactoryImpl: BroadcastViewControllerFactory private let fetchChannelInfoUsecase: any FetchChannelInfoUsecase private let makeBroadcastUsecase: any MakeBroadcastUsecase private let deleteBroadCastUsecase: any DeleteBroadcastUsecase - + public init(fetchChannelInfoUsecase: any FetchChannelInfoUsecase, makeBroadcastUsecase: any MakeBroadcastUsecase, deleteBroadCastUsecase: any DeleteBroadcastUsecase) { self.fetchChannelInfoUsecase = fetchChannelInfoUsecase self.makeBroadcastUsecase = makeBroadcastUsecase self.deleteBroadCastUsecase = deleteBroadCastUsecase } - + public func make() -> UIViewController { let viewModel = SettingViewModel( fetchChannelInfoUsecase: fetchChannelInfoUsecase, makeBroadcastUsecase: makeBroadcastUsecase, deleteBroadCastUsecase: deleteBroadCastUsecase ) - + return BroadcastViewController(viewModel: viewModel) } } diff --git a/Projects/Features/MainFeature/Sources/Factory/SettingViewControllerFactoryImpl.swift b/Projects/Features/MainFeature/Sources/Factory/SettingViewControllerFactoryImpl.swift index 16cbd2ef..e1d82efc 100644 --- a/Projects/Features/MainFeature/Sources/Factory/SettingViewControllerFactoryImpl.swift +++ b/Projects/Features/MainFeature/Sources/Factory/SettingViewControllerFactoryImpl.swift @@ -8,20 +8,20 @@ public struct SettingViewControllerFactoryImpl: SettingViewControllerFactory { private let fetchChannelInfoUsecase: any FetchChannelInfoUsecase private let makeBroadcastUsecase: any MakeBroadcastUsecase private let deleteBroadCastUsecase: any DeleteBroadcastUsecase - + public init(fetchChannelInfoUsecase: any FetchChannelInfoUsecase, makeBroadcastUsecase: any MakeBroadcastUsecase, deleteBroadCastUsecase: any DeleteBroadcastUsecase) { self.fetchChannelInfoUsecase = fetchChannelInfoUsecase self.makeBroadcastUsecase = makeBroadcastUsecase self.deleteBroadCastUsecase = deleteBroadCastUsecase } - + public func make() -> UIViewController { let viewModel = SettingViewModel( fetchChannelInfoUsecase: fetchChannelInfoUsecase, makeBroadcastUsecase: makeBroadcastUsecase, deleteBroadCastUsecase: deleteBroadCastUsecase ) - + return SettingUIViewController(viewModel: viewModel) } } diff --git a/Projects/Features/MainFeature/Sources/Models/Channel.swift b/Projects/Features/MainFeature/Sources/Models/Channel.swift index dc09cc0a..7fbc494c 100644 --- a/Projects/Features/MainFeature/Sources/Models/Channel.swift +++ b/Projects/Features/MainFeature/Sources/Models/Channel.swift @@ -13,7 +13,7 @@ public struct Channel: Hashable { description: String = "" ) { self.id = id - self.name = title + name = title self.thumbnailImageURLString = thumbnailImageURLString self.owner = owner self.description = description diff --git a/Projects/Features/MainFeature/Sources/Utilities/CollectionViewCellTransitioning.swift b/Projects/Features/MainFeature/Sources/Utilities/CollectionViewCellTransitioning.swift index f17d6b53..b1b33d34 100644 --- a/Projects/Features/MainFeature/Sources/Utilities/CollectionViewCellTransitioning.swift +++ b/Projects/Features/MainFeature/Sources/Utilities/CollectionViewCellTransitioning.swift @@ -1,25 +1,27 @@ import UIKit +// MARK: - CollectionViewCellTransitioning + final class CollectionViewCellTransitioning: NSObject { enum Transition { case present case dismiss - - var blurAlpha: CGFloat { return self == .present ? 1 : 0 } - var dimmingAlpha: CGFloat { return self == .present ? 0.6 : 0 } - var closeAlpha: CGFloat { return self == .present ? 1 : 0 } - var cornerRadius: CGFloat { return self == .present ? 16 : 0 } - var next: Transition { return self == .present ? .dismiss : .present } + + var blurAlpha: CGFloat { self == .present ? 1 : 0 } + var dimmingAlpha: CGFloat { self == .present ? 0.6 : 0 } + var closeAlpha: CGFloat { self == .present ? 1 : 0 } + var cornerRadius: CGFloat { self == .present ? 16 : 0 } + var next: Transition { self == .present ? .dismiss : .present } } - + var transition: Transition = .present - + let transitionDuration: Double = 1 let shrinkDuration: Double = 0.2 - + private let blurEffectView = UIVisualEffectView() private let backgroundView = UIView() - + override init() { super.init() blurEffectView.effect = UIBlurEffect(style: .dark) @@ -27,67 +29,68 @@ final class CollectionViewCellTransitioning: NSObject { } } -// MARK: - UIViewControllerTransitioningDelegate +// MARK: UIViewControllerTransitioningDelegate /// transition 속성을 변경하고 UIViewControllerAnimatedTransitioning를 채택한 자기 자신을 반환합니다. extension CollectionViewCellTransitioning: UIViewControllerTransitioningDelegate { - func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? { + func animationController(forPresented _: UIViewController, presenting _: UIViewController, source _: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? { transition = .present return self } - - func animationController(forDismissed dismissed: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? { + + func animationController(forDismissed _: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? { transition = .dismiss return self } } -// MARK: - UIViewControllerAnimatedTransitioning +// MARK: UIViewControllerAnimatedTransitioning + extension CollectionViewCellTransitioning: UIViewControllerAnimatedTransitioning { - func transitionDuration(using transitionContext: (any UIViewControllerContextTransitioning)?) -> TimeInterval { + func transitionDuration(using _: (any UIViewControllerContextTransitioning)?) -> TimeInterval { transitionDuration } - + /// 실제 Transition 구현 부분 func animateTransition(using transitionContext: any UIViewControllerContextTransitioning) { /// 전환에 관련된 뷰를 사용합니다. let containerView = transitionContext.containerView /// 1. 깔끔한 애니메이션을 위해 transition되는 containerView의 모든 요소를 제거합니다. containerView.subviews.forEach { $0.removeFromSuperview() } - + /// 2. 전환에 사용되는 뷰에 blurEffectView 추가합니다. blurEffectView.frame = containerView.frame blurEffectView.alpha = transition.next.blurAlpha containerView.addSubview(blurEffectView) - + let fromView = transitionContext.viewController(forKey: .from) let toView = transitionContext.viewController(forKey: .to) - + var thumbnailView: ThumbnailView? /// 3. Present: 시작하는 뷰의 썸네일을 가져옵니다. (시작 좌표 및 복사를 위해) if let navigationController = (fromView as? UINavigationController), let viewController = (navigationController.topViewController as? BroadcastCollectionViewController) { thumbnailView = viewController.selectedThumbnailView } - + /// 3. Dismiss: 목적지 뷰의 썸네일을 가져옵니다. (도착 좌표 및 복사를 위해) if let navigationController = (toView as? UINavigationController), let viewController = (navigationController.topViewController as? BroadcastCollectionViewController) { thumbnailView = viewController.selectedThumbnailView } - + /// 4. 썸네일 뷰를 복사하고 절대 프레임을 가져옵니다. guard let thumbnailView else { return } let thumbnailViewCopy = copy(of: thumbnailView) let absoluteFrame = thumbnailView.convert(thumbnailView.frame, to: nil) - + /// 애니메이션의 디테일을 위한 설정 backgroundView.frame = transition == .present ? absoluteFrame : containerView.frame backgroundView.layer.cornerRadius = transition.cornerRadius thumbnailViewCopy.insertSubview(backgroundView, aboveSubview: thumbnailViewCopy.shadowView) - + /// 5. 기존 썸네일을 숨기고 복사한 썸네일을 containerView에 추가합니다. containerView.addSubview(thumbnailViewCopy) thumbnailView.isHidden = true - + /// 6. Present: 애니메이션, 작았다 커지면서 위로 이동합니다. if transition == .present, let toView { /// 6-1. 썸네일은 원래 위치에서 시작 @@ -106,7 +109,7 @@ extension CollectionViewCellTransitioning: UIViewControllerAnimatedTransitioning transitionContext.completeTransition(true) } } - + /// 6. Dismiss: 위에서 셀의 위치로 돌아오면서 작아집니다. if transition == .dismiss, let fromView { /// 6-1. 시작 뷰 숨김 @@ -126,13 +129,14 @@ extension CollectionViewCellTransitioning: UIViewControllerAnimatedTransitioning } // MARK: - Methods + extension CollectionViewCellTransitioning { private func copy(of thumbnailView: ThumbnailView) -> ThumbnailView { let thumbnailViewCopy = ThumbnailView(with: thumbnailView.size) thumbnailViewCopy.configure(with: thumbnailView.imageView.image) return thumbnailViewCopy } - + /// Present 때 사용되는 애니메이션 /// 썸네일을 잠깐 축소했다가 확대시키면서 애니메이션이 진행됩니다. private func makeShrinkAnimator(of thumbnailView: ThumbnailView) -> UIViewPropertyAnimator { @@ -140,13 +144,13 @@ extension CollectionViewCellTransitioning { thumbnailView.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) } } - + /// Present, Dismiss 때 사용되는 애니메이션 private func makeScaleAndPositionAnimator(of thumbnailView: ThumbnailView, in containerView: UIView, to frame: CGRect) -> UIViewPropertyAnimator { /// 애니메이션 설정 let springTiming = UISpringTimingParameters(dampingRatio: 0.75, initialVelocity: CGVector(dx: 0, dy: 2)) let animator = UIViewPropertyAnimator(duration: transitionDuration - shrinkDuration, timingParameters: springTiming) - + animator.addAnimations { switch self.transition { case .present: @@ -157,7 +161,7 @@ extension CollectionViewCellTransitioning { thumbnailView.updateLayouts(for: .present) /// 썸네일 뷰 플레이어뷰 위치로 이동 및 확대 thumbnailView.frame = CGRect(x: 0, y: containerView.layoutMargins.top, width: containerView.frame.width, height: containerView.frame.width * 0.5625) - + case .dismiss: /// 썸네일 뷰 Style, Layout 업데이트 thumbnailView.updateStyles(for: .dismiss) @@ -165,39 +169,39 @@ extension CollectionViewCellTransitioning { /// 인자로 받은 frame위치로 이동 (기존 썸네일 뷰의 절대 프레임) thumbnailView.frame = frame } - + self.blurEffectView.alpha = self.transition.blurAlpha - + self.backgroundView.layer.cornerRadius = self.transition.next.cornerRadius self.backgroundView.frame = containerView.frame - + containerView.layoutIfNeeded() - + self.backgroundView.frame = self.transition == .present ? containerView.frame : thumbnailView.imageView.frame } - + return animator } - + private func moveAndConvert(thumbnailView: ThumbnailView, containerView: UIView, to frame: CGRect, completion: @escaping () -> Void) { let shrinkAnimator = makeShrinkAnimator(of: thumbnailView) let scaleAndPositionAnimator = makeScaleAndPositionAnimator(of: thumbnailView, in: containerView, to: frame) - + switch transition { case .present: shrinkAnimator.startAnimation() - + /// 축소 애니메이션 종료 후 확대 애니메이션 shrinkAnimator.addCompletion { _ in scaleAndPositionAnimator.startAnimation() } - + case .dismiss: /// 썸네일 뷰의 위치를 먼저 잡고 축소 애니메이션 thumbnailView.layoutIfNeeded() scaleAndPositionAnimator.startAnimation() } - + /// 크기, 위치 애니메이션 종료 후 scaleAndPositionAnimator.addCompletion { _ in completion() diff --git a/Projects/Features/MainFeature/Sources/ViewControllers/BroadcastCollectionViewController.swift b/Projects/Features/MainFeature/Sources/ViewControllers/BroadcastCollectionViewController.swift index 7a4e318d..6225671d 100644 --- a/Projects/Features/MainFeature/Sources/ViewControllers/BroadcastCollectionViewController.swift +++ b/Projects/Features/MainFeature/Sources/ViewControllers/BroadcastCollectionViewController.swift @@ -8,31 +8,35 @@ import EasyLayout import LiveStreamFeatureInterface import MainFeatureInterface +// MARK: - BroadcastCollectionViewController + public class BroadcastCollectionViewController: BaseViewController { private enum Section: Int, Hashable { - case empty, large, small + case empty + case large + case small } - + private typealias DataSource = UICollectionViewDiffableDataSource private typealias Snapshot = NSDiffableDataSourceSnapshot - + private let input = BroadcastCollectionViewModel.Input() private var cancellables = Set() - + private let refreshControl = SHRefreshControl() - private let rightBarButton: UIBarButtonItem = UIBarButtonItem() - + private let rightBarButton: UIBarButtonItem = .init() + private let layout = setupCollectionViewCompositionalLayout() private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) private var dataSource: DataSource? private var factory: LiveStreamViewControllerFactory? private var settingFactory: SettingViewControllerFactory? private var broadcastFactory: BroadcastViewControllerFactory? - + private let transitioning = CollectionViewCellTransitioning() - + private let dataLoadView = BroadcastCollectionLoadView() - + var selectedThumbnailView: ThumbnailView? { guard let indexPath = collectionView.indexPathsForSelectedItems?.first else { return nil } let cell = collectionView.cellForItem(at: indexPath) @@ -51,26 +55,27 @@ public class BroadcastCollectionViewController: BaseViewController UICollectionViewCompositionalLayout { - return UICollectionViewCompositionalLayout { sectionIndex, _ in + UICollectionViewCompositionalLayout { sectionIndex, _ in let section = Section(rawValue: sectionIndex) ?? .small switch section { case .empty: @@ -155,7 +162,7 @@ extension BroadcastCollectionViewController { let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item]) let section = NSCollectionLayoutSection(group: group) return section - + case .large: let size = NSCollectionLayoutSize( widthDimension: NSCollectionLayoutDimension.fractionalWidth(1), @@ -163,13 +170,13 @@ extension BroadcastCollectionViewController { ) let item = NSCollectionLayoutItem(layoutSize: size) let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item]) - + let section = NSCollectionLayoutSection(group: group) section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 0, bottom: 24, trailing: 0) section.interGroupSpacing = 24 - + return section - + case .small: let size = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), @@ -177,22 +184,22 @@ extension BroadcastCollectionViewController { ) let item = NSCollectionLayoutItem(layoutSize: size) let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item]) - + let section = NSCollectionLayoutSection(group: group) section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 0, bottom: 24, trailing: 0) section.interGroupSpacing = 24 - + let headerSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(10) ) - + let header = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top ) - + section.boundarySupplementaryItems = [header] return section } @@ -201,11 +208,12 @@ extension BroadcastCollectionViewController { } // MARK: - CollectionView Diffable DataSource + extension BroadcastCollectionViewController { private func setupDataSource() { dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, channel in guard let section = Section(rawValue: indexPath.section) else { return UICollectionViewCell() } - + switch section { case .empty: guard let emptyCell = collectionView.dequeueReusableCell( @@ -215,7 +223,7 @@ extension BroadcastCollectionViewController { return UICollectionViewCell() } return emptyCell - + case .large: guard let largeCell = collectionView.dequeueReusableCell( withReuseIdentifier: LargeBroadcastCollectionViewCell.identifier, @@ -225,7 +233,7 @@ extension BroadcastCollectionViewController { } largeCell.configure(channel: channel) return largeCell - + case .small: guard let smallCell = collectionView.dequeueReusableCell( withReuseIdentifier: SmallBroadcastCollectionViewCell.identifier, @@ -237,14 +245,14 @@ extension BroadcastCollectionViewController { return smallCell } } - + dataSource?.supplementaryViewProvider = { collectionView, kind, indexPath in let header = collectionView.dequeueReusableSupplementaryView( ofKind: kind, withReuseIdentifier: "Header", for: indexPath ) - + if indexPath.section == 1 { let label = UILabel() label.font = .setFont(.title()) @@ -256,14 +264,14 @@ extension BroadcastCollectionViewController { .vertical(to: header) } } - + return header } } - + private func applySnapshot(with channels: [Channel]) { dataLoadView.isHidden = true - + var snapshot = Snapshot() snapshot.appendSections([.empty, .large]) @@ -279,39 +287,40 @@ extension BroadcastCollectionViewController { snapshot.appendItems(smallSectionItems, toSection: .small) } } - + dataSource?.applySnapshotUsingReloadData(snapshot) } } // MARK: - CollectionView Methods + extension BroadcastCollectionViewController { @objc private func didTapRightBarButton() { guard let settingUIViewController = settingFactory?.make() else { return } let settingNavigationController = UINavigationController(rootViewController: settingUIViewController) navigationController?.present(settingNavigationController, animated: true) } - + private func showBroadcastUIView() { guard let broadcastViewController = broadcastFactory?.make() else { return } navigationController?.setNavigationBarHidden(true, animated: false) - self.addChild(broadcastViewController) - self.view.addSubview(broadcastViewController.view) - broadcastViewController.view.frame = self.view.bounds + addChild(broadcastViewController) + view.addSubview(broadcastViewController.view) + broadcastViewController.view.frame = view.bounds broadcastViewController.didMove(toParent: self) } - + private func dismissBroadcastView() { DispatchQueue.main.async { [weak self] in guard let self else { return } guard let broadcastViewController = children.first(where: { $0 is BroadcastViewController }) else { return } UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut], animations: { - broadcastViewController.view.alpha = 0 }, completion: { _ in - broadcastViewController.willMove(toParent: nil) - broadcastViewController.view.removeFromSuperview() - broadcastViewController.removeFromParent() - } - ) + broadcastViewController.view.alpha = 0 + }, completion: { _ in + broadcastViewController.willMove(toParent: nil) + broadcastViewController.view.removeFromSuperview() + broadcastViewController.removeFromParent() + }) navigationController?.setNavigationBarHidden(false, animated: false) input.fetch.send() } diff --git a/Projects/Features/MainFeature/Sources/ViewControllers/BroadcastViewController.swift b/Projects/Features/MainFeature/Sources/ViewControllers/BroadcastViewController.swift index b3c92ebd..73a096da 100644 --- a/Projects/Features/MainFeature/Sources/ViewControllers/BroadcastViewController.swift +++ b/Projects/Features/MainFeature/Sources/ViewControllers/BroadcastViewController.swift @@ -10,21 +10,21 @@ public final class BroadcastViewController: BaseViewController deinit { viewModel.sharedDefaults?.removeObserver(self, forKeyPath: viewModel.isStreamingKey) } - + private let broadcastStatusStackView = UIStackView() private let broadcastStatusImageView = UIImageView() private let broadcastStateText = UILabel() private let finshBroadcastButton = UIButton() - + private let viewModelInput = SettingViewModel.Input() - - public override func setupBind() { + + override public func setupBind() { _ = viewModel.transform(input: viewModelInput) } - + private var broadcastPicker = RPSystemBroadcastPickerView() - - public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { + + override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { if keyPath == viewModel.isStreamingKey { if let newValue = change?[.newKey] as? Bool, !newValue { didFinishBroadCast() @@ -34,25 +34,25 @@ public final class BroadcastViewController: BaseViewController } } - public override func setupViews() { + override public func setupViews() { broadcastPicker.frame = CGRect(x: 0, y: 0, width: 0, height: 0) broadcastPicker.preferredExtension = viewModel.extensionBundleID broadcastPicker.showsMicrophoneButton = false - + finshBroadcastButton.addSubview(broadcastPicker) - + viewModel.sharedDefaults?.addObserver(self, forKeyPath: viewModel.isStreamingKey, options: [.initial, .new], context: nil) - + broadcastStatusStackView.addArrangedSubview(broadcastStatusImageView) broadcastStatusStackView.addArrangedSubview(broadcastStateText) - + view.addSubview(broadcastStatusStackView) view.addSubview(finshBroadcastButton) } - - public override func setupStyles() { + + override public func setupStyles() { view.backgroundColor = .systemBackground - + broadcastStatusStackView.axis = .vertical broadcastStatusStackView.spacing = 7 broadcastStatusStackView.alignment = .center @@ -67,41 +67,41 @@ public final class BroadcastViewController: BaseViewController finshBroadcastButton.backgroundColor = DesignSystemAsset.Color.mainGreen.color finshBroadcastButton.setTitleColor(DesignSystemAsset.Color.mainBlack.color, for: .normal) } - - public override func setupLayouts() { + + override public func setupLayouts() { broadcastStatusStackView.ezl.makeConstraint { $0.horizontal(to: view.safeAreaLayoutGuide) .centerY(to: view) } - + broadcastStatusImageView.ezl.makeConstraint { $0.size(with: 117) .centerX(to: broadcastStatusStackView) } - + finshBroadcastButton.ezl.makeConstraint { $0.height(56) .bottom(to: view.safeAreaLayoutGuide, offset: -23) .horizontal(to: view, padding: 20) } - + broadcastPicker.ezl.makeConstraint { $0.center(to: finshBroadcastButton) .width(finshBroadcastButton.frame.width) .height(finshBroadcastButton.frame.height) } } - - public override func setupActions() { + + override public func setupActions() { finshBroadcastButton.addTarget(self, action: #selector(didTapFinishButton), for: .touchUpInside) } - + @objc private func didTapFinishButton() { guard let broadcastPickerButton = broadcastPicker.subviews.first(where: { $0 is UIButton }) as? UIButton else { return } broadcastPickerButton.sendActions(for: .touchUpInside) } - + private func didFinishBroadCast() { viewModelInput.didTapFinishStreamingButton.send() dismiss(animated: false) diff --git a/Projects/Features/MainFeature/Sources/ViewControllers/SettingUIViewController.swift b/Projects/Features/MainFeature/Sources/ViewControllers/SettingUIViewController.swift index dade5ceb..144ae16b 100644 --- a/Projects/Features/MainFeature/Sources/ViewControllers/SettingUIViewController.swift +++ b/Projects/Features/MainFeature/Sources/ViewControllers/SettingUIViewController.swift @@ -7,11 +7,13 @@ import DesignSystem import EasyLayout import MainFeatureInterface +// MARK: - SettingUIViewController + public final class SettingUIViewController: BaseViewController { deinit { viewModel.sharedDefaults?.removeObserver(self, forKeyPath: viewModel.isStreamingKey) } - + private let settingTableView = UITableView() private let closeBarButton = UIBarButtonItem() private let startBroadcastButton = UIButton() @@ -19,13 +21,13 @@ public final class SettingUIViewController: BaseViewController private let streamingNameCell = SettingTableViewCell(style: .default, reuseIdentifier: nil) private let placeholderStringOfCells = ["어떤 방송인지 알려주세요!", "방송 내용을 알려주세요!"] private lazy var loadingView = SHLoadingView(message: "방송 생성 중") - + private var broadcastPicker = RPSystemBroadcastPickerView() - + private let viewModelInput = SettingViewModel.Input() private var cancellables = Set() - - public override func observeValue( + + override public func observeValue( forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, @@ -39,27 +41,27 @@ public final class SettingUIViewController: BaseViewController super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } } - - public override func setupBind() { + + override public func setupBind() { let output = viewModel.transform(input: viewModelInput) - + output.streamingStartButtonIsActive .sink { [weak self] isActive in guard let self else { return } - self.startBroadcastButton.isEnabled = isActive - self.startBroadcastButton.backgroundColor = isActive + startBroadcastButton.isEnabled = isActive + startBroadcastButton.backgroundColor = isActive ? DesignSystemAsset.Color.mainGreen.color : DesignSystemAsset.Color.gray.color } .store(in: &cancellables) - + output.errorMessage .sink { [weak self] errorMessage in guard let self else { return } - self.streamingNameCell.setErrorMessage(message: errorMessage) + streamingNameCell.setErrorMessage(message: errorMessage) } .store(in: &cancellables) - + output.isReadyToStream .receive(on: DispatchQueue.main) .sink { [weak self] isReady in @@ -74,99 +76,101 @@ public final class SettingUIViewController: BaseViewController } .store(in: &cancellables) } - - public override func setupViews() { + + override public func setupViews() { closeBarButton.image = DesignSystemAsset.Image.xmark24.image closeBarButton.tintColor = .gray - + viewModel.sharedDefaults?.addObserver(self, forKeyPath: viewModel.isStreamingKey, options: [.initial, .new], context: nil) viewModel.sharedDefaults?.set(false, forKey: viewModel.isStreamingKey) - + navigationItem.title = "방송설정" navigationItem.rightBarButtonItem = closeBarButton - + settingTableView.delegate = self settingTableView.dataSource = self settingTableView.register(SettingTableViewCell.self, forCellReuseIdentifier: SettingTableViewCell.identifier) settingTableView.rowHeight = UITableView.automaticDimension settingTableView.estimatedRowHeight = 100 - + broadcastPicker.frame = CGRect(x: 0, y: 0, width: 0, height: 0) broadcastPicker.preferredExtension = viewModel.extensionBundleID broadcastPicker.showsMicrophoneButton = false startBroadcastButton.isEnabled = false startBroadcastButton.addSubview(broadcastPicker) - + view.addSubview(settingTableView) view.addSubview(startBroadcastButton) } - - public override func setupStyles() { + + override public func setupStyles() { startBroadcastButton.setTitle("방송시작", for: .normal) startBroadcastButton.layer.cornerRadius = 16 startBroadcastButton.titleLabel?.font = .setFont(.body1(weight: .semiBold)) startBroadcastButton.backgroundColor = DesignSystemAsset.Color.gray.color startBroadcastButton.setTitleColor(DesignSystemAsset.Color.mainBlack.color, for: .normal) } - - public override func setupLayouts() { + + override public func setupLayouts() { settingTableView.ezl.makeConstraint { $0.diagonal(to: view) } - + startBroadcastButton.ezl.makeConstraint { $0.height(56) .bottom(to: view.keyboardLayoutGuide.ezl.top, offset: -23) .horizontal(to: view, padding: 20) } - + broadcastPicker.ezl.makeConstraint { $0.center(to: startBroadcastButton) .width(startBroadcastButton.frame.width) .height(startBroadcastButton.frame.height) } } - - public override func setupActions() { + + override public func setupActions() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) view.addGestureRecognizer(tapGesture) - + startBroadcastButton.addTarget(self, action: #selector(didTapStartBroadcastButton), for: .touchUpInside) - + closeBarButton.target = self closeBarButton.action = #selector(didTapRightBarButton) } - + /// X 모양 버튼이 눌렸을 때 호출되는 메서드 @objc private func didTapRightBarButton() { dismiss(animated: true) } - + /// 방송 시작 버튼이 눌렸을 때 호출되는 메서드 @objc private func didTapStartBroadcastButton() { viewModelInput.didTapStartBroadcastButton.send() } - + private func didStartBroadCast() { NotificationCenter.default.post(name: NotificationName.startStreaming, object: self) dismiss(animated: true) } - + @objc private func dismissKeyboard() { view.endEditing(true) } } +// MARK: UITableViewDelegate, UITableViewDataSource + extension SettingUIViewController: UITableViewDelegate, UITableViewDataSource { - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + public func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { placeholderStringOfCells.count } - - public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + public func tableView(_: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if indexPath.row == 0 { streamingNameCell.configure( label: "방송이름", @@ -185,8 +189,8 @@ extension SettingUIViewController: UITableViewDelegate, UITableViewDataSource { return streamingDescriptionCell } } - - public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + + public func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { UITableView.automaticDimension } } @@ -199,16 +203,16 @@ extension SettingUIViewController { $0.diagonal(to: view) } } - + private func removeLoadingView() { loadingView.removeFromSuperview() } - + private func disableButton() { startBroadcastButton.isEnabled = false startBroadcastButton.backgroundColor = DesignSystemAsset.Color.gray.color } - + private func enableButton() { startBroadcastButton.isEnabled = true startBroadcastButton.backgroundColor = DesignSystemAsset.Color.mainGreen.color diff --git a/Projects/Features/MainFeature/Sources/ViewModels/BroadcastCollectionViewModel.swift b/Projects/Features/MainFeature/Sources/ViewModels/BroadcastCollectionViewModel.swift index 6d99f8d9..f82ab9f3 100644 --- a/Projects/Features/MainFeature/Sources/ViewModels/BroadcastCollectionViewModel.swift +++ b/Projects/Features/MainFeature/Sources/ViewModels/BroadcastCollectionViewModel.swift @@ -10,22 +10,22 @@ public class BroadcastCollectionViewModel: ViewModel { public struct Input { let fetch: PassthroughSubject = .init() } - + public struct Output { let channels: PassthroughSubject<[Channel], Never> = .init() } - + private let output = Output() - + private let fetchChannelListUsecase: any FetchChannelListUsecase private let fetchAllBroadcastUsecase: any FetchAllBroadcastUsecase - + private var cancellables = Set() - + private let extensionBundleID = "kr.codesquad.boostcamp9.Shook.BroadcastUploadExtension" private let isStreamingKey = "IS_STREAMING" private let channelID = UserDefaults.standard.string(forKey: "CHANNEL_ID") - + public init( fetchChannelListUsecase: FetchChannelListUsecase, fetchAllBroadcastUsecase: FetchAllBroadcastUsecase @@ -33,7 +33,7 @@ public class BroadcastCollectionViewModel: ViewModel { self.fetchChannelListUsecase = fetchChannelListUsecase self.fetchAllBroadcastUsecase = fetchAllBroadcastUsecase } - + public func transform(input: Input) -> Output { input.fetch .sink { [weak self] in @@ -43,7 +43,7 @@ public class BroadcastCollectionViewModel: ViewModel { return output } - + private func fetchData() { fetchChannelListUsecase.execute() .zip(fetchAllBroadcastUsecase.execute()) diff --git a/Projects/Features/MainFeature/Sources/ViewModels/SettingViewModel.swift b/Projects/Features/MainFeature/Sources/ViewModels/SettingViewModel.swift index 252ffc50..9290c1ee 100644 --- a/Projects/Features/MainFeature/Sources/ViewModels/SettingViewModel.swift +++ b/Projects/Features/MainFeature/Sources/ViewModels/SettingViewModel.swift @@ -13,32 +13,32 @@ public class SettingViewModel: ViewModel { let didTapStartBroadcastButton: PassthroughSubject = .init() let didTapFinishStreamingButton: PassthroughSubject = .init() } - + public struct Output { let streamingStartButtonIsActive: PassthroughSubject = .init() let errorMessage: PassthroughSubject = .init() let isReadyToStream: PassthroughSubject = .init() } - + private var cancellables = Set() - + private let fetchChannelInfoUsecase: any FetchChannelInfoUsecase private let makeBroadcastUsecase: any MakeBroadcastUsecase private let deleteBroadCastUsecase: any DeleteBroadcastUsecase - + private var broadcastName: String = "" private var channelDescription: String = "" - + private let channelID = UserDefaults.standard.string(forKey: "CHANNEL_ID") private let userName = UserDefaults.standard.string(forKey: "USER_NAME") - + private let rtmpKey = "RTMP_SEVICE_URL" private let streamKey = "STREAMING_KEY" let sharedDefaults = UserDefaults(suiteName: "group.kr.codesquad.boostcamp9.Shook") let extensionBundleID = "kr.codesquad.boostcamp9.Shook.BroadcastUploadExtension" let isStreamingKey = "IS_STREAMING" - + public init( fetchChannelInfoUsecase: FetchChannelInfoUsecase, makeBroadcastUsecase: MakeBroadcastUsecase, @@ -48,10 +48,10 @@ public class SettingViewModel: ViewModel { self.makeBroadcastUsecase = makeBroadcastUsecase self.deleteBroadCastUsecase = deleteBroadCastUsecase } - + public func transform(input: Input) -> Output { let output = Output() - + input.didWriteStreamingName .sink { [weak self] name in guard let self else { return } @@ -63,13 +63,13 @@ public class SettingViewModel: ViewModel { } } .store(in: &cancellables) - + input.didWriteStreamingDescription .sink { [weak self] description in self?.channelDescription = description } .store(in: &cancellables) - + input.didTapStartBroadcastButton .flatMap { [weak self] in guard let self, let channelID, let userName else { return Empty().eraseToAnyPublisher() } @@ -94,7 +94,7 @@ public class SettingViewModel: ViewModel { output.isReadyToStream.send(true) } .store(in: &cancellables) - + input.didTapFinishStreamingButton .flatMap { [weak self] _ in guard let self, @@ -107,16 +107,16 @@ public class SettingViewModel: ViewModel { NotificationCenter.default.post(name: NotificationName.finishStreaming, object: self) } .store(in: &cancellables) - + return output } - + /// 방송 이름이 유효한지 확인하는 메서드 /// - Parameter _: 방송 이름 /// - Returns: (Bool, String?) - 유효 여부와 에러 메시지 private func valid(_ value: String) -> (isValid: Bool, errorMessage: String?) { let trimmedValue = value.trimmingCharacters(in: .whitespaces) - + if trimmedValue.isEmpty { return (false, "공백을 제외하고 최소 1글자 이상 입력해주세요.") } else { diff --git a/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionLoadView.swift b/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionLoadView.swift index 56ae8c34..dd36afcb 100644 --- a/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionLoadView.swift +++ b/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionLoadView.swift @@ -4,47 +4,49 @@ import BaseFeature import DesignSystem import Lottie +// MARK: - BroadcastCollectionLoadView + final class BroadcastCollectionLoadView: BaseView { private let shookAnimationView = LottieAnimationView(name: "shook", bundle: Bundle(for: DesignSystemResources.self)) - + private let titleLabel = UILabel() private let subtitleLabel = UILabel() - + private lazy var stackView = UIStackView(arrangedSubviews: [shookAnimationView, titleLabel, subtitleLabel]) - + override func layoutSubviews() { super.layoutSubviews() shookAnimationView.play() } - + override func setupViews() { addSubview(stackView) - + titleLabel.text = "방송을 불러오는 중이에요!" - + subtitleLabel.text = "잠시만 기다려주세요!" } - + override func setupStyles() { shookAnimationView.contentMode = .scaleAspectFit shookAnimationView.loopMode = .loop shookAnimationView.animationSpeed = 0.4 - + titleLabel.font = .setFont(.title()) - + subtitleLabel.textColor = .gray subtitleLabel.font = .setFont(.body2()) - + stackView.axis = .vertical stackView.spacing = 12 stackView.alignment = .center } - + override func setupLayouts() { stackView.ezl.makeConstraint { $0.center(to: self) } - + shookAnimationView.ezl.makeConstraint { $0.width(220) .height(80) @@ -53,20 +55,20 @@ final class BroadcastCollectionLoadView: BaseView { } #if DEBUG -import SwiftUI + import SwiftUI -struct BroadcastCollectionLoadViewPreview: UIViewRepresentable { - func makeUIView(context: Context) -> BroadcastCollectionLoadView { - return BroadcastCollectionLoadView() + struct BroadcastCollectionLoadViewPreview: UIViewRepresentable { + func makeUIView(context _: Context) -> BroadcastCollectionLoadView { + BroadcastCollectionLoadView() + } + + func updateUIView(_: BroadcastCollectionLoadView, context _: Context) {} } - - func updateUIView(_ uiView: BroadcastCollectionLoadView, context: Context) { } -} -struct BroadcastCollectionLoadViewPreview_Previews: PreviewProvider { - static var previews: some View { - BroadcastCollectionLoadViewPreview() - .background(Color.black) + struct BroadcastCollectionLoadViewPreview_Previews: PreviewProvider { + static var previews: some View { + BroadcastCollectionLoadViewPreview() + .background(Color.black) + } } -} #endif diff --git a/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionViewCell/EmptyBroadcastCollectionViewCell.swift b/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionViewCell/EmptyBroadcastCollectionViewCell.swift index f2b3e7e4..c93ce515 100644 --- a/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionViewCell/EmptyBroadcastCollectionViewCell.swift +++ b/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionViewCell/EmptyBroadcastCollectionViewCell.swift @@ -8,42 +8,42 @@ final class EmptyBroadcastCollectionViewCell: BaseCollectionViewCell { private let imageView = UIImageView() private let titleLabel = UILabel() private let subtitleLabel = UILabel() - + private lazy var textStackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]) private lazy var stackView = UIStackView(arrangedSubviews: [imageView, textStackView]) - + override func setupViews() { contentView.addSubview(stackView) - + imageView.image = DesignSystemAsset.Image.tv48.image - + titleLabel.text = "아직 라이브 방송이 없어요!" - + subtitleLabel.text = "잠시 후 다시 확인해 주세요!" } - + override func setupStyles() { titleLabel.font = .setFont(.title()) - + subtitleLabel.textColor = .gray subtitleLabel.font = .setFont(.body2()) - + textStackView.axis = .vertical textStackView.spacing = 12 textStackView.alignment = .center - + stackView.axis = .vertical stackView.spacing = 7 stackView.alignment = .center - + contentView.backgroundColor = .systemBackground } - + override func setupLayouts() { stackView.ezl.makeConstraint { $0.center(to: contentView) } - + imageView.ezl.makeConstraint { $0.size(with: 117) } diff --git a/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionViewCell/LargeBroadcastCollectionViewCell.swift b/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionViewCell/LargeBroadcastCollectionViewCell.swift index 252e1856..938c185f 100644 --- a/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionViewCell/LargeBroadcastCollectionViewCell.swift +++ b/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionViewCell/LargeBroadcastCollectionViewCell.swift @@ -6,44 +6,44 @@ import EasyLayout final class LargeBroadcastCollectionViewCell: BaseCollectionViewCell, ThumbnailViewContainer { let thumbnailView = ThumbnailView(with: .large) - + private let titleLabel = UILabel() private let descriptionLabel = UILabel() private let liveBadgeLabel = PaddingLabel() - + override func setupViews() { liveBadgeLabel.text = "L I V E" - + contentView.addSubview(thumbnailView) contentView.addSubview(titleLabel) contentView.addSubview(descriptionLabel) contentView.addSubview(liveBadgeLabel) } - + override func setupLayouts() { thumbnailView.ezl.makeConstraint { $0.top(to: contentView) .horizontal(to: contentView) .height(contentView.frame.width * 0.5625) } - + titleLabel.ezl.makeConstraint { $0.top(to: thumbnailView.imageView.ezl.bottom, offset: 6) .horizontal(to: contentView, padding: 20) } - + descriptionLabel.ezl.makeConstraint { $0.top(to: titleLabel.ezl.bottom, offset: 6) .horizontal(to: contentView, padding: 20) .bottom(to: contentView) } - + liveBadgeLabel.ezl.makeConstraint { $0.top(to: thumbnailView.imageView, offset: 12) .leading(to: thumbnailView.imageView, offset: 12) } } - + override func setupStyles() { liveBadgeLabel.textInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12) liveBadgeLabel.backgroundColor = DesignSystemAsset.Color.mainGreen.color @@ -51,20 +51,20 @@ final class LargeBroadcastCollectionViewCell: BaseCollectionViewCell, ThumbnailV liveBadgeLabel.font = .setFont(.caption1(weight: .bold)) liveBadgeLabel.layer.cornerRadius = 16 liveBadgeLabel.clipsToBounds = true - + titleLabel.font = .setFont(.body1()) titleLabel.numberOfLines = 2 titleLabel.lineBreakMode = .byWordWrapping - + descriptionLabel.font = .setFont(.body2()) descriptionLabel.textColor = .gray descriptionLabel.numberOfLines = 2 descriptionLabel.lineBreakMode = .byWordWrapping } - + func configure(channel: Channel) { - self.thumbnailView.configure(with: channel.thumbnailImageURLString) - self.titleLabel.text = channel.name - self.descriptionLabel.text = channel.owner + (channel.description.isEmpty ? "" : " • \(channel.description)") + thumbnailView.configure(with: channel.thumbnailImageURLString) + titleLabel.text = channel.name + descriptionLabel.text = channel.owner + (channel.description.isEmpty ? "" : " • \(channel.description)") } } diff --git a/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionViewCell/SmallBroadcastCollectionViewCell.swift b/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionViewCell/SmallBroadcastCollectionViewCell.swift index 89ac4adf..afaab956 100644 --- a/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionViewCell/SmallBroadcastCollectionViewCell.swift +++ b/Projects/Features/MainFeature/Sources/Views/BroadcastCollectionViewCell/SmallBroadcastCollectionViewCell.swift @@ -6,25 +6,25 @@ import EasyLayout final class SmallBroadcastCollectionViewCell: BaseCollectionViewCell, ThumbnailViewContainer { let thumbnailView = ThumbnailView(with: .small) - + private let descriptionStack = UIStackView() private let titleLabel = UILabel() private let ownerLabel = UILabel() private let descriptionLabel = UILabel() private let liveBadgeLabel = PaddingLabel() - + override func setupViews() { liveBadgeLabel.text = "L I V E" - + contentView.addSubview(thumbnailView) contentView.addSubview(liveBadgeLabel) contentView.addSubview(descriptionStack) - + descriptionStack.addArrangedSubview(titleLabel) descriptionStack.addArrangedSubview(ownerLabel) descriptionStack.addArrangedSubview(descriptionLabel) } - + override func setupLayouts() { thumbnailView.ezl.makeConstraint { $0.leading(to: contentView) @@ -32,19 +32,19 @@ final class SmallBroadcastCollectionViewCell: BaseCollectionViewCell, ThumbnailV .width(contentView.frame.width * 0.45) .height(contentView.frame.width * 0.45 * 0.5625) } - + descriptionStack.ezl.makeConstraint { $0.leading(to: thumbnailView.ezl.trailing) .trailing(to: contentView, offset: -16) .centerY(to: thumbnailView) } - + liveBadgeLabel.ezl.makeConstraint { $0.top(to: thumbnailView.imageView, offset: 8) .leading(to: thumbnailView.imageView, offset: 8) } } - + override func setupStyles() { liveBadgeLabel.textInsets = UIEdgeInsets(top: 4, left: 6, bottom: 4, right: 6) liveBadgeLabel.backgroundColor = DesignSystemAsset.Color.mainGreen.color @@ -52,25 +52,25 @@ final class SmallBroadcastCollectionViewCell: BaseCollectionViewCell, ThumbnailV liveBadgeLabel.font = .setFont(.caption2(weight: .bold)) liveBadgeLabel.layer.cornerRadius = 8 liveBadgeLabel.clipsToBounds = true - + descriptionStack.axis = .vertical descriptionStack.spacing = 4 - + titleLabel.font = .setFont(.body2()) titleLabel.numberOfLines = 2 - + ownerLabel.font = .setFont(.caption1()) ownerLabel.textColor = .gray - + descriptionLabel.font = .setFont(.caption1()) descriptionLabel.textColor = .gray descriptionLabel.numberOfLines = 2 } - + func configure(channel: Channel) { - self.thumbnailView.configure(with: channel.thumbnailImageURLString) - self.titleLabel.text = channel.name - self.ownerLabel.text = channel.owner - self.descriptionLabel.text = channel.description + thumbnailView.configure(with: channel.thumbnailImageURLString) + titleLabel.text = channel.name + ownerLabel.text = channel.owner + descriptionLabel.text = channel.description } } diff --git a/Projects/Features/MainFeature/Sources/Views/BroadcastThumbnailView.swift b/Projects/Features/MainFeature/Sources/Views/BroadcastThumbnailView.swift index bce34bf2..5d86df64 100644 --- a/Projects/Features/MainFeature/Sources/Views/BroadcastThumbnailView.swift +++ b/Projects/Features/MainFeature/Sources/Views/BroadcastThumbnailView.swift @@ -3,55 +3,62 @@ import UIKit import BaseFeature import EasyLayout +// MARK: - ThumbnailViewContainer + protocol ThumbnailViewContainer { var thumbnailView: ThumbnailView { get } } +// MARK: - ThumbnailView + final class ThumbnailView: BaseView { enum Size { - case large, small + case large + case small } - + enum Transition { - case present, dismiss + case present + case dismiss } - + let shadowView = UIView() let imageView = UIImageView() - + var size: Size - + init(with size: Size) { self.size = size super.init() } - - required init?(coder: NSCoder) { + + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func setupViews() { addSubview(shadowView) addSubview(imageView) } - + override func setupStyles() { imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true imageView.layer.cornerRadius = size == .large ? 16 : 12 } - + override func setupLayouts() { imageView.ezl.makeConstraint { $0.horizontal(to: self, padding: 16) .vertical(to: self) } - + shadowView.ezl.makeConstraint { $0.diagonal(to: self) } } - + func updateStyles(for transition: Transition) { if transition == .present { imageView.layer.cornerRadius = 0 @@ -59,10 +66,10 @@ final class ThumbnailView: BaseView { imageView.layer.cornerRadius = size == .large ? 16 : 12 } } - + func updateLayouts(for transition: Transition) { removeImageViewConstraints() - + if transition == .present { imageView.ezl.makeConstraint { $0.diagonal(to: self) @@ -74,7 +81,7 @@ final class ThumbnailView: BaseView { } } } - + func configure(with imageURLString: String) { guard let url = URL(string: imageURLString) else { return } URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in @@ -84,16 +91,16 @@ final class ThumbnailView: BaseView { } }.resume() } - + func configure(with image: UIImage?) { imageView.image = image } - + private func removeImageViewConstraints() { if let superview = imageView.superview { - superview.constraints.forEach { - if $0.firstItem as? UIView == imageView || $0.secondItem as? UIView == imageView { - superview.removeConstraint($0) + for constraint in superview.constraints { + if constraint.firstItem as? UIView == imageView || constraint.secondItem as? UIView == imageView { + superview.removeConstraint(constraint) } } } diff --git a/Projects/Features/MainFeature/Sources/Views/PaddingLabel.swift b/Projects/Features/MainFeature/Sources/Views/PaddingLabel.swift index 75430936..293ed4db 100644 --- a/Projects/Features/MainFeature/Sources/Views/PaddingLabel.swift +++ b/Projects/Features/MainFeature/Sources/Views/PaddingLabel.swift @@ -2,7 +2,7 @@ import UIKit final class PaddingLabel: UILabel { var textInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - + override func drawText(in rect: CGRect) { let insetRect = rect.inset(by: textInsets) super.drawText(in: insetRect) diff --git a/Projects/Features/MainFeature/Sources/Views/SettingTableViewCell.swift b/Projects/Features/MainFeature/Sources/Views/SettingTableViewCell.swift index 27df8493..49037527 100644 --- a/Projects/Features/MainFeature/Sources/Views/SettingTableViewCell.swift +++ b/Projects/Features/MainFeature/Sources/Views/SettingTableViewCell.swift @@ -4,6 +4,8 @@ import BaseFeature import DesignSystem import EasyLayout +// MARK: - SettingTableViewCell + final class SettingTableViewCell: BaseTableViewCell { private let infoInputStackView = UIStackView() private let titleLabel = UILabel() @@ -12,66 +14,66 @@ final class SettingTableViewCell: BaseTableViewCell { private var placeholderValue = "" private let errorMessageLabel = UILabel() private var textDidChange: ((String) -> Void)? - + func configure(label: String, placeholder: String, textDidChange: ((String) -> Void)?) { titleLabel.text = label - self.placeholderValue = placeholder - self.placeholderLabel.text = placeholderValue + placeholderValue = placeholder + placeholderLabel.text = placeholderValue self.textDidChange = textDidChange } - + override func setupViews() { inputTextView.delegate = self inputTextView.returnKeyType = .done errorMessageLabel.isHidden = true inputTextView.addSubview(placeholderLabel) - + infoInputStackView.addArrangedSubview(titleLabel) infoInputStackView.addArrangedSubview(inputTextView) - + contentView.addSubview(infoInputStackView) contentView.addSubview(errorMessageLabel) } - + override func setupStyles() { selectionStyle = .none - + infoInputStackView.axis = .horizontal infoInputStackView.spacing = 10 - + titleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) titleLabel.font = .setFont(.body1(weight: .semiBold)) titleLabel.textColor = .white - + inputTextView.textColor = .white inputTextView.textContainerInset = .zero inputTextView.isScrollEnabled = false inputTextView.font = .setFont(.body1(weight: .regular)) - + placeholderLabel.font = .setFont(.body1(weight: .regular)) placeholderLabel.textColor = DesignSystemAsset.Color.gray.color placeholderLabel.alpha = 0.5 - + errorMessageLabel.preferredMaxLayoutWidth = errorMessageLabel.frame.width errorMessageLabel.font = .setFont(.caption1(weight: .regular)) errorMessageLabel.numberOfLines = 0 errorMessageLabel.lineBreakMode = .byWordWrapping errorMessageLabel.textColor = DesignSystemAsset.Color.errorRed.color } - + override func setupLayouts() { infoInputStackView.ezl.makeConstraint { $0.horizontal(to: contentView, padding: 30) .top(to: contentView, offset: 27) } - + placeholderLabel.ezl.makeConstraint { $0.centerY(to: inputTextView) .leading(to: inputTextView, offset: 10) } - + errorMessageLabel.ezl.makeConstraint { $0.top(to: inputTextView.ezl.bottom, offset: 10) .leading(to: inputTextView) @@ -79,7 +81,7 @@ final class SettingTableViewCell: BaseTableViewCell { .bottom(to: contentView) } } - + func setErrorMessage(message: String?) { if let message, placeholderLabel.text != placeholderValue { errorMessageLabel.text = message @@ -91,18 +93,19 @@ final class SettingTableViewCell: BaseTableViewCell { } } -// MARK: Text View의 Delegate +// MARK: UITextViewDelegate + extension SettingTableViewCell: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { placeholderLabel.text = textView.text.isEmpty ? placeholderValue : "" textDidChange?(textView.text) - - guard let tableView = self.superview as? UITableView else { return } + + guard let tableView = superview as? UITableView else { return } tableView.beginUpdates() tableView.endUpdates() } - - func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + + func textView(_ textView: UITextView, shouldChangeTextIn _: NSRange, replacementText text: String) -> Bool { guard text == "\n" else { return true } textView.resignFirstResponder() return false diff --git a/Projects/Modules/ChatSoketModule/Demo/Sources/AppDelegate.swift b/Projects/Modules/ChatSoketModule/Demo/Sources/AppDelegate.swift index a6160580..ea2f8e8e 100644 --- a/Projects/Modules/ChatSoketModule/Demo/Sources/AppDelegate.swift +++ b/Projects/Modules/ChatSoketModule/Demo/Sources/AppDelegate.swift @@ -7,8 +7,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let viewController = SoketTestViewController() diff --git a/Projects/Modules/ChatSoketModule/Sources/Message/ChatMessage.swift b/Projects/Modules/ChatSoketModule/Sources/Message/ChatMessage.swift index 2272fd8e..0a4f499b 100644 --- a/Projects/Modules/ChatSoketModule/Sources/Message/ChatMessage.swift +++ b/Projects/Modules/ChatSoketModule/Sources/Message/ChatMessage.swift @@ -5,7 +5,7 @@ public struct ChatMessage: Codable { public let content: String? public let sender: String public let roomId: String - + public init(type: MessageType, content: String?, sender: String, roomId: String) { self.type = type self.content = content diff --git a/Projects/Modules/ChatSoketModule/Sources/SoketTestViewController.swift b/Projects/Modules/ChatSoketModule/Sources/SoketTestViewController.swift index ff5aea67..3f33b4c2 100644 --- a/Projects/Modules/ChatSoketModule/Sources/SoketTestViewController.swift +++ b/Projects/Modules/ChatSoketModule/Sources/SoketTestViewController.swift @@ -1,79 +1,79 @@ import UIKit +// MARK: - SoketTestViewController + public class SoketTestViewController: UIViewController { + var tableView: UITableView = .init() + var button: UIButton = .init() - var tableView: UITableView = UITableView() - var button: UIButton = UIButton() - var data: [String] = [] { didSet { - self.tableView.reloadData() + tableView.reloadData() } } - + var webSocket = WebSocket.shared - - public override func viewDidLoad() { + + override public func viewDidLoad() { super.viewDidLoad() button.backgroundColor = .systemBlue - + view.addSubview(tableView) view.addSubview(button) - + button.setTitle("보내기", for: .normal) button.setTitleColor(.white, for: .normal) - button.addAction(UIAction { [weak self] _ in + button.addAction(UIAction { [weak self] _ in guard let self else { return } - self.webSocket.send(data: ChatMessage(type: .CHAT, content: "HELLo", sender: "iOS", roomId: "1234")) + webSocket.send(data: ChatMessage(type: .CHAT, content: "HELLo", sender: "iOS", roomId: "1234")) }, for: .touchUpInside) - + tableView.translatesAutoresizingMaskIntoConstraints = false button.translatesAutoresizingMaskIntoConstraints = false - + NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: button.bottomAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) - + button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true - + tableView.dataSource = self - + try? webSocket.openWebSocket() webSocket.send(data: ChatMessage(type: .ENTER, content: "HELLo", sender: "iOS", roomId: "1234")) webSocket.delegate = self - - webSocket.receive {[weak self] data in + + webSocket.receive { [weak self] data in guard let self else { return } - + DispatchQueue.main.async { - guard let data else {return } + guard let data else { return } self.data.append(data.content ?? "") } } - } - } +// MARK: URLSessionWebSocketDelegate + extension SoketTestViewController: URLSessionWebSocketDelegate { - public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { - - } - public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { - - } + public func urlSession(_: URLSession, webSocketTask _: URLSessionWebSocketTask, didOpenWithProtocol _: String?) {} + + public func urlSession(_: URLSession, webSocketTask _: URLSessionWebSocketTask, didCloseWith _: URLSessionWebSocketTask.CloseCode, reason _: Data?) {} } +// MARK: UITableViewDataSource + extension SoketTestViewController: UITableViewDataSource { - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return data.count + public func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + data.count } - - public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + public func tableView(_: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell() var config = cell.defaultContentConfiguration() config.text = data[indexPath.row] diff --git a/Projects/Modules/ChatSoketModule/Sources/WebSocket.swift b/Projects/Modules/ChatSoketModule/Sources/WebSocket.swift index 8d3e7b65..adb8c940 100644 --- a/Projects/Modules/ChatSoketModule/Sources/WebSocket.swift +++ b/Projects/Modules/ChatSoketModule/Sources/WebSocket.swift @@ -1,30 +1,34 @@ import Foundation +// MARK: - WebSocketError + enum WebSocketError: Error { case invalidURL } +// MARK: - WebSocket + public final class WebSocket: NSObject { public static let shared = WebSocket() - + var url: URL? var onReceiveClosure: ((ChatMessage?) -> Void)? weak var delegate: URLSessionWebSocketDelegate? - + private var webSocketTask: URLSessionWebSocketTask? { didSet { oldValue?.cancel(with: .goingAway, reason: nil) } } - + private var timer: Timer? private let encoder: JSONEncoder = .init() private let decoder: JSONDecoder = .init() - - private override init() {} - + + override private init() {} + public func openWebSocket() throws { - url = URL(string: "ws:/\(host):\(port)/ws/chat" ) - guard let url = url else { throw WebSocketError.invalidURL } - + url = URL(string: "ws:/\(host):\(port)/ws/chat") + guard let url else { throw WebSocketError.invalidURL } + let urlSession = URLSession( configuration: .default, delegate: self, @@ -32,96 +36,100 @@ public final class WebSocket: NSObject { ) let webSocketTask = urlSession.webSocketTask(with: url) webSocketTask.resume() - + self.webSocketTask = webSocketTask - - self.startPing() + + startPing() } public func send(data: ChatMessage) { guard let data = try? encoder.encode(data) else { return } - + let taskMessage = URLSessionWebSocketTask.Message.data(data) - - self.webSocketTask?.send(taskMessage) { error in + + webSocketTask?.send(taskMessage) { error in guard error != nil else { return } } } - + public func closeWebSocket() { - self.timer?.invalidate() - self.onReceiveClosure = nil - self.delegate = nil - self.webSocketTask?.cancel(with: .goingAway, reason: nil) - self.webSocketTask = nil + timer?.invalidate() + onReceiveClosure = nil + delegate = nil + webSocketTask?.cancel(with: .goingAway, reason: nil) + webSocketTask = nil } - + public func receive(onReceive: @escaping ((ChatMessage?) -> Void)) { - self.onReceiveClosure = onReceive - self.webSocketTask?.receive { [weak self] result in + onReceiveClosure = onReceive + webSocketTask?.receive { [weak self] result in guard let self else { return } switch result { case let .success(message): switch message { case let .string(string): - guard let data = string.data(using: .utf8) else { + guard let data = string.data(using: .utf8) else { onReceive(nil) return } let message = try? decoder.decode(ChatMessage.self, from: data) onReceive(message) - + case let .data(data): let message = try? decoder.decode(ChatMessage.self, from: data) onReceive(message) - + @unknown default: onReceive(nil) } - + case .failure: - self.closeWebSocket() + closeWebSocket() } receive(onReceive: onReceive) } } - + private func startPing() { - self.timer?.invalidate() - self.timer = Timer.scheduledTimer( + timer?.invalidate() + timer = Timer.scheduledTimer( withTimeInterval: 10, - repeats: true) { [weak self] _ in - self?.ping() - } + repeats: true + ) { [weak self] _ in + self?.ping() + } } + private func ping() { - self.webSocketTask?.sendPing { [weak self] error in + webSocketTask?.sendPing { [weak self] error in guard error != nil else { return } self?.startPing() } } } +// MARK: URLSessionWebSocketDelegate + extension WebSocket: URLSessionWebSocketDelegate { public func urlSession( _ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String? ) { - self.delegate?.urlSession?( + delegate?.urlSession?( session, webSocketTask: webSocketTask, didOpenWithProtocol: `protocol` ) } - + public func urlSession( _ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data? ) { - self.delegate?.urlSession?( + delegate?.urlSession?( session, webSocketTask: webSocketTask, didCloseWith: closeCode, @@ -137,12 +145,12 @@ extension WebSocket { } return secrets[key] as? String ?? "not found key" } - + var host: String { - return config(key: "HOST") + config(key: "HOST") } - + var port: String { - return config(key: "PORT") + config(key: "PORT") } } diff --git a/Projects/Modules/EasyLayout/Demo/Sources/AppDelegate.swift b/Projects/Modules/EasyLayout/Demo/Sources/AppDelegate.swift index 89dea540..d1068791 100644 --- a/Projects/Modules/EasyLayout/Demo/Sources/AppDelegate.swift +++ b/Projects/Modules/EasyLayout/Demo/Sources/AppDelegate.swift @@ -5,8 +5,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let viewController = EasyLayoutDemoViewController() diff --git a/Projects/Modules/EasyLayout/Demo/Sources/ViewController.swift b/Projects/Modules/EasyLayout/Demo/Sources/ViewController.swift index a136bf6b..3b3d25c1 100644 --- a/Projects/Modules/EasyLayout/Demo/Sources/ViewController.swift +++ b/Projects/Modules/EasyLayout/Demo/Sources/ViewController.swift @@ -9,54 +9,54 @@ final class EasyLayoutDemoViewController: UIViewController { view.frame.origin = CGPoint(x: 0, y: 0) return view }() - + private let secondView: UIView = { let view = UIView() view.frame.origin = CGPoint(x: 0, y: 0) return view }() - + private let thirdView: UIView = { let view = UIView() view.backgroundColor = .blue view.frame.origin = CGPoint(x: 0, y: 0) return view }() - + init() { super.init(nibName: nil, bundle: nil) } - + @available(*, unavailable) - required init?(coder: NSCoder) { + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func viewDidLoad() { super.viewDidLoad() setupAttribute() setupSubViewsConstraints() } - + private func setupAttribute() {} - + private func setupSubViewsConstraints() { view.addSubview(firstView) view.addSubview(secondView) view.addSubview(thirdView) - + firstView.ezl.makeConstraint { $0.top(to: view.safeAreaLayoutGuide) .horizontal(to: view) .height(200) } - + secondView.ezl.makeConstraint { $0.top(to: firstView.ezl.bottom) .horizontal(to: view, padding: 40) .height(200) } - + thirdView.ezl.makeConstraint { $0.diagonal(to: view.safeAreaLayoutGuide, padding: 50) } diff --git a/Projects/Modules/EasyLayout/Sources/Anchor.swift b/Projects/Modules/EasyLayout/Sources/Anchor.swift index 439e697a..8d6d2fbe 100644 --- a/Projects/Modules/EasyLayout/Sources/Anchor.swift +++ b/Projects/Modules/EasyLayout/Sources/Anchor.swift @@ -1,11 +1,12 @@ import UIKit -// MARK: YAnchor +// MARK: - YAnchor + public struct YAnchor { enum Edge { case top(Anchorable) case bottom(Anchorable) - + var standard: NSLayoutYAxisAnchor { switch self { case let .top(view): view.topAnchor @@ -13,24 +14,25 @@ public struct YAnchor { } } } - + let edge: Edge - + static func top(_ view: Anchorable) -> Self { YAnchor(edge: .top(view)) } - + static func bottom(_ view: Anchorable) -> Self { YAnchor(edge: .bottom(view)) } } -// MARK: XAnchor +// MARK: - XAnchor + public struct XAnchor { enum Edge { case leading(Anchorable) case trailing(Anchorable) - + var standard: NSLayoutXAxisAnchor { switch self { case let .leading(view): view.leadingAnchor @@ -38,13 +40,13 @@ public struct XAnchor { } } } - + let edge: Edge - + static func leading(_ view: Anchorable) -> Self { XAnchor(edge: .leading(view)) } - + static func trailing(_ view: Anchorable) -> Self { XAnchor(edge: .trailing(view)) } diff --git a/Projects/Modules/EasyLayout/Sources/EasyConstraint.swift b/Projects/Modules/EasyLayout/Sources/EasyConstraint.swift index 885abfbc..94979ff5 100644 --- a/Projects/Modules/EasyLayout/Sources/EasyConstraint.swift +++ b/Projects/Modules/EasyLayout/Sources/EasyConstraint.swift @@ -2,56 +2,58 @@ import UIKit public struct EasyConstraint { let baseView: Anchorable - + init(_ baseView: Anchorable) { self.baseView = baseView guard let view = baseView as? UIView else { return } view.translatesAutoresizingMaskIntoConstraints = false } - + // MARK: About Size + @discardableResult public func width(_ width: CGFloat) -> Self { baseView.widthAnchor.constraint(equalToConstant: width).isActive = true return self } - + @discardableResult public func width(min width: CGFloat) -> Self { baseView.widthAnchor.constraint(greaterThanOrEqualToConstant: width).isActive = true return self } - + @discardableResult public func width(max width: CGFloat) -> Self { baseView.widthAnchor.constraint(lessThanOrEqualToConstant: width).isActive = true return self } - + @discardableResult public func height(_ height: CGFloat) -> Self { baseView.heightAnchor.constraint(equalToConstant: height).isActive = true return self } - + @discardableResult public func height(min height: CGFloat) -> Self { baseView.heightAnchor.constraint(greaterThanOrEqualToConstant: height).isActive = true return self } - + @discardableResult public func height(max height: CGFloat) -> Self { baseView.heightAnchor.constraint(lessThanOrEqualToConstant: height).isActive = true return self } - + @discardableResult public func size(with size: CGFloat) -> Self { width(size).height(size) } - + // MARK: About Position + @discardableResult public func top(to anchor: YAnchor, offset: CGFloat = 0) -> Self { baseView.topAnchor.constraint( @@ -60,12 +62,12 @@ public struct EasyConstraint { ).isActive = true return self } - + @discardableResult public func top(to view: Anchorable, offset: CGFloat = 0) -> Self { top(to: .top(view), offset: offset) } - + @discardableResult public func bottom(to anchor: YAnchor, offset: CGFloat = 0) -> Self { baseView.bottomAnchor.constraint( @@ -74,12 +76,12 @@ public struct EasyConstraint { ).isActive = true return self } - + @discardableResult public func bottom(to view: Anchorable, offset: CGFloat = 0) -> Self { bottom(to: .bottom(view), offset: offset) } - + @discardableResult public func leading(to anchor: XAnchor, offset: CGFloat = 0) -> Self { baseView.leadingAnchor.constraint( @@ -88,12 +90,12 @@ public struct EasyConstraint { ).isActive = true return self } - + @discardableResult public func leading(to view: Anchorable, offset: CGFloat = 0) -> Self { leading(to: .leading(view), offset: offset) } - + @discardableResult public func trailing(to anchor: XAnchor, offset: CGFloat = 0) -> Self { baseView.trailingAnchor.constraint( @@ -102,43 +104,44 @@ public struct EasyConstraint { ).isActive = true return self } - + @discardableResult public func trailing(to view: Anchorable, offset: CGFloat = 0) -> Self { trailing(to: .trailing(view), offset: offset) } - + @discardableResult public func horizontal(to view: Anchorable, padding: CGFloat = 0.0) -> Self { leading(to: .leading(view), offset: padding) .trailing(to: .trailing(view), offset: padding * -1) } - + @discardableResult public func vertical(to view: Anchorable, padding: CGFloat = 0.0) -> Self { top(to: .top(view), offset: padding) .bottom(to: .bottom(view), offset: padding * -1) } - + @discardableResult public func diagonal(to view: Anchorable, padding: CGFloat = 0.0) -> Self { horizontal(to: view, padding: padding) .vertical(to: view, padding: padding) } - + // MARK: About Center + @discardableResult public func centerX(to view: Anchorable) -> Self { baseView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true return self } - + @discardableResult public func centerY(to view: Anchorable) -> Self { baseView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true return self } - + @discardableResult public func center(to view: Anchorable) -> Self { centerX(to: view).centerY(to: view) diff --git a/Projects/Modules/EasyLayout/Sources/EasyLayout.swift b/Projects/Modules/EasyLayout/Sources/EasyLayout.swift index 97d80012..f7d2852e 100644 --- a/Projects/Modules/EasyLayout/Sources/EasyLayout.swift +++ b/Projects/Modules/EasyLayout/Sources/EasyLayout.swift @@ -1,26 +1,26 @@ public struct EasyLayout { private let constraint: EasyConstraint - + init(_ constraint: EasyConstraint) { self.constraint = constraint } - + public func makeConstraint(handler: (EasyConstraint) -> Void) { handler(constraint) } - + public var top: YAnchor { .top(constraint.baseView) } - + public var bottom: YAnchor { .bottom(constraint.baseView) } - + public var leading: XAnchor { .leading(constraint.baseView) } - + public var trailing: XAnchor { .trailing(constraint.baseView) } diff --git a/Projects/Modules/EasyLayout/Sources/Protocol/Anchorable.swift b/Projects/Modules/EasyLayout/Sources/Protocol/Anchorable.swift index 337310cb..bba887e5 100644 --- a/Projects/Modules/EasyLayout/Sources/Protocol/Anchorable.swift +++ b/Projects/Modules/EasyLayout/Sources/Protocol/Anchorable.swift @@ -1,5 +1,7 @@ import UIKit +// MARK: - Anchorable + public protocol Anchorable { var bottomAnchor: NSLayoutYAxisAnchor { get } var leadingAnchor: NSLayoutXAxisAnchor { get } @@ -11,12 +13,16 @@ public protocol Anchorable { var centerYAnchor: NSLayoutYAxisAnchor { get } } -extension Anchorable { - public var ezl: EasyLayout { +public extension Anchorable { + var ezl: EasyLayout { EasyLayout(EasyConstraint(self)) } } +// MARK: - UIView + Anchorable + extension UIView: Anchorable {} +// MARK: - UILayoutGuide + Anchorable + extension UILayoutGuide: Anchorable {} diff --git a/Projects/Modules/FastNetwork/Demo/Sources/AppDelegate.swift b/Projects/Modules/FastNetwork/Demo/Sources/AppDelegate.swift index ef2bae04..41a1c74f 100644 --- a/Projects/Modules/FastNetwork/Demo/Sources/AppDelegate.swift +++ b/Projects/Modules/FastNetwork/Demo/Sources/AppDelegate.swift @@ -5,8 +5,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let viewController = UIViewController() diff --git a/Projects/Modules/FastNetwork/Sources/Client/NetworkClient.swift b/Projects/Modules/FastNetwork/Sources/Client/NetworkClient.swift index 732373e5..c40666c3 100644 --- a/Projects/Modules/FastNetwork/Sources/Client/NetworkClient.swift +++ b/Projects/Modules/FastNetwork/Sources/Client/NetworkClient.swift @@ -2,15 +2,17 @@ import Foundation import FastNetworkInterface +// MARK: - NetworkClient + public final class NetworkClient: Requestable { private let session: URLSession private var interceptors: [any Interceptor] - - public init(session: URLSession = URLSession.shared, interceptors: [any Interceptor] = [] ) { + + public init(session: URLSession = URLSession.shared, interceptors: [any Interceptor] = []) { self.session = session self.interceptors = interceptors } - + public func request(_ endpoint: E) async throws -> Response { var request = try configureURLRequest(from: endpoint) request = try interceptRequest(with: request, from: endpoint) @@ -24,42 +26,42 @@ private extension NetworkClient { #warning("캐싱 정책 나중에 설정") var request = URLRequest(url: requestURL, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: endpoint.timeout) request.httpMethod = endpoint.method.description - + try endpoint.requestTask.configureRequest(request: &request) request.allHTTPHeaderFields = endpoint.header - + return request } - + func requestNetworkTask(with request: URLRequest, from endpoint: E) async throws -> Response { let (data, urlResponse) = try await session.data(for: request) let response = Response(request: request, data: data, response: urlResponse) try interceptResponse(with: response, from: endpoint) - + guard let httpResponse = urlResponse as? HTTPURLResponse else { throw NetworkError.invaildResponse } - + let statusCode = httpResponse.statusCode - + if !(endpoint.validationCode ~= statusCode) { throw HTTPError(statuscode: statusCode) } - + return response } - + func interceptRequest(with request: URLRequest, from endpoint: E) throws -> URLRequest { var request = request - - for interceptor in self.interceptors { + + for interceptor in interceptors { try interceptor.willRequest(request, from: endpoint) request = try interceptor.prepare(request, from: endpoint) } - + return request } - + func interceptResponse(with response: Response, from endpoint: E) throws { - for interceptor in self.interceptors { + for interceptor in interceptors { try interceptor.didReceive(response, from: endpoint) } } diff --git a/Projects/Modules/FastNetwork/Sources/Client/Requestable.swift b/Projects/Modules/FastNetwork/Sources/Client/Requestable.swift index 34302938..cc20f785 100644 --- a/Projects/Modules/FastNetwork/Sources/Client/Requestable.swift +++ b/Projects/Modules/FastNetwork/Sources/Client/Requestable.swift @@ -2,6 +2,6 @@ import Foundation protocol Requestable { associatedtype E: Endpoint - + func request(_ endpoint: E) async throws -> Response } diff --git a/Projects/Modules/FastNetwork/Sources/Endpoint.swift b/Projects/Modules/FastNetwork/Sources/Endpoint.swift index 3a3c2c38..ebfb14f3 100644 --- a/Projects/Modules/FastNetwork/Sources/Endpoint.swift +++ b/Projects/Modules/FastNetwork/Sources/Endpoint.swift @@ -1,5 +1,7 @@ import Foundation +// MARK: - Endpoint + public protocol Endpoint { var method: HTTPMethod { get } var header: [String: String]? { get } @@ -14,7 +16,7 @@ public protocol Endpoint { public extension Endpoint { var scheme: String { "https" } - var validationCode: ClosedRange { 200...500 } + var validationCode: ClosedRange { 200 ... 500 } var timeout: TimeInterval { 300 } var port: Int? { nil } } diff --git a/Projects/Modules/FastNetwork/Sources/Error/HTTPError.swift b/Projects/Modules/FastNetwork/Sources/Error/HTTPError.swift index 93d8eb5c..8c0ee8a1 100644 --- a/Projects/Modules/FastNetwork/Sources/Error/HTTPError.swift +++ b/Projects/Modules/FastNetwork/Sources/Error/HTTPError.swift @@ -1,8 +1,8 @@ import Foundation public enum HTTPError: String, LocalizedError { - // MARK: 400..<500 , Client Error + case badRequest /// 400 case unauthorized /// 401 case paymentRequired /// 402 @@ -10,14 +10,16 @@ public enum HTTPError: String, LocalizedError { case notFound /// 404 case methodNotAllowed /// 405 case conflict /// 409 - + // MARK: 500..<600 Server Error + case internalServerError /// 500 case badGateway /// 502 - + // MARK: Extra + case underlying - + init(statuscode: Int) { switch statuscode { case 400: self = .badRequest @@ -32,6 +34,6 @@ public enum HTTPError: String, LocalizedError { default: self = .underlying } } - + public var errorDescription: String? { rawValue } } diff --git a/Projects/Modules/FastNetwork/Sources/Error/NetworkError.swift b/Projects/Modules/FastNetwork/Sources/Error/NetworkError.swift index 792037a8..ad0509be 100644 --- a/Projects/Modules/FastNetwork/Sources/Error/NetworkError.swift +++ b/Projects/Modules/FastNetwork/Sources/Error/NetworkError.swift @@ -4,12 +4,12 @@ enum NetworkError: LocalizedError { case invaildURL case invaildResponse case jsonEncodingFailed(Error) - + var errorDescription: String? { switch self { case .invaildURL: "유효한 URL을 찾을 수 없습니다." case .invaildResponse: "유효한 응답이 아닙니다." - case let .jsonEncodingFailed(error): "JSON 인코딩에 실패했습니다: \(error.localizedDescription)" + case let .jsonEncodingFailed(error): "JSON 인코딩에 실패했습니다: \(error.localizedDescription)" } } } diff --git a/Projects/Modules/FastNetwork/Sources/Extensions/URL+Extension.swift b/Projects/Modules/FastNetwork/Sources/Extensions/URL+Extension.swift index 810e6cc5..89b4d317 100644 --- a/Projects/Modules/FastNetwork/Sources/Extensions/URL+Extension.swift +++ b/Projects/Modules/FastNetwork/Sources/Extensions/URL+Extension.swift @@ -8,11 +8,11 @@ public extension URL { urlComponents.path = endpoint.path urlComponents.port = endpoint.port #warning("포트 번호 추후 삭제") - + guard let url = urlComponents.url else { throw NetworkError.invaildURL } - + self = url } } diff --git a/Projects/Modules/FastNetwork/Sources/Interceptor/DefaultLoggingInterceptor.swift b/Projects/Modules/FastNetwork/Sources/Interceptor/DefaultLoggingInterceptor.swift index c763d345..e976ea8e 100644 --- a/Projects/Modules/FastNetwork/Sources/Interceptor/DefaultLoggingInterceptor.swift +++ b/Projects/Modules/FastNetwork/Sources/Interceptor/DefaultLoggingInterceptor.swift @@ -3,86 +3,85 @@ import OSLog #if DEBUG -public final class DefaultLoggingInterceptor: Interceptor { - private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "NETWORK") - - public init() {} - - public func willRequest(_ request: URLRequest, from endpoint: any Endpoint) throws { - guard let url = request.url else { throw NetworkError.invaildURL } - let method = endpoint.method - - var log = "====================\n\n[\(method)] \(url)\n\n====================\n" - log.append("✅ Endpoint: \(endpoint)\n") - - log.append( "------------------- Header -------------------\n") - if let header = endpoint.header, !header.isEmpty { - log.append("✅ Header: \(header)\n") - } - log.append( "------------------- Header END -------------------\n") - - if let body = request.httpBody, !body.isEmpty, let bodyString = String(bytes: body, encoding: .utf8) { - log.append("✅ Body: \(bodyString)\n") - } - - log.append("------------------- END \(method) --------------------------\n") - logger.log(level: .debug, "\(log)") - } - - public func didReceive(_ response: Response, from endpoint: any Endpoint) throws { - - guard let httpResponse = response.response as? HTTPURLResponse else { - throw NetworkError.invaildResponse + public final class DefaultLoggingInterceptor: Interceptor { + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "NETWORK") + + public init() {} + + public func willRequest(_ request: URLRequest, from endpoint: any Endpoint) throws { + guard let url = request.url else { throw NetworkError.invaildURL } + let method = endpoint.method + + var log = "====================\n\n[\(method)] \(url)\n\n====================\n" + log.append("✅ Endpoint: \(endpoint)\n") + + log.append("------------------- Header -------------------\n") + if let header = endpoint.header, !header.isEmpty { + log.append("✅ Header: \(header)\n") + } + log.append("------------------- Header END -------------------\n") + + if let body = request.httpBody, !body.isEmpty, let bodyString = String(bytes: body, encoding: .utf8) { + log.append("✅ Body: \(bodyString)\n") + } + + log.append("------------------- END \(method) --------------------------\n") + logger.log(level: .debug, "\(log)") } - - switch httpResponse.statusCode { - case endpoint.validationCode: - onSucceed(response, from: endpoint) - - default: - onFail(response, from: endpoint) + + public func didReceive(_ response: Response, from endpoint: any Endpoint) throws { + guard let httpResponse = response.response as? HTTPURLResponse else { + throw NetworkError.invaildResponse + } + + switch httpResponse.statusCode { + case endpoint.validationCode: + onSucceed(response, from: endpoint) + + default: + onFail(response, from: endpoint) + } } } -} - -private extension DefaultLoggingInterceptor { - func onSucceed(_ response: Response, from endpoint: any Endpoint) { - let request = response.request - let url = request.url?.absoluteString ?? "nil" - guard let httpResponse = response.response as? HTTPURLResponse else { return } - let statusCode = httpResponse.statusCode - - var log = "------------------- 네트워크 통신 성공 -------------------" - log.append("\n[✅ Status Codee: \(statusCode)] \(url)\n----------------------------------------------------\n") - log.append("✅ Endpoint: \(endpoint)\n") - - log.append( "------------------- Header -------------------\n") - httpResponse.allHeaderFields.forEach { - log.append("\($0): \($1)\n") + + private extension DefaultLoggingInterceptor { + func onSucceed(_ response: Response, from endpoint: any Endpoint) { + let request = response.request + let url = request.url?.absoluteString ?? "nil" + guard let httpResponse = response.response as? HTTPURLResponse else { return } + let statusCode = httpResponse.statusCode + + var log = "------------------- 네트워크 통신 성공 -------------------" + log.append("\n[✅ Status Codee: \(statusCode)] \(url)\n----------------------------------------------------\n") + log.append("✅ Endpoint: \(endpoint)\n") + + log.append("------------------- Header -------------------\n") + httpResponse.allHeaderFields.forEach { + log.append("\($0): \($1)\n") + } + log.append("------------------- Header END -------------------\n") + + log.append("------------------- Body -------------------\n") + if let resDataString = String(bytes: response.data, encoding: String.Encoding.utf8) { + log.append("\(resDataString)\n") + } + log.append("------------------- Body END -------------------\n") + + log.append("------------------- END HTTP (\(response.data.count)-byte body) -------------------\n") + logger.log(level: .debug, "\(log)") } - log.append( "------------------- Header END -------------------\n") - - log.append( "------------------- Body -------------------\n") - if let resDataString = String(bytes: response.data, encoding: String.Encoding.utf8) { - log.append("\(resDataString)\n") + + func onFail(_ response: Response, from endpoint: any Endpoint) { + guard let httpResponse = response.response as? HTTPURLResponse else { return } + let statusCode = httpResponse.statusCode + let error = HTTPError(statuscode: statusCode) + + var log = "네트워크 오류" + log.append("<-- ❌ \(statusCode) \(error) \(endpoint)\n") + log.append("<-- END HTTP\n") + + logger.log(level: .error, "\(log)") } - log.append( "------------------- Body END -------------------\n") - - log.append("------------------- END HTTP (\(response.data.count)-byte body) -------------------\n") - logger.log(level: .debug, "\(log)") - } - - func onFail(_ response: Response, from endpoint: any Endpoint) { - guard let httpResponse = response.response as? HTTPURLResponse else { return } - let statusCode = httpResponse.statusCode - let error = HTTPError(statuscode: statusCode) - - var log = "네트워크 오류" - log.append("<-- ❌ \(statusCode) \(error) \(endpoint)\n") - log.append("<-- END HTTP\n") - - logger.log(level: .error, "\(log)") } -} #endif diff --git a/Projects/Modules/FastNetwork/Sources/Interceptor/Interceptor.swift b/Projects/Modules/FastNetwork/Sources/Interceptor/Interceptor.swift index 12f46519..61439e57 100644 --- a/Projects/Modules/FastNetwork/Sources/Interceptor/Interceptor.swift +++ b/Projects/Modules/FastNetwork/Sources/Interceptor/Interceptor.swift @@ -1,5 +1,7 @@ import Foundation +// MARK: - Interceptor + public protocol Interceptor { func prepare(_ request: URLRequest, from endpoint: Endpoint) throws -> URLRequest func willRequest(_ request: URLRequest, from endpoint: Endpoint) throws @@ -7,7 +9,7 @@ public protocol Interceptor { } public extension Interceptor { - func prepare(_ request: URLRequest, from endpoint: Endpoint) throws -> URLRequest { request } - func willRequest(_ request: URLRequest, from endpoint: Endpoint) throws { } - func didReceive(_ response: Response, from endpoint: Endpoint) throws { } + func prepare(_ request: URLRequest, from _: Endpoint) throws -> URLRequest { request } + func willRequest(_: URLRequest, from _: Endpoint) throws {} + func didReceive(_: Response, from _: Endpoint) throws {} } diff --git a/Projects/Modules/FastNetwork/Sources/Request/Components/HTTPMethod.swift b/Projects/Modules/FastNetwork/Sources/Request/Components/HTTPMethod.swift index d7fac94d..5faa2969 100644 --- a/Projects/Modules/FastNetwork/Sources/Request/Components/HTTPMethod.swift +++ b/Projects/Modules/FastNetwork/Sources/Request/Components/HTTPMethod.swift @@ -8,6 +8,6 @@ public enum HTTPMethod: String, CustomStringConvertible { case options case trace case patch - + public var description: String { rawValue.uppercased() } } diff --git a/Projects/Modules/FastNetwork/Sources/Request/Components/RequestTask.swift b/Projects/Modules/FastNetwork/Sources/Request/Components/RequestTask.swift index a483aee1..a3adb461 100644 --- a/Projects/Modules/FastNetwork/Sources/Request/Components/RequestTask.swift +++ b/Projects/Modules/FastNetwork/Sources/Request/Components/RequestTask.swift @@ -1,5 +1,7 @@ import Foundation +// MARK: - RequestTask + public enum RequestTask { case empty case withParameters( @@ -20,7 +22,7 @@ extension RequestTask { switch self { case .empty: request.setValue("application/json", forHTTPHeaderField: "Content-Type") - + case let .withParameters(body, query, bodyEncoder, urlQueryEncoder): try configureParam( request: &request, @@ -29,7 +31,7 @@ extension RequestTask { bodyEncoder: bodyEncoder, urlQueryEncoder: urlQueryEncoder ) - + case let .withObject(body, query, urlQueryEncoder): try configureObject( request: &request, @@ -39,7 +41,7 @@ extension RequestTask { ) } } - + func configureParam( request: inout URLRequest, body: Parameters?, @@ -50,12 +52,12 @@ extension RequestTask { if let body { try bodyEncoder.encode(request: &request, with: body) } - + if let query { try urlQueryEncoder.encode(request: &request, with: query) } } - + func configureObject( request: inout URLRequest, body: any Encodable, @@ -64,7 +66,7 @@ extension RequestTask { ) throws { request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONEncoder().encode(body) - + if let query { try urlQueryEncoder.encode(request: &request, with: query) } diff --git a/Projects/Modules/FastNetwork/Sources/Request/Encoding/ParamterJSONEncoder.swift b/Projects/Modules/FastNetwork/Sources/Request/Encoding/ParamterJSONEncoder.swift index 1ef4ba03..4289ec2e 100644 --- a/Projects/Modules/FastNetwork/Sources/Request/Encoding/ParamterJSONEncoder.swift +++ b/Projects/Modules/FastNetwork/Sources/Request/Encoding/ParamterJSONEncoder.swift @@ -1,5 +1,7 @@ import Foundation +// MARK: - ParamterJSONEncoder + public struct ParamterJSONEncoder: RequestParameterEncodable { public func encode(request: inout URLRequest, with parameters: Parameters) throws { do { diff --git a/Projects/Modules/FastNetwork/Sources/Request/Encoding/RequestParameterEncodable.swift b/Projects/Modules/FastNetwork/Sources/Request/Encoding/RequestParameterEncodable.swift index 936431c2..363d0bdb 100644 --- a/Projects/Modules/FastNetwork/Sources/Request/Encoding/RequestParameterEncodable.swift +++ b/Projects/Modules/FastNetwork/Sources/Request/Encoding/RequestParameterEncodable.swift @@ -2,6 +2,8 @@ import Foundation public typealias Parameters = [String: Any] +// MARK: - RequestParameterEncodable + public protocol RequestParameterEncodable { func encode(request: inout URLRequest, with parameters: Parameters) throws } diff --git a/Projects/Modules/FastNetwork/Sources/Request/Encoding/URLQueryEncoder.swift b/Projects/Modules/FastNetwork/Sources/Request/Encoding/URLQueryEncoder.swift index 25f71673..107d9f7c 100644 --- a/Projects/Modules/FastNetwork/Sources/Request/Encoding/URLQueryEncoder.swift +++ b/Projects/Modules/FastNetwork/Sources/Request/Encoding/URLQueryEncoder.swift @@ -1,10 +1,12 @@ import Foundation +// MARK: - URLQueryEncoder + public struct URLQueryEncoder: RequestParameterEncodable { #warning("배열 query value는 추후 구현") public func encode(request: inout URLRequest, with parameters: Parameters) throws { guard let url = request.url else { throw NetworkError.invaildURL } - + if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty { urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: "\($0.value)".urlQueryAllowed) @@ -16,7 +18,7 @@ public struct URLQueryEncoder: RequestParameterEncodable { private extension String { var urlQueryAllowed: String? { - self.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) + addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) } } diff --git a/Projects/Modules/FastNetwork/Testing/MockData.swift b/Projects/Modules/FastNetwork/Testing/MockData.swift index eea714c4..2be96db9 100644 --- a/Projects/Modules/FastNetwork/Testing/MockData.swift +++ b/Projects/Modules/FastNetwork/Testing/MockData.swift @@ -1,27 +1,27 @@ import Foundation let mockData = Data( -""" -[ - { - "title": "Black Coffee", - "description": "Svart kaffe är så enkelt som det kan bli med malda kaffebönor dränkta i hett vatten, serverat varmt. Och om du vill låta fancy kan du kalla svart kaffe med sitt rätta namn: café noir.", - "ingredients": [ - "Coffee" - ], - "image": "https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", - "id": 1 - }, - { - "title": "Latte", - "description": "Som den mest populära kaffedrycken där ute består latte av en skvätt espresso och ångad mjölk med bara en gnutta skum. Den kan beställas utan smak eller med smak av allt från vanilj till pumpa kryddor.", - "ingredients": [ - "Espresso", - "Ångad mjölk" - ], - "image": "https://images.unsplash.com/photo-1561882468-9110e03e0f78?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGxhdHRlfGVufDB8fDB8fHww", - "id": 2 - } -] -""".utf8 + """ + [ + { + "title": "Black Coffee", + "description": "Svart kaffe är så enkelt som det kan bli med malda kaffebönor dränkta i hett vatten, serverat varmt. Och om du vill låta fancy kan du kalla svart kaffe med sitt rätta namn: café noir.", + "ingredients": [ + "Coffee" + ], + "image": "https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "id": 1 + }, + { + "title": "Latte", + "description": "Som den mest populära kaffedrycken där ute består latte av en skvätt espresso och ångad mjölk med bara en gnutta skum. Den kan beställas utan smak eller med smak av allt från vanilj till pumpa kryddor.", + "ingredients": [ + "Espresso", + "Ångad mjölk" + ], + "image": "https://images.unsplash.com/photo-1561882468-9110e03e0f78?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGxhdHRlfGVufDB8fDB8fHww", + "id": 2 + } + ] + """.utf8 ) diff --git a/Projects/Modules/FastNetwork/Testing/MockURLProtocol.swift b/Projects/Modules/FastNetwork/Testing/MockURLProtocol.swift index 61cddb6d..f9becb65 100644 --- a/Projects/Modules/FastNetwork/Testing/MockURLProtocol.swift +++ b/Projects/Modules/FastNetwork/Testing/MockURLProtocol.swift @@ -3,25 +3,25 @@ import Foundation final class MockURLProtocol: URLProtocol { static var mockData: Data? static var mockResponse: HTTPURLResponse? - + private(set) static var mockRequest: URLRequest? - - override static func canInit(with request: URLRequest) -> Bool { true } + + override static func canInit(with _: URLRequest) -> Bool { true } override static func canonicalRequest(for request: URLRequest) -> URLRequest { request } - + override func startLoading() { MockURLProtocol.mockRequest = request - + if let response = MockURLProtocol.mockResponse { client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) } - + if let data = MockURLProtocol.mockData { client?.urlProtocol(self, didLoad: data) } - + client?.urlProtocolDidFinishLoading(self) } - + override func stopLoading() {} } diff --git a/Projects/Modules/FastNetwork/Tests/MockEndpoint.swift b/Projects/Modules/FastNetwork/Tests/MockEndpoint.swift index 5eea8172..cf3d8369 100644 --- a/Projects/Modules/FastNetwork/Tests/MockEndpoint.swift +++ b/Projects/Modules/FastNetwork/Tests/MockEndpoint.swift @@ -2,43 +2,47 @@ import Foundation import FastNetwork +// MARK: - MockEndpoint + enum MockEndpoint { case fetch case getwithParameters(queryParams: [String: Any], bodyParams: [String: Any]) } +// MARK: Endpoint + extension MockEndpoint: Endpoint { var method: FastNetwork.HTTPMethod { switch self { case .fetch, .getwithParameters: .get } } - + var header: [String: String]? { switch self { case .fetch: nil - + case .getwithParameters: ["임시 헤더": "shookHeader"] } } - + var host: String { "www.example.com" } - + var path: String { switch self { - case .fetch, .getwithParameters: return "/fetch" + case .fetch, .getwithParameters: "/fetch" } } - + var requestTask: FastNetwork.RequestTask { switch self { case .fetch: .empty - + case let .getwithParameters(queryParams, bodyParams): - .withParameters(body: bodyParams, query: queryParams) + .withParameters(body: bodyParams, query: queryParams) } } - - var validationCode: ClosedRange { 200...299 } + + var validationCode: ClosedRange { 200 ... 299 } } diff --git a/Projects/Modules/FastNetwork/Tests/NetworkClientTest.swift b/Projects/Modules/FastNetwork/Tests/NetworkClientTest.swift index f8c4262b..6467a12f 100644 --- a/Projects/Modules/FastNetwork/Tests/NetworkClientTest.swift +++ b/Projects/Modules/FastNetwork/Tests/NetworkClientTest.swift @@ -6,7 +6,7 @@ import XCTest final class NetworkClientTest: XCTestCase { private var interceptors: [any Interceptor]! private var client: NetworkClient! - + override func setUp() { super.setUp() interceptors = [DefaultLoggingInterceptor()] @@ -15,30 +15,32 @@ final class NetworkClientTest: XCTestCase { let session = URLSession(configuration: configuration) client = NetworkClient(session: session, interceptors: interceptors) } - + override func tearDown() { super.tearDown() } // MARK: - Success + func test_success_response() async throws { MockURLProtocol.mockData = mockData MockURLProtocol.mockResponse = mockSuccessResponse - + let mockEndpoint = MockEndpoint.fetch let response = try await client.request(mockEndpoint) let request = MockURLProtocol.mockRequest - + guard let httpResponse = response.response as? HTTPURLResponse else { return XCTFail("HTTP 응답이 아닙니다.") } - + XCTAssertEqual(request?.url?.absoluteString, "https://www.example.com/fetch") XCTAssertEqual(httpResponse.statusCode, 200) XCTAssertEqual(response.data, MockURLProtocol.mockData) } - + // MARK: - BadGateway + func test_bad_gateway_response() async throws { MockURLProtocol.mockResponse = mockBadGatewayResponse let mockEndpoint = MockEndpoint.fetch @@ -55,6 +57,7 @@ final class NetworkClientTest: XCTestCase { } // MARK: - BadRequest + func test_bad_request_response() async throws { MockURLProtocol.mockResponse = mockBadRequestResponse let mockEndpoint = MockEndpoint.fetch @@ -69,15 +72,15 @@ final class NetworkClientTest: XCTestCase { let expectation = HTTPError.badRequest XCTAssertEqual(result, expectation) } - + func test_query_and_body_withParams() async throws { MockURLProtocol.mockData = mockData MockURLProtocol.mockResponse = mockSuccessResponse let mockEndpoint = MockEndpoint.getwithParameters(queryParams: ["sort": "asc"], bodyParams: ["age": 1]) - + let response = try await client.request(mockEndpoint) guard let httpResponse = response.response as? HTTPURLResponse else { return XCTFail("HTTP 응답이 아닙니다.") } - + XCTAssertEqual(httpResponse.statusCode, 200) XCTAssertEqual(response.data, mockData) } diff --git a/Projects/Modules/FastNetwork/Tests/NetworkEncoderTests.swift b/Projects/Modules/FastNetwork/Tests/NetworkEncoderTests.swift index 2c2d64fa..301a5337 100644 --- a/Projects/Modules/FastNetwork/Tests/NetworkEncoderTests.swift +++ b/Projects/Modules/FastNetwork/Tests/NetworkEncoderTests.swift @@ -2,17 +2,21 @@ import XCTest @testable import FastNetwork +// MARK: - Body + private struct Body: Encodable { let name: String } +// MARK: - NetworkEncoderTests + final class NetworkEncoderTests: XCTestCase { let jsonEncoder: JSONEncoder = .init() - + func test_query_encoder() throws { var request = URLRequest(url: URL(string: "https://example.com")!) let encoder = URLQueryEncoder() - + try encoder.encode(request: &request, with: ["key1": "value1 ! #!@$**", "key2": 123, "key3": true]) var components = URLComponents(string: "https://example.com") components?.queryItems = [ @@ -22,21 +26,20 @@ final class NetworkEncoderTests: XCTestCase { ] XCTAssertEqual(request.url?.absoluteString.sorted(), components?.url?.absoluteString.sorted()) } - + func test_json_encoder() throws { // 우리가 만든 인코더 이용 var request1 = URLRequest(url: URL(string: "https://example.com")!) let encoder = ParamterJSONEncoder() try encoder.encode(request: &request1, with: ["name": "shook"]) - + // struct + json ecoder 이용 var request2 = URLRequest(url: URL(string: "https://example.com")!) - + let data = try jsonEncoder.encode(Body(name: "shook")) request2.addValue("application/json", forHTTPHeaderField: "Content-Type") request2.httpBody = data - + XCTAssertEqual(request1, request2) } - } diff --git a/Projects/UserInterfaces/DesignSystem/Sources/SHFontSystem.swift b/Projects/UserInterfaces/DesignSystem/Sources/SHFontSystem.swift index 99888523..2a37a5ca 100644 --- a/Projects/UserInterfaces/DesignSystem/Sources/SHFontSystem.swift +++ b/Projects/UserInterfaces/DesignSystem/Sources/SHFontSystem.swift @@ -1,5 +1,7 @@ import UIKit +// MARK: - SHFontable + protocol SHFontable { var font: UIFont { get } } @@ -12,55 +14,56 @@ public extension UIFont { case body3(weight: SHFontWeight = .medium) case caption1(weight: SHFontWeight = .regular) case caption2(weight: SHFontWeight = .regular) - } - + static func setFont(_ style: SHFontSystem) -> UIFont { - return style.font + style.font } } public extension UIFont.SHFontSystem { enum SHFontWeight { - case bold, semiBold, regular, medium + case bold + case semiBold + case regular + case medium } - + var font: UIFont { - return UIFont(font: weight.font, size: size) ?? .init() + UIFont(font: weight.font, size: size) ?? .init() } - + var weight: SHFontWeight { switch self { - case let .title(weight), - let .body1(weight), - let .body2(weight), - let .body3(weight), - let .caption1(weight), - let .caption2(weight): - return weight + case let .body1(weight), + let .body2(weight), + let .body3(weight), + let .caption1(weight), + let .caption2(weight), + let .title(weight): + weight } } - + var size: CGFloat { switch self { - case .title: return 22 - case .body1: return 17 - case .body2: return 16 - case .body3: return 14 - case .caption1: return 12 - case .caption2: return 10 + case .title: 22 + case .body1: 17 + case .body2: 16 + case .body3: 14 + case .caption1: 12 + case .caption2: 10 } } - } private extension UIFont.SHFontSystem.SHFontWeight { var font: DesignSystemFontConvertible { switch self { - case .bold: return DesignSystemFontFamily.PretendardVariable.bold - case .semiBold: return DesignSystemFontFamily.PretendardVariable.semiBold - case .regular: return DesignSystemFontFamily.PretendardVariable.regular - case .medium: return DesignSystemFontFamily.PretendardVariable.medium + case .bold: DesignSystemFontFamily.PretendardVariable.bold + case .semiBold: DesignSystemFontFamily.PretendardVariable.semiBold + case .regular: DesignSystemFontFamily.PretendardVariable.regular + case .medium: DesignSystemFontFamily.PretendardVariable.medium } } } diff --git a/Projects/UserInterfaces/DesignSystem/Sources/SHLoadingView.swift b/Projects/UserInterfaces/DesignSystem/Sources/SHLoadingView.swift index 0ac3d245..a562bc8e 100644 --- a/Projects/UserInterfaces/DesignSystem/Sources/SHLoadingView.swift +++ b/Projects/UserInterfaces/DesignSystem/Sources/SHLoadingView.swift @@ -10,14 +10,14 @@ public final class SHLoadingView: UIView { view.translatesAutoresizingMaskIntoConstraints = false return view }() - + private let animationView: LottieAnimationView = { let animation = LottieAnimationView(name: "loading", bundle: Bundle(for: DesignSystemResources.self)) animation.loopMode = .loop animation.translatesAutoresizingMaskIntoConstraints = false return animation }() - + private let messageLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 16, weight: .medium) @@ -26,7 +26,7 @@ public final class SHLoadingView: UIView { label.translatesAutoresizingMaskIntoConstraints = false return label }() - + public init(message: String) { self.message = message super.init(frame: .zero) @@ -35,19 +35,20 @@ public final class SHLoadingView: UIView { setupStyles() animationView.play() } - - required init?(coder: NSCoder) { + + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func setupViews() { addSubview(blurEffectView) addSubview(animationView) addSubview(messageLabel) - + messageLabel.text = message } - + private func setupConstraints() { NSLayoutConstraint.activate([ blurEffectView.centerXAnchor.constraint(equalTo: centerXAnchor), @@ -55,20 +56,20 @@ public final class SHLoadingView: UIView { blurEffectView.widthAnchor.constraint(equalToConstant: 200), blurEffectView.heightAnchor.constraint(equalToConstant: 200) ]) - + NSLayoutConstraint.activate([ animationView.centerXAnchor.constraint(equalTo: centerXAnchor), animationView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -24), animationView.widthAnchor.constraint(equalToConstant: 300), animationView.heightAnchor.constraint(equalToConstant: 150) ]) - + NSLayoutConstraint.activate([ messageLabel.centerXAnchor.constraint(equalTo: centerXAnchor), messageLabel.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 56) ]) } - + private func setupStyles() { blurEffectView.layer.cornerRadius = 24 blurEffectView.clipsToBounds = true diff --git a/Projects/UserInterfaces/DesignSystem/Sources/SHRefreshControl.swift b/Projects/UserInterfaces/DesignSystem/Sources/SHRefreshControl.swift index c46d6e1e..e5f8fc68 100644 --- a/Projects/UserInterfaces/DesignSystem/Sources/SHRefreshControl.swift +++ b/Projects/UserInterfaces/DesignSystem/Sources/SHRefreshControl.swift @@ -4,26 +4,27 @@ import Lottie public final class SHRefreshControl: UIRefreshControl { private let animationView = LottieAnimationView(name: "shook", bundle: Bundle(for: DesignSystemResources.self)) - - public override init() { + + override public init() { super.init(frame: .zero) setupView() setupLayout() } - - required init?(coder aDecoder: NSCoder) { + + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - - public override func beginRefreshing() { + + override public func beginRefreshing() { super.beginRefreshing() animationView.loopMode = .loop animationView.play() let generator = UIImpactFeedbackGenerator(style: .medium) generator.impactOccurred() } - - public override func endRefreshing() { + + override public func endRefreshing() { animationView.loopMode = .playOnce animationView.play { isFinished in if isFinished { @@ -32,13 +33,13 @@ public final class SHRefreshControl: UIRefreshControl { } } } - + func setupView() { tintColor = .clear addSubview(animationView) addTarget(self, action: #selector(beginRefreshing), for: .valueChanged) } - + func setupLayout() { animationView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ From 97bae16c64c59cec418b10e4204a262e35b5d85f Mon Sep 17 00:00:00 2001 From: Hyunjun Date: Wed, 4 Dec 2024 17:11:12 +0900 Subject: [PATCH 3/8] =?UTF-8?q?chore=20trailing=20comma=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Modules/FastNetwork/Project.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Modules/FastNetwork/Project.swift b/Projects/Modules/FastNetwork/Project.swift index 329a206c..739926af 100644 --- a/Projects/Modules/FastNetwork/Project.swift +++ b/Projects/Modules/FastNetwork/Project.swift @@ -7,7 +7,7 @@ let project = Project.module( targets: [ .interface(module: .module(.FastNetwork)), .implements(module: .module(.FastNetwork), dependencies: [ - .module(target: .FastNetwork, type: .interface), + .module(target: .FastNetwork, type: .interface) ]), .testing(module: .module(.FastNetwork), dependencies: [ .module(target: .FastNetwork, type: .interface) From 63d0b7629da6ed81e4f4670b571fedbb2da7c411 Mon Sep 17 00:00:00 2001 From: Hyunjun Date: Wed, 4 Dec 2024 17:16:54 +0900 Subject: [PATCH 4/8] =?UTF-8?q?chore=20swiftLint=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MockFetchChannelListUsecaseImpl.swift | 1 - .../FastNetwork/Sources/Error/HTTPError.swift | 18 +++++++++--------- .../FastNetwork/Tests/NetworkClientTest.swift | 4 ---- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/Projects/App/Sources/MockFetchChannelListUsecaseImpl.swift b/Projects/App/Sources/MockFetchChannelListUsecaseImpl.swift index b2dc9d6e..c6777394 100644 --- a/Projects/App/Sources/MockFetchChannelListUsecaseImpl.swift +++ b/Projects/App/Sources/MockFetchChannelListUsecaseImpl.swift @@ -54,7 +54,6 @@ final class MockChannelListFetcher { let randomBool = Bool.random() let image: Image = randomBool ? .ratio16x9 : .ratio4x3 - let fetchedImage = await image.fetch() channels.append(ChannelEntity(id: UUID().uuidString, name: name)) } diff --git a/Projects/Modules/FastNetwork/Sources/Error/HTTPError.swift b/Projects/Modules/FastNetwork/Sources/Error/HTTPError.swift index 8c0ee8a1..a2929f3a 100644 --- a/Projects/Modules/FastNetwork/Sources/Error/HTTPError.swift +++ b/Projects/Modules/FastNetwork/Sources/Error/HTTPError.swift @@ -3,18 +3,18 @@ import Foundation public enum HTTPError: String, LocalizedError { // MARK: 400..<500 , Client Error - case badRequest /// 400 - case unauthorized /// 401 - case paymentRequired /// 402 - case forbidden /// 403 - case notFound /// 404 - case methodNotAllowed /// 405 - case conflict /// 409 + case badRequest // 400 + case unauthorized // 401 + case paymentRequired // 402 + case forbidden // 403 + case notFound // 404 + case methodNotAllowed // 405 + case conflict // 409 // MARK: 500..<600 Server Error - case internalServerError /// 500 - case badGateway /// 502 + case internalServerError // 500 + case badGateway // 502 // MARK: Extra diff --git a/Projects/Modules/FastNetwork/Tests/NetworkClientTest.swift b/Projects/Modules/FastNetwork/Tests/NetworkClientTest.swift index 6467a12f..5f0e2111 100644 --- a/Projects/Modules/FastNetwork/Tests/NetworkClientTest.swift +++ b/Projects/Modules/FastNetwork/Tests/NetworkClientTest.swift @@ -16,10 +16,6 @@ final class NetworkClientTest: XCTestCase { client = NetworkClient(session: session, interceptors: interceptors) } - override func tearDown() { - super.tearDown() - } - // MARK: - Success func test_success_response() async throws { From 05b9db73a7bf516830a803c7f4e42ae0078d1632 Mon Sep 17 00:00:00 2001 From: hyunjuntyler Date: Thu, 5 Dec 2024 11:58:23 +0900 Subject: [PATCH 5/8] =?UTF-8?q?style=20swiftformat=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Player/ViewModels/LiveStreamViewModel.swift | 10 +++++----- .../Modules/ChatSoketModule/Sources/WebSocket.swift | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Projects/Features/LiveStreamFeature/Sources/Player/ViewModels/LiveStreamViewModel.swift b/Projects/Features/LiveStreamFeature/Sources/Player/ViewModels/LiveStreamViewModel.swift index 1ee68f74..489fd010 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Player/ViewModels/LiveStreamViewModel.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Player/ViewModels/LiveStreamViewModel.swift @@ -56,7 +56,7 @@ public final class LiveStreamViewModel: ViewModel { let output = Output() input.expandButtonDidTap - .compactMap { $0 } + .compactMap(\.self) .sink { let nextValue = !output.isExpanded.value output.isExpanded.send(nextValue) @@ -66,7 +66,7 @@ public final class LiveStreamViewModel: ViewModel { .store(in: &subscription) input.sliderValueDidChange - .compactMap { $0 } + .compactMap(\.self) .map { Double($0) } .sink { input.autoDissmissDidRegister.send() @@ -75,14 +75,14 @@ public final class LiveStreamViewModel: ViewModel { .store(in: &subscription) input.playerStateDidChange - .compactMap { $0 } + .compactMap(\.self) .sink { flag in output.isPlaying.send(flag) } .store(in: &subscription) input.playerGestureDidTap - .compactMap { $0 } + .compactMap(\.self) .sink { _ in let nextValue1 = !output.isShowedPlayerControl.value output.isShowedPlayerControl.send(nextValue1) @@ -100,7 +100,7 @@ public final class LiveStreamViewModel: ViewModel { .store(in: &subscription) input.playButtonDidTap - .compactMap { $0 } + .compactMap(\.self) .sink { _ in input.autoDissmissDidRegister.send() output.isPlaying.send(!output.isPlaying.value) diff --git a/Projects/Modules/ChatSoketModule/Sources/WebSocket.swift b/Projects/Modules/ChatSoketModule/Sources/WebSocket.swift index adb8c940..ef4b6aad 100644 --- a/Projects/Modules/ChatSoketModule/Sources/WebSocket.swift +++ b/Projects/Modules/ChatSoketModule/Sources/WebSocket.swift @@ -42,7 +42,7 @@ public final class WebSocket: NSObject { startPing() } - public func send(data: ChatMessage) { + public func send(data _: ChatMessage) { guard let data = try? encoder.encode(data) else { return } let taskMessage = URLSessionWebSocketTask.Message.data(data) From 822ebe2b5349aca11762a181856f31f4ef3339dc Mon Sep 17 00:00:00 2001 From: hyunjuntyler Date: Thu, 5 Dec 2024 12:26:07 +0900 Subject: [PATCH 6/8] =?UTF-8?q?chore=20swiftformat=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/MockFetchChannelListUsecaseImpl.swift | 3 --- Projects/Domains/BaseDomain/Project.swift | 8 ++++---- Projects/Domains/BroadcastDomain/Project.swift | 8 ++++---- .../Sources/Endpoint/BroadcastEndpoint.swift | 2 +- Projects/Domains/LiveStationDomain/Project.swift | 6 +++--- .../Sources/Endpoint/LiveStationEndpoint.swift | 8 ++++---- Projects/Features/AuthFeature/Project.swift | 8 ++++---- Projects/Features/BaseFeature/Project.swift | 10 +++++----- Projects/Features/LiveStreamFeature/Project.swift | 10 +++++----- .../ViewControllers/LiveStreamViewController.swift | 2 +- .../Sources/Player/Views/PlayerControlView.swift | 3 +-- Projects/Features/MainFeature/Project.swift | 8 ++++---- Projects/Modules/ChatSoketModule/Project.swift | 10 +++++----- .../Sources/SoketTestViewController.swift | 2 +- .../Modules/ChatSoketModule/Sources/WebSocket.swift | 6 +++--- Projects/Modules/EasyLayout/Project.swift | 4 ++-- Projects/Modules/FastNetwork/Project.swift | 8 ++++---- .../FastNetwork/Tests/NetworkEncoderTests.swift | 2 +- Projects/Modules/ThirdPartyLibModule/Project.swift | 6 +++--- Projects/UserInterfaces/DesignSystem/Project.swift | 4 ++-- .../DesignSystem/Sources/SHLoadingView.swift | 6 +++--- .../DesignSystem/Sources/SHRefreshControl.swift | 2 +- 22 files changed, 61 insertions(+), 65 deletions(-) diff --git a/Projects/App/Sources/MockFetchChannelListUsecaseImpl.swift b/Projects/App/Sources/MockFetchChannelListUsecaseImpl.swift index c6777394..5ff87510 100644 --- a/Projects/App/Sources/MockFetchChannelListUsecaseImpl.swift +++ b/Projects/App/Sources/MockFetchChannelListUsecaseImpl.swift @@ -52,9 +52,6 @@ final class MockChannelListFetcher { "가나다라마바사아자차카타파하".randomElement()! }) - let randomBool = Bool.random() - let image: Image = randomBool ? .ratio16x9 : .ratio4x3 - channels.append(ChannelEntity(id: UUID().uuidString, name: name)) } diff --git a/Projects/Domains/BaseDomain/Project.swift b/Projects/Domains/BaseDomain/Project.swift index 0db562a6..a0bb5e85 100644 --- a/Projects/Domains/BaseDomain/Project.swift +++ b/Projects/Domains/BaseDomain/Project.swift @@ -8,14 +8,14 @@ let project = Project.module( .interface(module: .domain(.BaseDomain)), .implements(module: .domain(.BaseDomain), dependencies: [ .domain(target: .BaseDomain, type: .interface), - .module(target: .FastNetwork), + .module(target: .FastNetwork) ]), .testing(module: .domain(.BaseDomain), dependencies: [ - .domain(target: .BaseDomain, type: .interface), + .domain(target: .BaseDomain, type: .interface) ]), .tests(module: .domain(.BaseDomain), dependencies: [ .domain(target: .BaseDomain), - .domain(target: .BaseDomain, type: .testing), - ]), + .domain(target: .BaseDomain, type: .testing) + ]) ] ) diff --git a/Projects/Domains/BroadcastDomain/Project.swift b/Projects/Domains/BroadcastDomain/Project.swift index a59da384..c40b10b8 100644 --- a/Projects/Domains/BroadcastDomain/Project.swift +++ b/Projects/Domains/BroadcastDomain/Project.swift @@ -8,14 +8,14 @@ let project = Project.module( .interface(module: .domain(.BroadcastDomain)), .implements(module: .domain(.BroadcastDomain), dependencies: [ .domain(target: .BroadcastDomain, type: .interface), - .domain(target: .BaseDomain), + .domain(target: .BaseDomain) ]), .testing(module: .domain(.BroadcastDomain), dependencies: [ - .domain(target: .BroadcastDomain, type: .interface), + .domain(target: .BroadcastDomain, type: .interface) ]), .tests(module: .domain(.BroadcastDomain), dependencies: [ .domain(target: .BroadcastDomain), - .domain(target: .BroadcastDomain, type: .testing), - ]), + .domain(target: .BroadcastDomain, type: .testing) + ]) ] ) diff --git a/Projects/Domains/BroadcastDomain/Sources/Endpoint/BroadcastEndpoint.swift b/Projects/Domains/BroadcastDomain/Sources/Endpoint/BroadcastEndpoint.swift index 1fdd05dd..e1aef6c4 100644 --- a/Projects/Domains/BroadcastDomain/Sources/Endpoint/BroadcastEndpoint.swift +++ b/Projects/Domains/BroadcastDomain/Sources/Endpoint/BroadcastEndpoint.swift @@ -24,7 +24,7 @@ extension BroadcastEndpoint: Endpoint { public var header: [String: String]? { [ - "Content-Type": "application/json", + "Content-Type": "application/json" ] } diff --git a/Projects/Domains/LiveStationDomain/Project.swift b/Projects/Domains/LiveStationDomain/Project.swift index e8c3c1e5..f8153346 100644 --- a/Projects/Domains/LiveStationDomain/Project.swift +++ b/Projects/Domains/LiveStationDomain/Project.swift @@ -8,10 +8,10 @@ let project = Project.module( .interface(module: .domain(.LiveStationDomain)), .implements(module: .domain(.LiveStationDomain), dependencies: [ .domain(target: .LiveStationDomain, type: .interface), - .domain(target: .BaseDomain), + .domain(target: .BaseDomain) ]), .testing(module: .domain(.LiveStationDomain), dependencies: [ - .domain(target: .LiveStationDomain, type: .interface), - ]), + .domain(target: .LiveStationDomain, type: .interface) + ]) ] ) diff --git a/Projects/Domains/LiveStationDomain/Sources/Endpoint/LiveStationEndpoint.swift b/Projects/Domains/LiveStationDomain/Sources/Endpoint/LiveStationEndpoint.swift index 9948e087..b74dd324 100644 --- a/Projects/Domains/LiveStationDomain/Sources/Endpoint/LiveStationEndpoint.swift +++ b/Projects/Domains/LiveStationDomain/Sources/Endpoint/LiveStationEndpoint.swift @@ -32,7 +32,7 @@ extension LiveStationEndpoint: Endpoint { "x-ncp-apigw-timestamp": timestamp, "x-ncp-iam-access-key": config(key: .accessKey), "x-ncp-apigw-signature-v2": makeSignature(with: timestamp), - "x-ncp-region_code": "KR", + "x-ncp-region_code": "KR" ] } @@ -75,16 +75,16 @@ extension LiveStationEndpoint: Endpoint { "cdnDomain": config(key: .cdnDomain), "profileId": config(key: .profileID), "cdnInstanceNo": config(key: .cdnInstanceNo), - "regionType": "KOREA", + "regionType": "KOREA" ], "qualitySetId": 4430, "useDvr": true, "immediateOnAir": true, "record": [ - "type": "MANUAL_UPLOAD", + "type": "MANUAL_UPLOAD" ], "drmEnabledYn": false, - "timemachineMin": 360, + "timemachineMin": 360 ] ) diff --git a/Projects/Features/AuthFeature/Project.swift b/Projects/Features/AuthFeature/Project.swift index 36b60a0c..8c9e5564 100644 --- a/Projects/Features/AuthFeature/Project.swift +++ b/Projects/Features/AuthFeature/Project.swift @@ -8,13 +8,13 @@ let project = Project.module( .interface(module: .feature(.AuthFeature)), .implements(module: .feature(.AuthFeature), dependencies: [ .feature(target: .AuthFeature, type: .interface), - .feature(target: .BaseFeature), + .feature(target: .BaseFeature) ]), .tests(module: .feature(.AuthFeature), dependencies: [ - .feature(target: .AuthFeature), + .feature(target: .AuthFeature) ]), .demo(module: .feature(.AuthFeature), dependencies: [ - .feature(target: .AuthFeature), - ]), + .feature(target: .AuthFeature) + ]) ] ) diff --git a/Projects/Features/BaseFeature/Project.swift b/Projects/Features/BaseFeature/Project.swift index 9e3ad24c..0ffc50cd 100644 --- a/Projects/Features/BaseFeature/Project.swift +++ b/Projects/Features/BaseFeature/Project.swift @@ -9,18 +9,18 @@ let project = Project.module( .implements(module: .feature(.BaseFeature), dependencies: [ .feature(target: .BaseFeature, type: .interface), .userInterface(target: .DesignSystem), - .module(target: .ThirdPartyLibModule), + .module(target: .ThirdPartyLibModule) ]), .testing(module: .feature(.BaseFeature), dependencies: [ - .feature(target: .BaseFeature, type: .interface), + .feature(target: .BaseFeature, type: .interface) ]), .tests(module: .feature(.BaseFeature), dependencies: [ .feature(target: .BaseFeature), - .feature(target: .BaseFeature, type: .testing), + .feature(target: .BaseFeature, type: .testing) ]), .demo(module: .feature(.BaseFeature), dependencies: [ .feature(target: .BaseFeature), - .feature(target: .BaseFeature, type: .testing), - ]), + .feature(target: .BaseFeature, type: .testing) + ]) ] ) diff --git a/Projects/Features/LiveStreamFeature/Project.swift b/Projects/Features/LiveStreamFeature/Project.swift index 80d91376..c25e2bd3 100644 --- a/Projects/Features/LiveStreamFeature/Project.swift +++ b/Projects/Features/LiveStreamFeature/Project.swift @@ -6,21 +6,21 @@ let project = Project.module( name: ModulePaths.Feature.LiveStreamFeature.rawValue, targets: [ .interface(module: .feature(.LiveStreamFeature), dependencies: [ - .feature(target: .BaseFeature, type: .interface), + .feature(target: .BaseFeature, type: .interface) ]), .implements(module: .feature(.LiveStreamFeature), dependencies: [ .feature(target: .LiveStreamFeature, type: .interface), .feature(target: .BaseFeature), .domain(target: .LiveStationDomain, type: .interface), .domain(target: .BroadcastDomain, type: .interface), - .module(target: .ChatSoketModule), + .module(target: .ChatSoketModule) ]), .tests(module: .feature(.LiveStreamFeature), dependencies: [ - .feature(target: .LiveStreamFeature), + .feature(target: .LiveStreamFeature) ]), .demo(module: .feature(.LiveStreamFeature), dependencies: [ .feature(target: .LiveStreamFeature), - .domain(target: .LiveStationDomain, type: .interface), - ]), + .domain(target: .LiveStationDomain, type: .interface) + ]) ] ) diff --git a/Projects/Features/LiveStreamFeature/Sources/Player/ViewControllers/LiveStreamViewController.swift b/Projects/Features/LiveStreamFeature/Sources/Player/ViewControllers/LiveStreamViewController.swift index 4e214816..57c525d2 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Player/ViewControllers/LiveStreamViewController.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Player/ViewControllers/LiveStreamViewController.swift @@ -106,7 +106,7 @@ public final class LiveStreamViewController: BaseViewController Date: Thu, 5 Dec 2024 12:26:10 +0900 Subject: [PATCH 7/8] =?UTF-8?q?style=20swiftformat=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Player/Views/PlayerControlView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Projects/Features/LiveStreamFeature/Sources/Player/Views/PlayerControlView.swift b/Projects/Features/LiveStreamFeature/Sources/Player/Views/PlayerControlView.swift index 4af98b4f..c9c4af42 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Player/Views/PlayerControlView.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Player/Views/PlayerControlView.swift @@ -131,7 +131,8 @@ extension PlayerControlView { delay: .zero, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.4, - options: .allowUserInteraction) { + options: .allowUserInteraction) + { if isPlaying { self.playButton.configuration?.image = ImageConstants.pause.image } else { From 1c9aa941cb8e13c127486cafbc96de8293daf96f Mon Sep 17 00:00:00 2001 From: hyunjuntyler Date: Thu, 5 Dec 2024 12:26:58 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix=20=EB=B9=88=20=EB=B0=A9=EC=86=A1=20Aler?= =?UTF-8?q?t=20=EB=A9=94=EC=9D=B8=EC=8A=A4=EB=A0=88=EB=93=9C=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Player/ViewControllers/LiveStreamViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Projects/Features/LiveStreamFeature/Sources/Player/ViewControllers/LiveStreamViewController.swift b/Projects/Features/LiveStreamFeature/Sources/Player/ViewControllers/LiveStreamViewController.swift index 57c525d2..ef01fd51 100644 --- a/Projects/Features/LiveStreamFeature/Sources/Player/ViewControllers/LiveStreamViewController.swift +++ b/Projects/Features/LiveStreamFeature/Sources/Player/ViewControllers/LiveStreamViewController.swift @@ -209,6 +209,7 @@ public final class LiveStreamViewController: BaseViewController