diff --git a/Projects/App/Tests/.gitkeep b/Configurations/Base/Projects/Project-Development.xcconfig similarity index 100% rename from Projects/App/Tests/.gitkeep rename to Configurations/Base/Projects/Project-Development.xcconfig diff --git a/Projects/Core/Tests/.gitkeep b/Configurations/Base/Projects/Project-PROD.xcconfig similarity index 100% rename from Projects/Core/Tests/.gitkeep rename to Configurations/Base/Projects/Project-PROD.xcconfig diff --git a/Projects/Domain/Tests/.gitkeep b/Configurations/Base/Projects/Project-QA.xcconfig similarity index 100% rename from Projects/Domain/Tests/.gitkeep rename to Configurations/Base/Projects/Project-QA.xcconfig diff --git a/Projects/Features/BaseFeatureDependency/Tests/.gitkeep b/Configurations/Base/Projects/Project-Test.xcconfig similarity index 100% rename from Projects/Features/BaseFeatureDependency/Tests/.gitkeep rename to Configurations/Base/Projects/Project-Test.xcconfig diff --git a/Configurations/Targets/iOS-Demo.xcconfig b/Configurations/Targets/iOS-Demo.xcconfig new file mode 100644 index 0000000..124fa75 --- /dev/null +++ b/Configurations/Targets/iOS-Demo.xcconfig @@ -0,0 +1,9 @@ +// +// iOS-Demo.xcconfig +// Manifests +// +// Created by 류희재 on 6/28/24. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 diff --git a/Configurations/Targets/iOS-Framework.xcconfig b/Configurations/Targets/iOS-Framework.xcconfig new file mode 100644 index 0000000..d758176 --- /dev/null +++ b/Configurations/Targets/iOS-Framework.xcconfig @@ -0,0 +1,9 @@ +// +// iOS-Framework.xcconfig +// Manifests +// +// Created by 류희재 on 6/28/24. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 diff --git a/Configurations/Targets/iOS-Tests.xcconfig b/Configurations/Targets/iOS-Tests.xcconfig new file mode 100644 index 0000000..e526977 --- /dev/null +++ b/Configurations/Targets/iOS-Tests.xcconfig @@ -0,0 +1,9 @@ +// +// iOS-Tests.xcconfig +// Manifests +// +// Created by 류희재 on 6/28/24. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 diff --git a/Plugins/ConfigPlugin/Plugin.swift b/Plugins/ConfigPlugin/Plugin.swift new file mode 100644 index 0000000..9e5826f --- /dev/null +++ b/Plugins/ConfigPlugin/Plugin.swift @@ -0,0 +1,3 @@ +import ProjectDescription + +let configPlugin = Plugin(name: "ConfigPlugin") diff --git a/Plugins/ConfigPlugin/ProjectDescriptionHelpers/Configurations.swift b/Plugins/ConfigPlugin/ProjectDescriptionHelpers/Configurations.swift new file mode 100644 index 0000000..02ee565 --- /dev/null +++ b/Plugins/ConfigPlugin/ProjectDescriptionHelpers/Configurations.swift @@ -0,0 +1,48 @@ +import Foundation +import ProjectDescription + +/// 빌드할 환경에 대한 설정 + +/// Target 분리 (The Modular Architecture 기반으로 분리했습니다) + + +/// DEV : 실제 프로덕트 BaseURL을 사용하는 debug scheme +/// TEST : 테스트 BaseURL을 사용하는 debug scheme +/// QA : 테스트 BaseURL을 사용하는 release scheme +/// RELEASE : 실제 프로덕트 BaseURL을 사용하는 release scheme + + +public struct XCConfig { + private struct Path { + static var framework: ProjectDescription.Path { .relativeToRoot("Configurations/Targets/iOS-Framework.xcconfig") } + static var demo: ProjectDescription.Path { .relativeToRoot("Configurations/Targets/iOS-Demo.xcconfig") } + static var tests: ProjectDescription.Path { .relativeToRoot("Configurations/Targets/iOS-Tests.xcconfig") } + static func project(_ config: String) -> ProjectDescription.Path { .relativeToRoot("Configurations/Base/Projects/Project-\(config).xcconfig") } + } + + public static let framework: [Configuration] = [ + .debug(name: "Development", xcconfig: Path.framework), + .debug(name: "Test", xcconfig: Path.framework), + .release(name: "QA", xcconfig: Path.framework), + .release(name: "PROD", xcconfig: Path.framework), + ] + + public static let tests: [Configuration] = [ + .debug(name: "Development", xcconfig: Path.tests), + .debug(name: "Test", xcconfig: Path.tests), + .release(name: "QA", xcconfig: Path.tests), + .release(name: "PROD", xcconfig: Path.tests), + ] + public static let demo: [Configuration] = [ + .debug(name: "Development", xcconfig: Path.demo), + .debug(name: "Test", xcconfig: Path.demo), + .release(name: "QA", xcconfig: Path.demo), + .release(name: "PROD", xcconfig: Path.demo), + ] + public static let project: [Configuration] = [ + .debug(name: "Development", xcconfig: Path.project("Development")), + .debug(name: "Test", xcconfig: Path.project("Test")), + .release(name: "QA", xcconfig: Path.project("QA")), + .release(name: "PROD", xcconfig: Path.project("PROD")), + ] +} diff --git a/Plugins/DependencyPlugin/Plugin.swift b/Plugins/DependencyPlugin/Plugin.swift index 001e9bf..16e44d2 100644 --- a/Plugins/DependencyPlugin/Plugin.swift +++ b/Plugins/DependencyPlugin/Plugin.swift @@ -1,3 +1,3 @@ import ProjectDescription - let dependencyPlugin = Plugin(name: "DependencyPlugin") +let dependencyPlugin = Plugin(name: "DependencyPlugin") diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Project.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Project.swift index ef71062..42f0c76 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Project.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Project.swift @@ -7,6 +7,9 @@ import ProjectDescription +/// 프로젝트 내 모듈 및 기능별 종속성을 체계적으로 관리하기 위한 유틸리티를 제공 +/// 새로운 모듈이나 기능이 추가되더라도 해당 파일만 업데이트하면 되어 유지보수가 용이합니다. + public extension Dep { struct Features { public struct Main {} @@ -17,36 +20,58 @@ public extension Dep { struct Modules {} } -// MARK: - Root +// MARK: - Root: 프로젝트의 핵심 모듈에 대한 종속성을 정의 public extension Dep { static let data = Dep.project(target: "Data", path: .data) - + static let domain = Dep.project(target: "Domain", path: .domain) static let core = Dep.project(target: "Core", path: .core) } -// MARK: - Modules +// MARK: - Modules: 프로젝트 내 모듈 단위 종속성을 정의 public extension Dep.Modules { - static let dsKit = Dep.project(target: "DSKit", path: .relativeToModules("DSKit")) + static let dsKit = Dep.project( + target: "DSKit", + path: .relativeToModules("DSKit") + ) - static let networks = Dep.project(target: "Networks", path: .relativeToModules("Networks")) + static let networks = Dep.project( + target: "Networks", + path: .relativeToModules("Networks") + ) - static let thirdPartyLibs = Dep.project(target: "ThirdPartyLibs", path: .relativeToModules("ThirdPartyLibs")) + static let thirdPartyLibs = Dep.project( + target: "ThirdPartyLibs", + path: .relativeToModules("ThirdPartyLibs") + ) } // MARK: - Features public extension Dep.Features { - static func project(name: String, group: String) -> Dep { .project(target: "\(group)\(name)", path: .relativeToFeature("\(group)\(name)")) } + static func project(name: String, group: String) -> Dep { + .project( + target: "\(group)\(name)", + path: .relativeToFeature("\(group)\(name)") + ) + } - static let BaseFeatureDependency = TargetDependency.project(target: "BaseFeatureDependency", path: .relativeToFeature("BaseFeatureDependency")) + static let BaseFeatureDependency = Dep.project( + target: "BaseFeatureDependency", + path: .relativeToFeature("BaseFeatureDependency") + ) - static let RootFeature = TargetDependency.project(target: "RootFeature", path: .relativeToFeature("RootFeature")) + static let RootFeature = Dep.project( + target: "RootFeature", + path: .relativeToFeature("RootFeature") + ) } +//TODO: 폴더별로 분기처리하기 위해서 이런식으로 했다면 하나의 그룹이름만 주입해서 만드는게 좋지않나? + public extension Dep.Features.Main { static let group = "Main" diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+SPM.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+SPM.swift index a2102d2..72b5e21 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+SPM.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+SPM.swift @@ -7,6 +7,9 @@ import ProjectDescription +/// TargetDependency의 확장을 통해 +/// 프로젝트 내 외부 라이브러리 종속성을 보다 체계적으로 관리하기 위한 유틸리티를 제공하는 파일 + public extension TargetDependency { enum SPM {} enum Carthage {} @@ -21,4 +24,5 @@ public extension TargetDependency.SPM { static let RxSwift = TargetDependency.external(name: "RxSwift") static let RxCocoa = TargetDependency.external(name: "RxCocoa") static let RxRelay = TargetDependency.external(name: "RxRelay") + static let ReactorKit = TargetDependency.external(name: "ReactorKit") } diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/PathExtension.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/PathExtension.swift index 0e6715b..11f0b59 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/PathExtension.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/PathExtension.swift @@ -7,27 +7,36 @@ import ProjectDescription +/// ProjectDescription.Path의 확장을 통해 +/// 프로젝트 내 경로를 보다 간결하고 직관적으로 관리하기 위한 유틸리티를 제공하는 파일 + public extension ProjectDescription.Path { + /// 기능 폴더에 대한 상대 경로를 생성 static func relativeToFeature(_ path: String) -> Self { return .relativeToRoot("Projects/Features/\(path)") } + /// 모듈 폴더에 대한 상대 경로를 생성 static func relativeToModules(_ path: String) -> Self { return .relativeToRoot("Projects/Modules/\(path)") } + /// 각각 앱 폴더에 대한 경로를 반환하는 속성 static var app: Self { return .relativeToRoot("Projects/App") } + /// 각각 데이터 폴더에 대한 경로를 반환하는 속성 static var data: Self { return .relativeToRoot("Projects/Data") } + /// 각각 도메인 폴더에 대한 경로를 반환하는 속성 static var domain: Self { return .relativeToRoot("Projects/Domain") } + /// 각각 코어 폴더에 대한 경로를 반환하는 속성 static var core: Self { return .relativeToRoot("Projects/Core") } diff --git a/Plugins/EnvPlugin/ProjectDescriptionHelpers/Enviroment.swift b/Plugins/EnvPlugin/ProjectDescriptionHelpers/Enviroment.swift index cac2151..cd48090 100644 --- a/Plugins/EnvPlugin/ProjectDescriptionHelpers/Enviroment.swift +++ b/Plugins/EnvPlugin/ProjectDescriptionHelpers/Enviroment.swift @@ -7,15 +7,18 @@ import ProjectDescription -public enum Environment { - public static let workspaceName = "Weather-iOS" -} +/// 프로젝트 환경 관련 파일입니다 -public extension Project { - enum Environment { - public static let workspaceName = "Weather-iOS" - public static let deploymentTarget = DeploymentTarget.iOS(targetVersion: "16.0", devices: [.iphone]) - public static let platform = Platform.iOS - public static let bundlePrefix = "com.Weather-iOS" - } +public struct ProjectEnvironment { + public let workspaceName: String + public let deploymentTarget: DeploymentTarget + public let platform: Platform + public let bundlePrefix: String } + +public let env = ProjectEnvironment( + workspaceName: "Weather-iOS", + deploymentTarget: DeploymentTarget.iOS(targetVersion: "17.0", devices: [.iphone]), + platform: .iOS, + bundlePrefix: "com.Weather-iOS" +) diff --git a/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift index ca2ea31..6ccc473 100644 --- a/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift +++ b/Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift @@ -7,14 +7,16 @@ import ProjectDescription +/// InfoPList를 정리해둔 파일이빈다 +/// public extension Project { - static let appInfoPlist: [String: InfoPlist.Value] = [ + static let appInfoPlist: [String: Plist.Value] = [ "BASE_URL": "https://api.openweathermap.org/data/2.5", "API_KEY": "7618d35ff394f5dd39212928a3a4692f", "CFBundleShortVersionString": "1.0.0", "CFBundleDevelopmentRegion": "ko", "CFBundleVersion": "1", - "CFBundleIdentifier": "com.Weather-iOS.release", + "CFBundleIdentifier": "com.Weather-iOS.$(PRODUCT_MODULE_NAME)", "CFBundleDisplayName": "Weather-iOS", "UILaunchStoryboardName": "LaunchScreen", "UIApplicationSceneManifest": [ @@ -38,18 +40,9 @@ public extension Project { "App Transport Security Settings": ["Allow Arbitrary Loads": true], "NSAppTransportSecurity": ["NSAllowsArbitraryLoads": true], "ITSAppUsesNonExemptEncryption": false, -// "UIUserInterfaceStyle": "Dark", -// "CFBundleURLTypes": [ -// [ -// "CFBundleTypeRole": "Editor", -// "CFBundleURLName": "sopt-makers", -// "CFBundleURLSchemes": ["sopt-makers"] -// ] -// ], -// "UIBackgroundModes": ["remote-notification"] ] - static let demoInfoPlist: [String: InfoPlist.Value] = [ + static let demoInfoPlist: [String: Plist.Value] = [ "CFBundleShortVersionString": "1.0.0", "CFBundleDevelopmentRegion": "ko", "CFBundleVersion": "1", diff --git a/Plugins/EnvPlugin/ProjectDescriptionHelpers/SettingDictionary+.swift b/Plugins/EnvPlugin/ProjectDescriptionHelpers/SettingDictionary+.swift new file mode 100644 index 0000000..4518e97 --- /dev/null +++ b/Plugins/EnvPlugin/ProjectDescriptionHelpers/SettingDictionary+.swift @@ -0,0 +1,74 @@ +// +// SettingDictionary+.swift +// EnvPlugin +// +// Created by 류희재 on 6/26/24. +// + +import ProjectDescription + +// 아직 여기의 필요성은 못 느끼는중 무슨 역할을 하게 될지 궁금하다! + +public extension SettingsDictionary { + // allLoadSettings와 baseSettings는 빌드 설정 딕셔너리의 기본값을 정의 + static let allLoadSettings: Self = [ + "OTHER_LDFLAGS" : [ + "$(inherited) -all_load", + "-Xlinker -interposable" + ] + ] + + static let baseSettings: Self = [ + "OTHER_LDFLAGS" : [ + "$(inherited)", + "-ObjC" + ] + ] + + func setProductBundleIdentifier(_ value: String = "com.iOS$(BUNDLE_ID_SUFFIX)") -> SettingsDictionary { + merging(["PRODUCT_BUNDLE_IDENTIFIER": SettingValue(stringLiteral: value)]) + } + + func setAssetcatalogCompilerAppIconName(_ value: String = "AppIcon$(BUNDLE_ID_SUFFIX)") -> SettingsDictionary { + merging(["ASSETCATALOG_COMPILER_APPICON_NAME": SettingValue(stringLiteral: value)]) + } + + func setBuildActiveArchitectureOnly(_ value: Bool) -> SettingsDictionary { + merging(["ONLY_ACTIVE_ARCH": SettingValue(stringLiteral: value ? "YES" : "NO")]) + } + + func setExcludedArchitectures(sdk: String = "iphonesimulator*", _ value: String = "arm64") -> SettingsDictionary { + merging(["EXCLUDED_ARCHS[sdk=\(sdk)]": SettingValue(stringLiteral: value)]) + } + + func setSwiftActiveComplationConditions(_ value: String) -> SettingsDictionary { + merging(["SWIFT_ACTIVE_COMPILATION_CONDITIONS": SettingValue(stringLiteral: value)]) + } + + func setAlwaysSearchUserPath(_ value: String = "NO") -> SettingsDictionary { + merging(["ALWAYS_SEARCH_USER_PATHS": SettingValue(stringLiteral: value)]) + } + + func setStripDebugSymbolsDuringCopy(_ value: String = "NO") -> SettingsDictionary { + merging(["COPY_PHASE_STRIP": SettingValue(stringLiteral: value)]) + } + + func setDynamicLibraryInstallNameBase(_ value: String = "@rpath") -> SettingsDictionary { + merging(["DYLIB_INSTALL_NAME_BASE": SettingValue(stringLiteral: value)]) + } + + func setSkipInstall(_ value: Bool = false) -> SettingsDictionary { + merging(["SKIP_INSTALL": SettingValue(stringLiteral: value ? "YES" : "NO")]) + } + + func setCodeSignManual() -> SettingsDictionary { + merging(["CODE_SIGN_STYLE": SettingValue(stringLiteral: "Manual")]) + .merging(["DEVELOPMENT_TEAM": SettingValue(stringLiteral: "9K86FQHDLU")]) + .merging(["CODE_SIGN_IDENTITY": SettingValue(stringLiteral: "$(CODE_SIGN_IDENTITY)")]) + } + + func setProvisioning() -> SettingsDictionary { + merging(["PROVISIONING_PROFILE_SPECIFIER": SettingValue(stringLiteral: "$(APP_PROVISIONING_PROFILE)")]) + .merging(["PROVISIONING_PROFILE": SettingValue(stringLiteral: "$(APP_PROVISIONING_PROFILE)")]) + } +} diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index 3e759e6..5602bbe 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -11,12 +11,10 @@ import DependencyPlugin import EnvPlugin let project = Project.makeModule( - name: Environment.workspaceName, - product: .app, - dependencies: [ - .Features.RootFeature, - .data - ], - sources: ["Sources/**"], - resources: ["Resources/**"] + name: env.workspaceName, + targets: [.app, .unitTest], + internalDependencies: [ + .data, + .Features.RootFeature + ] ) diff --git a/Projects/App/Tests/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Projects/App/Tests/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Projects/App/Tests/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/App/Tests/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Projects/App/Tests/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/Projects/App/Tests/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/App/Tests/Resources/Assets.xcassets/Contents.json b/Projects/App/Tests/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Projects/App/Tests/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/App/Tests/Resources/LaunchScreen.storyboard b/Projects/App/Tests/Resources/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Projects/App/Tests/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Features/RootFeature/Tests/.gitkeep b/Projects/App/Tests/Sources/UnitTest.swift similarity index 100% rename from Projects/Features/RootFeature/Tests/.gitkeep rename to Projects/App/Tests/Sources/UnitTest.swift diff --git a/Projects/App/Weather_iOSTests/Weather_iOSTests.swift b/Projects/App/Weather_iOSTests/Weather_iOSTests.swift new file mode 100644 index 0000000..fff0400 --- /dev/null +++ b/Projects/App/Weather_iOSTests/Weather_iOSTests.swift @@ -0,0 +1,36 @@ +// +// Weather_iOSTests.swift +// Weather_iOSTests +// +// Created by 류희재 on 6/25/24. +// Copyright © 2024 hellohidi. All rights reserved. +// + +import XCTest + +final class Weather_iOSTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Projects/Core/Demo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Projects/Core/Demo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Projects/Core/Demo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Core/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Projects/Core/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/Projects/Core/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Core/Demo/Resources/Assets.xcassets/Contents.json b/Projects/Core/Demo/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Projects/Core/Demo/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Core/Demo/Resources/LaunchScreen.storyboard b/Projects/Core/Demo/Resources/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Projects/Core/Demo/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Modules/ThirdPartyLibs/Tests/.gitkeep b/Projects/Core/Demo/Resources/dummy.txt similarity index 100% rename from Projects/Modules/ThirdPartyLibs/Tests/.gitkeep rename to Projects/Core/Demo/Resources/dummy.txt diff --git a/Projects/Core/Demo/Sources/Demo.swift b/Projects/Core/Demo/Sources/Demo.swift new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Core/Project.swift b/Projects/Core/Project.swift index 75da975..0b836b2 100644 --- a/Projects/Core/Project.swift +++ b/Projects/Core/Project.swift @@ -9,11 +9,14 @@ import ProjectDescription import ProjectDescriptionHelpers import DependencyPlugin +import ProjectDescription +import ProjectDescriptionHelpers +import DependencyPlugin + let project = Project.makeModule( name: "Core", - product: .framework, - dependencies: [ + targets: [.unitTest, .dynamicFramework, .demo], + internalDependencies: [ .Modules.thirdPartyLibs - ], - sources: ["Sources/**"] + ] ) diff --git a/Projects/Core/Tests/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Projects/Core/Tests/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Projects/Core/Tests/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Core/Tests/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Projects/Core/Tests/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/Projects/Core/Tests/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Core/Tests/Resources/Assets.xcassets/Contents.json b/Projects/Core/Tests/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Projects/Core/Tests/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Core/Tests/Resources/LaunchScreen.storyboard b/Projects/Core/Tests/Resources/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Projects/Core/Tests/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Core/Tests/Sources/UnitTest.swift b/Projects/Core/Tests/Sources/UnitTest.swift new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Data/Project.swift b/Projects/Data/Project.swift index a5ac5e2..fdd44a8 100644 --- a/Projects/Data/Project.swift +++ b/Projects/Data/Project.swift @@ -11,11 +11,10 @@ import DependencyPlugin let project = Project.makeModule( name: "Data", - product: .staticFramework, - dependencies: [ + targets: [.unitTest, .staticFramework], + internalDependencies: [ .domain, .Modules.networks - ], - sources: ["Sources/**"] + ] ) diff --git a/Projects/Data/Tests/Resources/dummy.txt b/Projects/Data/Tests/Resources/dummy.txt new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Data/Tests/Sources/UnitTest.swift b/Projects/Data/Tests/Sources/UnitTest.swift new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Domain/Project.swift b/Projects/Domain/Project.swift index a85e3f3..42b6719 100644 --- a/Projects/Domain/Project.swift +++ b/Projects/Domain/Project.swift @@ -11,9 +11,8 @@ import DependencyPlugin let project = Project.makeModule( name: "Domain", - product: .staticFramework, - dependencies: [ + targets: [.unitTest, .dynamicFramework], + internalDependencies: [ .core - ], - sources: ["Sources/**"] + ] ) diff --git a/Projects/Domain/Sources/Model/HourlyWeatherModel.swift b/Projects/Domain/Sources/Model/HourlyWeatherModel.swift index 7686df4..56713b6 100644 --- a/Projects/Domain/Sources/Model/HourlyWeatherModel.swift +++ b/Projects/Domain/Sources/Model/HourlyWeatherModel.swift @@ -8,8 +8,6 @@ import UIKit -import DSKit - public struct HourlyWeatherModel { public let time, weatherImage: String public let temparature: Double diff --git a/Projects/Domain/Sources/Model/WeatherForecastModel.swift b/Projects/Domain/Sources/Model/WeatherForecastModel.swift index 600abe4..a77ce97 100644 --- a/Projects/Domain/Sources/Model/WeatherForecastModel.swift +++ b/Projects/Domain/Sources/Model/WeatherForecastModel.swift @@ -8,8 +8,6 @@ import UIKit -import DSKit - public struct WeatherForecastModel { public let day: String public let weatherImg: UIImage diff --git a/Projects/Domain/Sources/UseCase/DefaultMainUseCase.swift b/Projects/Domain/Sources/UseCase/DefaultMainUseCase.swift index 8e2491c..8209225 100644 --- a/Projects/Domain/Sources/UseCase/DefaultMainUseCase.swift +++ b/Projects/Domain/Sources/UseCase/DefaultMainUseCase.swift @@ -5,6 +5,7 @@ // Created by 류희재 on 2023/10/24. // Copyright © 2023 hellohidi. All rights reserved. // +// import RxSwift import RxCocoa @@ -21,28 +22,69 @@ public final class DefaultMainUseCase: MainUseCase { self.repository = repository } - public func updateSearchResult(_ text: String) { + public func updateSearchResult(_ text: String) -> Observable<[CurrentWeatherModel]> { let defaultWeatherList: [CurrentWeatherModel] = [] - if text.isEmpty { - weatherList.accept(defaultWeatherList) - } else { - let filteredList = defaultWeatherList.filter { $0.place.contains(text) } - weatherList.accept(filteredList) - } + return Observable.just( + defaultWeatherList.filter { $0.place.contains(text)} + ) + + +// let defaultWeatherList: [CurrentWeatherModel] = [] +// if text.isEmpty { +// weatherList.accept(defaultWeatherList) +// } else { +// let filteredList = defaultWeatherList.filter { $0.place.contains(text) } +// weatherList.accept(filteredList) +// } } - public func getCurrentWeatherData() { + public func getCurrentWeatherData() -> Observable<[CurrentWeatherModel]> { let currentCityWeatherList = City.cityList.map { city in return repository.getCityWeatherData(city: city) } - - Observable.zip(currentCityWeatherList) - .subscribe(onNext: { cityWeatherArray in - let updateCurrentWeatherList = cityWeatherArray.compactMap { $0 } - self.weatherList.accept(updateCurrentWeatherList) - }) - .disposed(by: disposeBag) - - + return Observable.zip(currentCityWeatherList) } } + + + +//import RxSwift +//import RxCocoa +// +//import Core +// +//public final class DefaultMainUseCase: MainUseCase { +// public var weatherList = BehaviorRelay<[CurrentWeatherModel]>(value: []) +// +// public let repository: WeatherRepository +// private let disposeBag = DisposeBag() +// +// public init(repository: WeatherRepository) { +// self.repository = repository +// } +// +// public func updateSearchResult(_ text: String) { +// let defaultWeatherList: [CurrentWeatherModel] = [] +// if text.isEmpty { +// weatherList.accept(defaultWeatherList) +// } else { +// let filteredList = defaultWeatherList.filter { $0.place.contains(text) } +// weatherList.accept(filteredList) +// } +// } +// +// public func getCurrentWeatherData() { +// let currentCityWeatherList = City.cityList.map { city in +// return repository.getCityWeatherData(city: city) +// } +// +// Observable.zip(currentCityWeatherList) +// .subscribe(onNext: { cityWeatherArray in +// let updateCurrentWeatherList = cityWeatherArray.compactMap { $0 } +// self.weatherList.accept(updateCurrentWeatherList) +// }) +// .disposed(by: disposeBag) +// +// +// } +//} diff --git a/Projects/Domain/Sources/UseCase/Protocol/MainUseCase.swift b/Projects/Domain/Sources/UseCase/Protocol/MainUseCase.swift index 5f4545f..96fe9ff 100644 --- a/Projects/Domain/Sources/UseCase/Protocol/MainUseCase.swift +++ b/Projects/Domain/Sources/UseCase/Protocol/MainUseCase.swift @@ -10,8 +10,6 @@ import RxSwift import RxCocoa public protocol MainUseCase { - var weatherList: BehaviorRelay<[CurrentWeatherModel]> { get } - - func updateSearchResult(_ text: String) - func getCurrentWeatherData() + func updateSearchResult(_ text: String) -> Observable<[CurrentWeatherModel]> + func getCurrentWeatherData() -> Observable<[CurrentWeatherModel]> } diff --git a/Projects/Domain/Tests/Resources/dummy.txt b/Projects/Domain/Tests/Resources/dummy.txt new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Domain/Tests/Sources/Unittest.swift b/Projects/Domain/Tests/Sources/Unittest.swift new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Features/BaseFeatureDependency/Project.swift b/Projects/Features/BaseFeatureDependency/Project.swift index dd121a2..cdd7a9b 100644 --- a/Projects/Features/BaseFeatureDependency/Project.swift +++ b/Projects/Features/BaseFeatureDependency/Project.swift @@ -11,11 +11,10 @@ import DependencyPlugin let project = Project.makeModule( name: "BaseFeatureDependency", - product: .framework, - dependencies: [ - .Modules.dsKit, - .domain - ], - sources: ["Sources/**"] + targets: [.dynamicFramework], + internalDependencies: [ + .domain, + .Modules.dsKit + ] ) diff --git a/Projects/Features/BaseFeatureDependency/Sources/ViewControllable.swift b/Projects/Features/BaseFeatureDependency/Sources/ViewControllable.swift new file mode 100644 index 0000000..d8b1d4a --- /dev/null +++ b/Projects/Features/BaseFeatureDependency/Sources/ViewControllable.swift @@ -0,0 +1,26 @@ +// +// ViewControllable.swift +// BaseFeatureDependency +// +// Created by 류희재 on 7/4/24. +// Copyright © 2024 Weather-iOS. All rights reserved. +// + +import UIKit + +public protocol ViewControllable { + var viewController: UIViewController { get } + var asNavigationController: UINavigationController { get } +} +public extension ViewControllable where Self: UIViewController { + var viewController: UIViewController { + return self + } + + var asNavigationController: UINavigationController { + return self as? UINavigationController + ?? UINavigationController(rootViewController: self) + } +} + +extension UIViewController: ViewControllable {} diff --git a/Projects/Features/DetailFeature/Demo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Projects/Features/DetailFeature/Demo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Projects/Features/DetailFeature/Demo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Features/DetailFeature/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Projects/Features/DetailFeature/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/Projects/Features/DetailFeature/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Features/DetailFeature/Demo/Resources/Assets.xcassets/Contents.json b/Projects/Features/DetailFeature/Demo/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Projects/Features/DetailFeature/Demo/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Features/DetailFeature/Demo/Resources/LaunchScreen.storyboard b/Projects/Features/DetailFeature/Demo/Resources/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Projects/Features/DetailFeature/Demo/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Features/DetailFeature/Demo/Resources/dummy.txt b/Projects/Features/DetailFeature/Demo/Resources/dummy.txt new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Features/DetailFeature/Demo/Sources/Demo.swift b/Projects/Features/DetailFeature/Demo/Sources/Demo.swift new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Features/DetailFeature/Interface/Sources/DetailPresentable.swift b/Projects/Features/DetailFeature/Interface/Sources/DetailPresentable.swift new file mode 100644 index 0000000..7b7ebc1 --- /dev/null +++ b/Projects/Features/DetailFeature/Interface/Sources/DetailPresentable.swift @@ -0,0 +1,8 @@ +// +// DetailPresentable.swift +// DetailFeatureManifests +// +// Created by 류희재 on 6/28/24. +// + +import Foundation diff --git a/Projects/Features/DetailFeature/Project.swift b/Projects/Features/DetailFeature/Project.swift index bd483b2..95b3275 100644 --- a/Projects/Features/DetailFeature/Project.swift +++ b/Projects/Features/DetailFeature/Project.swift @@ -11,9 +11,8 @@ import DependencyPlugin let project = Project.makeModule( name: "DetailFeature", - product: .staticFramework, - dependencies: [ + targets: [.unitTest, .staticFramework, .demo, .interface], + interfaceDependencies: [ .Features.BaseFeatureDependency - ], - sources: ["Sources/**"] + ] ) diff --git a/Projects/Features/DetailFeature/Sources/View/DetailView.swift b/Projects/Features/DetailFeature/Sources/View/DetailView.swift index b7b4e02..9e5ddc6 100644 --- a/Projects/Features/DetailFeature/Sources/View/DetailView.swift +++ b/Projects/Features/DetailFeature/Sources/View/DetailView.swift @@ -18,7 +18,7 @@ import Then final class DetailView: UIView { let scrollView = UIScrollView() private let contentView = UIView() - private let backgroundImageView = UIImageView.init(image: DSKitAsset.backgroundImg.image) + private let backgroundImageView = UIImageView.init(image: DSKitAsset.backgroundIMG.image) let detailTopView = DetailTopView() let detailStickyHeaderView = DetailTopHeaderView() diff --git a/Projects/Features/DetailFeature/Tests/Resources/dummy.txt b/Projects/Features/DetailFeature/Tests/Resources/dummy.txt new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Features/DetailFeature/Tests/Sources/UnitTest.swift b/Projects/Features/DetailFeature/Tests/Sources/UnitTest.swift new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Features/MainFeature/Demo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Projects/Features/MainFeature/Demo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Projects/Features/MainFeature/Demo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Features/MainFeature/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Projects/Features/MainFeature/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/Projects/Features/MainFeature/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Features/MainFeature/Demo/Resources/Assets.xcassets/Contents.json b/Projects/Features/MainFeature/Demo/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Projects/Features/MainFeature/Demo/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Features/MainFeature/Demo/Resources/LaunchScreen.storyboard b/Projects/Features/MainFeature/Demo/Resources/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Projects/Features/MainFeature/Demo/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Features/MainFeature/Demo/Resources/dummy.txt b/Projects/Features/MainFeature/Demo/Resources/dummy.txt new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Features/MainFeature/Demo/Sources/Demo.Swift b/Projects/Features/MainFeature/Demo/Sources/Demo.Swift new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Features/MainFeature/Interface/Sources/MainPresentable.swift b/Projects/Features/MainFeature/Interface/Sources/MainPresentable.swift new file mode 100644 index 0000000..d171783 --- /dev/null +++ b/Projects/Features/MainFeature/Interface/Sources/MainPresentable.swift @@ -0,0 +1,8 @@ +// +// MainPresentable.swift +// MainFeatureManifests +// +// Created by 류희재 on 6/28/24. +// + +import Foundation diff --git a/Projects/Features/MainFeature/MainFeatureTests/MainViewModelTests.swift b/Projects/Features/MainFeature/MainFeatureTests/MainViewModelTests.swift new file mode 100644 index 0000000..7c508a3 --- /dev/null +++ b/Projects/Features/MainFeature/MainFeatureTests/MainViewModelTests.swift @@ -0,0 +1,39 @@ +// +// MainFeatureTests.swift +// MainFeatureTests +// +// Created by 류희재 on 6/25/24. +// Copyright © 2024 hellohidi. All rights reserved. +// + +import XCTest + +import RxRelay +import RxSwift +import MainFeature +import Domain +//import RxTest + +final class MainViewModelTests: XCTestCase { + private var viewModel: MainViewModel! + private var scheduler: + private var disposeBag: DisposeBag! + private var input: MainViewModel.Input! + private var output: MainViewModel.Output! + private var mainUseCase: MainUseCase! + + override func setUpWithError() throws { + self.mainUseCase = MockMainUseCase() + self.viewModel = MainViewModel.init( + mainCoordinator: nil, + mainUseCase: mainUseCase + ) + self.disposeBag = DisposeBag() +// self.scheduler = TestScheduler(initialClock: 0) + } + + override func tearDownWithError() throws { + self.viewModel = nil + self.disposeBag = nil + } +} diff --git a/Projects/Features/MainFeature/MainFeatureTests/Mock/MockMainUseCase.swift b/Projects/Features/MainFeature/MainFeatureTests/Mock/MockMainUseCase.swift new file mode 100644 index 0000000..87b8dd9 --- /dev/null +++ b/Projects/Features/MainFeature/MainFeatureTests/Mock/MockMainUseCase.swift @@ -0,0 +1,25 @@ +// +// MockLoginUseCase.swift +// MainFeatureTests +// +// Created by 류희재 on 6/25/24. +// Copyright © 2024 hellohidi. All rights reserved. +// + +import RxSwift +import RxCocoa + +import Domain +import Core + +public final class MockMainUseCase: MainUseCase { + public var weatherList = BehaviorRelay<[CurrentWeatherModel]>(value: []) + + public func updateSearchResult(_ text: String) -> Observable<[CurrentWeatherModel]> { + return Observable.just([]) + } + + public func getCurrentWeatherData() -> Observable<[CurrentWeatherModel]> { + return Observable.just([]) + } +} diff --git a/Projects/Features/MainFeature/Project.swift b/Projects/Features/MainFeature/Project.swift index 656b26f..3042503 100644 --- a/Projects/Features/MainFeature/Project.swift +++ b/Projects/Features/MainFeature/Project.swift @@ -11,10 +11,9 @@ import DependencyPlugin let project = Project.makeModule( name: "MainFeature", - product: .staticFramework, - dependencies: [ + targets: [.unitTest, .staticFramework, .demo, .interface], + interfaceDependencies: [ .Features.BaseFeatureDependency - ], - sources: ["Sources/**"] + ] ) diff --git a/Projects/Features/MainFeature/Sources/Coordinator/DefaultMainCoordinator.swift b/Projects/Features/MainFeature/Sources/Coordinator/DefaultMainCoordinator.swift index e9d1622..879281e 100644 --- a/Projects/Features/MainFeature/Sources/Coordinator/DefaultMainCoordinator.swift +++ b/Projects/Features/MainFeature/Sources/Coordinator/DefaultMainCoordinator.swift @@ -28,9 +28,9 @@ public class DefaultMainCoordinator: MainCoordinator { } public func start() { - let viewController = MainViewController( - viewModel: MainViewModel( - mainCoordinator: self, + let viewController = MainViewController_Reactor( + coordinator: self, + reactor: MainReactor( mainUseCase: DefaultMainUseCase( repository: DefaultWeatherRepository( urlSessionService: WeatherNetworkService() diff --git a/Projects/Features/MainFeature/Sources/ViewController/MainViewController.swift b/Projects/Features/MainFeature/Sources/ViewController/MainViewController.swift index 9de0840..75fc612 100644 --- a/Projects/Features/MainFeature/Sources/ViewController/MainViewController.swift +++ b/Projects/Features/MainFeature/Sources/ViewController/MainViewController.swift @@ -10,7 +10,6 @@ import UIKit import Domain import DSKit -import DetailFeature import SnapKit import Then diff --git a/Projects/Features/MainFeature/Sources/ViewController/MainViewController_Reactor.swift b/Projects/Features/MainFeature/Sources/ViewController/MainViewController_Reactor.swift new file mode 100644 index 0000000..e37d07d --- /dev/null +++ b/Projects/Features/MainFeature/Sources/ViewController/MainViewController_Reactor.swift @@ -0,0 +1,147 @@ +// +// MainViewController_Reactor.swift +// MainFeature +// +// Created by 류희재 on 6/6/24. +// Copyright © 2024 hellohidi. All rights reserved. +// + +import UIKit + +import Domain +import DSKit + +import SnapKit +import Then +import RxSwift +import RxGesture +import RxCocoa + +public final class MainViewController_Reactor : UIViewController { + + //MARK: - Properties + + public let reactor: MainReactor + public let coordinator: MainCoordinator? + + private let disposeBag = DisposeBag() + + public let searchBarDidChangeSubject = PublishSubject() + + //MARK: - UI Components + + let rootView = MainView() + + private lazy var seeMoreButton = UIBarButtonItem() + private lazy var weatherSearchController = UISearchController(searchResultsController: nil) + + //MARK: - Life Cycle + + public init(coordinator: MainCoordinator?, reactor: MainReactor) { + self.coordinator = coordinator + self.reactor = reactor + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func loadView() { + self.view = rootView + } + + public override func viewDidLoad() { + super.viewDidLoad() + + style() + setDelegate() + + bindInput() + bindOutput() + } + + //MARK: - Custom Method + + private func style() { + navigationItem.do { + $0.title = "날씨" + $0.rightBarButtonItem = seeMoreButton + $0.searchController = weatherSearchController + $0.hidesSearchBarWhenScrolling = false + } + + self.navigationController?.navigationBar.do { + $0.barTintColor = .black.withAlphaComponent(0.75) + $0.prefersLargeTitles = true + $0.titleTextAttributes = [.foregroundColor: UIColor.white] + $0.largeTitleTextAttributes = [ + .foregroundColor: UIColor.white, + .font: DSKitFontFamily.SFProDisplay.bold.font(size: 36)] + } + + seeMoreButton.do { + $0.image = DSKitAsset.menu.image + $0.tintColor = .white + } + + weatherSearchController.searchBar.do { + $0.searchTextField.setPlaceholderColor( + text: "도시 또는 공항 검색", + color: .lightGray + ) + $0.barTintColor = .black + $0.tintColor = .white + $0.searchTextField.backgroundColor = .darkGray + $0.setValue("취소", forKey: "cancelButtonText") + $0.setImage(DSKitAsset.search.image, for: .search, state: .normal) + } + + weatherSearchController.searchBar.searchTextField.textColor = .white + } + + private func setDelegate() { + weatherSearchController.searchResultsUpdater = self + } + + private func bindInput() { + rx.viewWillAppear.subscribe(with: self, onNext: { owner, _ in + owner.navigationController?.navigationBar.isHidden = false + }).disposed(by: disposeBag) + + rootView.weatherView.rx.itemSelected + .map { $0.item } + .asDriver(onErrorJustReturn: Int()) + .drive(with: self, onNext: { owner, page in + owner.coordinator?.pushToDetailVC(with: page) + }).disposed(by: disposeBag) + + rx.viewWillAppear + .map { MainReactor.Action.viewWillAppearEvent } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + searchBarDidChangeSubject + .map { MainReactor.Action.searchBarDidChangeEvent($0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindOutput() { + reactor.state.map { $0.weatherList } + .asDriver(onErrorJustReturn: []) + .drive(self.rootView.weatherView.rx.items( + cellIdentifier: MainWeatherCollectionViewCell.cellIdentifier, + cellType: MainWeatherCollectionViewCell.self) + ) { _, data, cell in + cell.dataBind(data) + }.disposed(by: disposeBag) + } +} + +extension MainViewController_Reactor: UISearchResultsUpdating { + public func updateSearchResults(for searchController: UISearchController) { + searchBarDidChangeSubject.onNext(searchController.searchBar.text ?? "") + } +} + diff --git a/Projects/Features/MainFeature/Sources/ViewModel/MainReactor.swift b/Projects/Features/MainFeature/Sources/ViewModel/MainReactor.swift new file mode 100644 index 0000000..9e97677 --- /dev/null +++ b/Projects/Features/MainFeature/Sources/ViewModel/MainReactor.swift @@ -0,0 +1,65 @@ +// +// MainReactor.swift +// MainFeature +// +// Created by 류희재 on 6/6/24. +// Copyright © 2024 hellohidi. All rights reserved. +// + +import Foundation + +import Domain + +import RxSwift +import ReactorKit + +public class MainReactor: Reactor { + + //MARK: - Properties + + public let mainUseCase: MainUseCase + + public let initialState: State = State() + + public enum Action { + case viewWillAppearEvent + case searchBarDidChangeEvent(String) + } + + public enum Mutation { + case getCurrentWeatherData([CurrentWeatherModel]) + case updateSearchResult([CurrentWeatherModel]) + } + + public struct State { + var weatherList: [CurrentWeatherModel] = [] + } + + init(mainUseCase: MainUseCase) { + self.mainUseCase = mainUseCase + } +} + +extension MainReactor { + public func mutate(action: Action) -> Observable { + switch action { + case .viewWillAppearEvent: + return mainUseCase.getCurrentWeatherData() + .map {Mutation.getCurrentWeatherData($0)} + case .searchBarDidChangeEvent(let city): + return mainUseCase.updateSearchResult(city) + .map {Mutation.updateSearchResult($0)} + } + } +} + +extension MainReactor { + public func reduce(state: State, mutation: Mutation) -> State { + var newstate = state + switch mutation { + case .getCurrentWeatherData(let weatherList), .updateSearchResult(let weatherList): + newstate.weatherList = weatherList + } + return newstate + } +} diff --git a/Projects/Features/MainFeature/Sources/ViewModel/MainViewModel.swift b/Projects/Features/MainFeature/Sources/ViewModel/MainViewModel.swift index 9d23f93..4e4d022 100644 --- a/Projects/Features/MainFeature/Sources/ViewModel/MainViewModel.swift +++ b/Projects/Features/MainFeature/Sources/ViewModel/MainViewModel.swift @@ -19,18 +19,18 @@ public final class MainViewModel { public let mainCoordinator: MainCoordinator? public let mainUseCase: MainUseCase - public init(mainCoordinator: MainCoordinator ,mainUseCase: MainUseCase) { + public init(mainCoordinator: MainCoordinator? ,mainUseCase: MainUseCase) { self.mainCoordinator = mainCoordinator self.mainUseCase = mainUseCase } - struct Input { + public struct Input { let viewWillAppearEvent: Observable let weatherListViewDidTapEvent: Observable let searchBarDidChangeEvent: Observable } - struct Output { + public struct Output { public var weatherList = BehaviorRelay<[CurrentWeatherModel]>(value: []) } @@ -54,15 +54,15 @@ public final class MainViewModel { private func bindOutput(output: Output, disposeBag: DisposeBag) { - mainUseCase.weatherList.subscribe(onNext: { weatherList in - output.weatherList.accept(weatherList) - }).disposed(by: disposeBag) +// mainUseCase.weatherList.subscribe(onNext: { weatherList in +// output.weatherList.accept(weatherList) +// }).disposed(by: disposeBag) } } -public extension MainViewModel { - func getWeatherList() -> [CurrentWeatherModel] { - return mainUseCase.weatherList.value - } -} +//public extension MainViewModel { +// func getWeatherList() -> [CurrentWeatherModel] { +// return mainUseCase.weatherList.value +// } +//} diff --git a/Projects/Features/MainFeature/Tests/Resources/dummy.txt b/Projects/Features/MainFeature/Tests/Resources/dummy.txt new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Features/MainFeature/Tests/Sources/UnitTest.swift b/Projects/Features/MainFeature/Tests/Sources/UnitTest.swift new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Features/RootFeature/Demo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Projects/Features/RootFeature/Demo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Projects/Features/RootFeature/Demo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Features/RootFeature/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Projects/Features/RootFeature/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/Projects/Features/RootFeature/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Features/RootFeature/Demo/Resources/Assets.xcassets/Contents.json b/Projects/Features/RootFeature/Demo/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Projects/Features/RootFeature/Demo/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Features/RootFeature/Demo/Resources/LaunchScreen.storyboard b/Projects/Features/RootFeature/Demo/Resources/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Projects/Features/RootFeature/Demo/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Features/RootFeature/Demo/Sources/Demo.swift b/Projects/Features/RootFeature/Demo/Sources/Demo.swift new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Features/RootFeature/Project.swift b/Projects/Features/RootFeature/Project.swift index 5e72e5e..54509aa 100644 --- a/Projects/Features/RootFeature/Project.swift +++ b/Projects/Features/RootFeature/Project.swift @@ -11,11 +11,9 @@ import DependencyPlugin let project = Project.makeModule( name: "RootFeature", - product: .staticFramework, - dependencies: [ + targets: [.unitTest, .staticFramework, .demo], + internalDependencies: [ .Features.Main.Feature, .Features.Detail.Feature - ], - sources: ["Sources/**"] + ] ) - diff --git a/Projects/Features/RootFeature/Sources/AppCoordinator.swift b/Projects/Features/RootFeature/Sources/AppCoordinator.swift index dabedd6..17f78ef 100644 --- a/Projects/Features/RootFeature/Sources/AppCoordinator.swift +++ b/Projects/Features/RootFeature/Sources/AppCoordinator.swift @@ -7,6 +7,7 @@ import UIKit +import Core import BaseFeatureDependency import MainFeature import DetailFeature diff --git a/Projects/Features/RootFeature/Tests/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Projects/Features/RootFeature/Tests/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Projects/Features/RootFeature/Tests/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Features/RootFeature/Tests/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Projects/Features/RootFeature/Tests/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/Projects/Features/RootFeature/Tests/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Features/RootFeature/Tests/Resources/Assets.xcassets/Contents.json b/Projects/Features/RootFeature/Tests/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Projects/Features/RootFeature/Tests/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Features/RootFeature/Tests/Resources/LaunchScreen.storyboard b/Projects/Features/RootFeature/Tests/Resources/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Projects/Features/RootFeature/Tests/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Features/RootFeature/Tests/Sources/UnitTest.swift b/Projects/Features/RootFeature/Tests/Sources/UnitTest.swift new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Modules/DSKit/Demo/ReSources/LaunchScreen.storyboard b/Projects/Modules/DSKit/Demo/ReSources/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Projects/Modules/DSKit/Demo/ReSources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Modules/DSKit/Demo/Sources/Demo.swift b/Projects/Modules/DSKit/Demo/Sources/Demo.swift new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Modules/DSKit/Project.swift b/Projects/Modules/DSKit/Project.swift index 1f79102..c1c8228 100644 --- a/Projects/Modules/DSKit/Project.swift +++ b/Projects/Modules/DSKit/Project.swift @@ -11,10 +11,9 @@ import DependencyPlugin let project = Project.makeModule( name: "DSKit", - product: .framework, - dependencies: [ + targets: [.unitTest, .demo, .dynamicFramework], + internalDependencies: [ .core ], - sources: ["Sources/**"], - resources: ["ReSources/**"] + hasResources: true ) diff --git a/Projects/Modules/DSKit/Tests/Resources/LaunchScreen.storyboard b/Projects/Modules/DSKit/Tests/Resources/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Projects/Modules/DSKit/Tests/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Modules/DSKit/Tests/Sources/UnitTests.swift b/Projects/Modules/DSKit/Tests/Sources/UnitTests.swift new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Modules/Networks/Project.swift b/Projects/Modules/Networks/Project.swift index b7f5c1b..77daeae 100644 --- a/Projects/Modules/Networks/Project.swift +++ b/Projects/Modules/Networks/Project.swift @@ -11,9 +11,8 @@ import DependencyPlugin let project = Project.makeModule( name: "Networks", - product: .staticFramework, - dependencies: [ + targets: [.unitTest, .staticFramework], + internalDependencies: [ .core - ], - sources: ["Sources/**"] + ] ) diff --git a/Projects/Modules/Networks/Tests/Resources/dummy.txt b/Projects/Modules/Networks/Tests/Resources/dummy.txt new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Modules/Networks/Tests/Sources/UnitTest.swift b/Projects/Modules/Networks/Tests/Sources/UnitTest.swift new file mode 100644 index 0000000..e69de29 diff --git a/Projects/Modules/ThirdPartyLibs/Project.swift b/Projects/Modules/ThirdPartyLibs/Project.swift index fcd32ba..6484a2b 100644 --- a/Projects/Modules/ThirdPartyLibs/Project.swift +++ b/Projects/Modules/ThirdPartyLibs/Project.swift @@ -11,9 +11,8 @@ import DependencyPlugin let project = Project.makeModule( name: "ThirdPartyLibs", - product: .framework, - packages: [], - dependencies: [ + targets: [.dynamicFramework], + externalDependencies: [ .SPM.SnapKit, .SPM.Then, .SPM.RxGesture, @@ -21,6 +20,7 @@ let project = Project.makeModule( .SPM.RxCocoa, .SPM.RxDataSources, .SPM.RxRelay, - .SPM.RxSwift + .SPM.RxSwift, + .SPM.ReactorKit ] ) diff --git a/Projects/Modules/ThirdPartyLibs/Sources/Example.swift b/Projects/Modules/ThirdPartyLibs/Sources/Example.swift new file mode 100644 index 0000000..e69de29 diff --git a/Tuist/Config.swift b/Tuist/Config.swift index cd6eded..5206f67 100644 --- a/Tuist/Config.swift +++ b/Tuist/Config.swift @@ -3,6 +3,7 @@ import ProjectDescription let config = Config( plugins: [ .local(path: .relativeToRoot("Plugins/DependencyPlugin")), - .local(path: .relativeToRoot("Plugins/EnvPlugin")) + .local(path: .relativeToRoot("Plugins/EnvPlugin")), + .local(path: .relativeToRoot("Plugins/ConfigPlugin")) ] ) diff --git a/Tuist/Dependencies.swift b/Tuist/Dependencies.swift index fc2a576..b7dbc97 100644 --- a/Tuist/Dependencies.swift +++ b/Tuist/Dependencies.swift @@ -14,7 +14,8 @@ let spm = SwiftPackageManagerDependencies([ .remote(url: "https://github.com/onevcat/Kingfisher.git", requirement: .upToNextMajor(from: "7.6.2")), .remote(url: "https://github.com/ReactiveX/RxSwift", requirement: .upToNextMajor(from: "6.6.0")), .remote(url: "https://github.com/RxSwiftCommunity/RxGesture", requirement: .upToNextMajor(from: "4.0.4")), - .remote(url: "https://github.com/RxSwiftCommunity/RxDataSources", requirement: .upToNextMajor(from: "5.0.2")) + .remote(url: "https://github.com/RxSwiftCommunity/RxDataSources", requirement: .upToNextMajor(from: "5.0.2")), + .remote(url: "https://github.com/ReactorKit/ReactorKit", requirement: .upToNextMajor(from: "3.0.0")) ], baseSettings: Settings.settings( )) diff --git a/Tuist/ProjectDescriptionHelpers/FeatureTargets.swift b/Tuist/ProjectDescriptionHelpers/FeatureTargets.swift new file mode 100644 index 0000000..13008ac --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/FeatureTargets.swift @@ -0,0 +1,28 @@ +// +// FeatureTargets.swift +// ProjectDescriptionHelpers +// +// Created by 류희재 on 6/24/24. +// + +import Foundation +import ProjectDescription + +public enum FeatureTarget { + case app // iOSApp + case interface // Feature Interface + case dynamicFramework + case staticFramework + case unitTest // Unit Test + case demo // Feature Excutable Test + + public var hasFramework: Bool { + switch self { + case .dynamicFramework, .staticFramework: return true + default: return false + } + } + public var hasDynamicFramework: Bool { return self == .dynamicFramework } +} + + diff --git a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift index 90ec246..d8f477e 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift @@ -1,78 +1,238 @@ import ProjectDescription +import ConfigPlugin import EnvPlugin + public extension Project { + /// Sumarry: 모듈을 만드는 함수 + /// + /// Discussion/Overview + /// + /// - Parameters: + /// - name: 프로젝트 이름 (모듈 이름) + /// - targets: 빌드할 타겟의 대상 + /// - organizationName: organization 이름 + /// - packages:SPM의 package + /// - internalDependencies: 내부 의존성 + /// - externalDependencies: 외부 의존성 + /// - interfaceDependencies: 인터페이스 의존성 + /// - sources: 소스 파일 + /// - hasResourcces: 리소스 파일 포함 여부 static func makeModule( name: String, - product: Product, + targets: Set = Set([.staticFramework, .unitTest, .demo]), organizationName: String = "hellohidi", packages: [Package] = [], - deploymentTarget: DeploymentTarget? = Environment.deploymentTarget, - dependencies: [TargetDependency] = [], + internalDependencies: [TargetDependency] = [], + externalDependencies: [TargetDependency] = [], + interfaceDependencies: [TargetDependency] = [], sources: SourceFilesList? = nil, - resources: ResourceFileElements? = nil, - infoPlist: InfoPlist = .extendingDefault(with: Project.appInfoPlist) + hasResources: Bool = false ) -> Project { - let settings: Settings = .settings( - base: [:], - configurations: [ - .debug(name: .debug), - .release(name: .release) - ], defaultSettings: .recommended) - - let appTarget = Target( - name: name, - platform: Environment.platform, - product: product, - bundleId: "\(Environment.bundlePrefix).\(name)", - deploymentTarget: deploymentTarget, - infoPlist: infoPlist, - sources: sources, - resources: resources, - dependencies: dependencies - ) - - let testTarget = Target( - name: "\(name)Tests", - platform: Environment.platform, - product: .unitTests, - bundleId: "\(Environment.bundlePrefix).\(name)Tests", - deploymentTarget: deploymentTarget, - infoPlist: .default, - sources: ["Tests/**"], - dependencies: [.target(name: name)] - ) - - let schemes: [Scheme] = [.makeScheme(target: .debug, name: name)] - - let targets: [Target] = [appTarget, testTarget] - + let configurationName: ConfigurationName = "Development" + let hasDynamicFramework = targets.contains(.dynamicFramework) + let baseSetting: SettingsDictionary = [:] + + var projectTargets: [Target] = [] + var schemes: [Scheme] = [] + + // MARK: - APP + + if targets.contains(.app) { + let bundleSuffix = name.contains("Demo") ? "test" : "release" + let infoPlist = name.contains("Demo") ? Project.demoInfoPlist : Project.appInfoPlist + let setting = baseSetting + + let target = Target( + name: name, + platform: env.platform, + product: .app, + bundleId: "\(env.bundlePrefix).\(bundleSuffix)", + deploymentTarget: env.deploymentTarget, + //infoPlist에 .extendingDefault로 Info.plsit에 추가 내용을 넣어준 이유는 tuist에서 .default 만들어주는 Info.plist는 앱을 실행할 때 화면이 어딘가 나사가 빠진상태로 실행되기 때문입니다. + infoPlist: .extendingDefault(with: infoPlist), + sources: .sources, + // 터미널 명령어랑 비슷한듯? 일단 바쁘니까 나중에 정리해보도록 하자ㅏ https://www.daleseo.com/glob-patterns/#google_vignette + resources: [.glob(pattern: "Resources/**", excluding: [])], + //entitlement: 주로 iOS 애플리케이션에서 특정 기능이나 권한을 활성화하기 위해 사용하는 설정 파일 +// entitlements: "\(name).entitlements", + dependencies: [ + internalDependencies, + externalDependencies + ].flatMap { $0 }, + settings: .settings(base: setting, configurations: XCConfig.project) + ) + projectTargets.append(target) + } + + //MARK: - Feature Interface + + if targets.contains(.interface) { + let setting = baseSetting + let target = Target( + name: "\(name)Interface", + platform: env.platform, + product: .framework, + bundleId: "\(env.bundlePrefix).\(name)Interface", + deploymentTarget: env.deploymentTarget, + infoPlist: .default, + sources: .interface, + dependencies: interfaceDependencies, + settings: .settings(base: setting,configurations: XCConfig.framework) + ) + + projectTargets.append(target) + } + + // MARK: - Framework + + if targets.contains(where: { $0.hasFramework }) { + let deps: [TargetDependency] = targets.contains(.interface) + ? [.target(name: "\(name)Interface")] + : [] + let settings = baseSetting + + let target = Target( + name: name, + platform: env.platform, + product: hasDynamicFramework ? .framework : .staticFramework, + bundleId: "\(env.bundlePrefix).\(name)", + deploymentTarget: env.deploymentTarget, + infoPlist: .default, + sources: .sources, + resources: hasResources ? [.glob(pattern: "Resources/**", excluding: [])] : [], + dependencies: deps + internalDependencies + externalDependencies, + settings: .settings(base: settings, configurations: XCConfig.framework) + ) + + projectTargets.append(target) + } + + //MARK: - Feature DemoApp + + if targets.contains(.demo) { + let deps: [TargetDependency] = [.target(name:name)] + let setting = baseSetting + + let target = Target( + name: "\(name)Demo", + platform: env.platform, + product: .app, + bundleId: "\(env.bundlePrefix).\(name)Demo", + deploymentTarget: env.deploymentTarget, + infoPlist: .extendingDefault(with: Project.demoInfoPlist), + sources: .demoSources, + resources: [.glob(pattern: "Demo/Resources/**", excluding: ["Demo/Resources/dummy.txt"])], + dependencies: deps, + settings: .settings(base: setting, configurations: XCConfig.demo) + ) + projectTargets.append(target) + } + + //MARK: - Unit Tests + + if targets.contains(.unitTest) { + let deps: [TargetDependency] = [.target(name: name)] + + let target = Target( + name: "\(name)Tests", + platform: env.platform, + product: .unitTests, + bundleId: "\(env.bundlePrefix).\(name)Tests", + deploymentTarget: env.deploymentTarget, + infoPlist: .default, + sources: .unitTests, + resources: [.glob(pattern: "Tests/Resources/**", excluding: [])], + dependencies: deps, + settings: .settings(base: SettingsDictionary().setCodeSignManual(), configurations: XCConfig.tests) + ) + + projectTargets.append(target) + } + + let additionalSchemes = targets.contains(.demo) ? + [ + Scheme.makeScheme(configs: configurationName, name: name), + Scheme.makeDemoScheme(configs: configurationName, name: name) + ] + : [ + Scheme.makeScheme(configs: configurationName, name: name) + ] + + schemes += additionalSchemes + + + var scheme = targets.contains(.app) ? appSchemes : schemes + + if name.contains("Demo") { + let testAppScheme = Scheme.makeScheme(configs: "QA", name: name) + scheme.append(testAppScheme) + } + return Project( name: name, - organizationName: organizationName, + organizationName: env.workspaceName, packages: packages, - settings: settings, - targets: targets, + settings: .settings(configurations: XCConfig.project), + targets: projectTargets, schemes: schemes ) } } -extension Scheme { - static func makeScheme(target: ConfigurationName, name: String) -> Scheme { - return Scheme( - name: name, +extension Project { + static let appSchemes: [Scheme] = [ + // PROD API, debug scheme : 실제 프로덕트 BaseURL을 사용하는 debug scheme + .init( + name: "\(env.workspaceName)-DEV", shared: true, - buildAction: .buildAction(targets: ["\(name)"]), + buildAction: .buildAction(targets: ["\(env.workspaceName)"]), testAction: .targets( - ["\(name)Tests"], - configuration: target, - options: .options(coverage: true, codeCoverageTargets: ["\(name)"]) + ["\(env.workspaceName)Tests", "\(env.workspaceName)UITests"], + configuration: "Development", + options: .options(coverage: true, codeCoverageTargets: ["\(env.workspaceName)"]) ), - runAction: .runAction(configuration: target), - archiveAction: .archiveAction(configuration: target), - profileAction: .profileAction(configuration: target), - analyzeAction: .analyzeAction(configuration: target) - ) - } + runAction: .runAction(configuration: "Development"), + archiveAction: .archiveAction(configuration: "Development"), + profileAction: .profileAction(configuration: "Development"), + analyzeAction: .analyzeAction(configuration: "Development") + ), + // Test API, debug scheme : 테스트 BaseURL을 사용하는 debug scheme + .init( + name: "\(env.workspaceName)-Test", + shared: true, + buildAction: .buildAction(targets: ["\(env.workspaceName)"]), + testAction: .targets( + ["\(env.workspaceName)Tests", "\(env.workspaceName)UITests"], + configuration: "Test", + options: .options(coverage: true, codeCoverageTargets: ["\(env.workspaceName)"]) + ), + runAction: .runAction(configuration: "Test"), + archiveAction: .archiveAction(configuration: "Test"), + profileAction: .profileAction(configuration: "Test"), + analyzeAction: .analyzeAction(configuration: "Test") + ), + // Test API, release scheme : 테스트 BaseURL을 사용하는 release scheme + .init( + name: "\(env.workspaceName)-QA", + shared: true, + buildAction: .buildAction(targets: ["\(env.workspaceName)"]), + runAction: .runAction(configuration: "QA"), + archiveAction: .archiveAction(configuration: "QA"), + profileAction: .profileAction(configuration: "QA"), + analyzeAction: .analyzeAction(configuration: "QA") + ), + // PROD API, release scheme : 실제 프로덕트 BaseURL을 사용하는 release scheme + .init( + name: "\(env.workspaceName)-PROD", + shared: true, + buildAction: .buildAction(targets: ["\(env.workspaceName)"]), + runAction: .runAction(configuration: "PROD"), + archiveAction: .archiveAction(configuration: "PROD"), + profileAction: .profileAction(configuration: "PROD"), + analyzeAction: .analyzeAction(configuration: "PROD") + ), + // Test API, debug scheme, Demo App Target + .makeDemoAppTestScheme() + ] } diff --git a/Tuist/ProjectDescriptionHelpers/Scheme+Template.swift b/Tuist/ProjectDescriptionHelpers/Scheme+Template.swift new file mode 100644 index 0000000..d9d3f6d --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Scheme+Template.swift @@ -0,0 +1,73 @@ +// +// Scheme+Template.swift +// ProjectDescriptionHelpers +// +// Created by 류희재 on 7/1/24. +// + +import ProjectDescription +import EnvPlugin + +extension Scheme { + /// Scheme 생성하는 method + /// 어떤 타겟을 빌드할 것인지, 어떤 테스트를 실행할 것인지 또한 어떤 환경에서 빌드할 것인지 설정 + /// + /// DEV : 실제 프로덕트 BaseURL을 사용하는 debug scheme + /// TEST : 테스트 BaseURL을 사용하는 debug scheme + /// QA : 테스트 BaseURL을 사용하는 release scheme + /// RELEASE : 실제 프로덕트 BaseURL을 사용하는 release scheme + + static func makeScheme(configs: ConfigurationName, name: String) -> Scheme { // 일반앱 + return Scheme( + name: name, + shared: true, + buildAction: .buildAction(targets: ["\(name)"]), + testAction: .targets( + ["\(name)Tests"], + configuration: configs, + options: .options(coverage: true, codeCoverageTargets: ["\(name)"]) + ), + runAction: .runAction(configuration: configs), + archiveAction: .archiveAction(configuration: configs), + profileAction: .profileAction(configuration: configs), + analyzeAction: .analyzeAction(configuration: configs) + ) + } + + static func makeDemoScheme(configs: ConfigurationName, name: String) -> Scheme { // 데모앱 + return Scheme( + name: "\(name)Demo", + shared: true, + buildAction: .buildAction(targets: ["\(name)Demo"]), + testAction: .targets( + ["\(name)Tests"], + configuration: configs, + options: .options(coverage: true, codeCoverageTargets: ["\(name)Demo"]) + ), + runAction: .runAction(configuration: configs), + archiveAction: .archiveAction(configuration: configs), + profileAction: .profileAction(configuration: configs), + analyzeAction: .analyzeAction(configuration: configs) + ) + } + + static func makeDemoAppTestScheme() -> Scheme { // 데모테스트앱 + let targetName = "\(env.workspaceName)-Demo" + return Scheme( + name: "\(targetName)-Test", + shared: true, + buildAction: .buildAction(targets: ["\(targetName)"]), + testAction: .targets( + ["\(targetName)Tests"], + configuration: "Test", + options: .options(coverage: true, codeCoverageTargets: ["\(targetName)"]) + ), + runAction: .runAction(configuration: "Test"), + archiveAction: .archiveAction(configuration: "Test"), + profileAction: .profileAction(configuration: "Test"), + analyzeAction: .analyzeAction(configuration: "Test") + ) + } +} + + diff --git a/Tuist/ProjectDescriptionHelpers/SourceFiles+Template.swift b/Tuist/ProjectDescriptionHelpers/SourceFiles+Template.swift new file mode 100644 index 0000000..3883cd8 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/SourceFiles+Template.swift @@ -0,0 +1,17 @@ +// +// SourceFiles+Template.swift +// ProjectDescriptionHelpers +// +// Created by 류희재 on 7/1/24. +// + +import ProjectDescription + +public extension SourceFilesList { + static let demoSources: SourceFilesList = "Demo/Sources/**/*.swift" + static let interface: SourceFilesList = "Interface/Sources/**/*.swift" + static let sources: SourceFilesList = "Sources/**/*.swift" + static let testing: SourceFilesList = "Testing/**" + static let unitTests: SourceFilesList = "Tests/Sources/**/*.swift" + static let uiTests: SourceFilesList = "UITests/**" +} diff --git a/graph.png b/graph.png index 32de867..fd15ef2 100644 Binary files a/graph.png and b/graph.png differ