From 40015e84221f6166c294b64abd4f80190b91890b Mon Sep 17 00:00:00 2001 From: baegteun Date: Wed, 29 May 2024 01:05:04 +0900 Subject: [PATCH 1/4] =?UTF-8?q?:lipstick:=20::=20[#334]=20GSMAuthenticatio?= =?UTF-8?q?nFormBuilderView=20/=20UIModel=EC=97=90=20=EB=94=B0=EB=9D=BC=20?= =?UTF-8?q?UI=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Demo/Sources/AppDelegate.swift | 72 +++++- .../Project.swift | 4 +- .../Intent/GSMAuthenticationFormIntent.swift | 7 + .../GSMAuthenticationFormIntentProtocol.swift | 3 + .../Model/GSMAuthenticationFormModel.swift | 7 + .../GSMAuthenticationFormModelProtocol.swift | 8 + .../Model/GSMAuthenticationFormUIModel.swift | 39 +++ .../GSMAuthenticationFormBuilderView.swift | 240 ++++++++++++++++++ .../Environment/FileFieldPresenter.swift | 44 ++++ .../Environment/OptionSelectPresenter.swift | 52 ++++ .../Scene/GSMAuthenticationFormView.swift | 7 + .../Sources/Source.swift | 1 - 12 files changed, 468 insertions(+), 16 deletions(-) create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/FileFieldPresenter.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/OptionSelectPresenter.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift delete mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Source.swift diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift index ef2bae04..7d456c3e 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift @@ -1,19 +1,63 @@ -import UIKit +import SwiftUI +@testable import GSMAuthenticationFormFeature @main -final class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? +struct GSMAuthenticationFormDemoApp: App { + var body: some Scene { + WindowGroup { + let uiModel = GSMAuthenticationFormUIModel( + areas: [ + .init( + title: "Area", + files: [], + sections: [ + .init( + title: "Section1", + description: "Description", + currentFieldCount: 2, + fields: [ + .init( + key: "text", + type: .text(value: nil), + placeholder: "text placeholder" + ), + .init( + key: "number", + type: .number(value: nil), + placeholder: "number placeholder" + ), + .init( + key: "file", + type: .file(fileName: nil), + placeholder: "file placeholder" + ) + ] + ), + .init( + title: "Section2", + description: "Description", + currentFieldCount: 3, + fields: [ + .init( + key: "boolean", + type: .boolean(isSelcted: false), + placeholder: nil + ), + .init( + key: "select", + type: .select(selectedValue: nil, values: ["a", "b", "c"]), + placeholder: "select placeholder" + ) + ] + ) + ] + ) + ] + ) - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool { - window = UIWindow(frame: UIScreen.main.bounds) - let viewController = UIViewController() - viewController.view.backgroundColor = .yellow - window?.rootViewController = viewController - window?.makeKeyAndVisible() - - return true + GSMAuthenticationFormBuilderView(uiModel: uiModel) { interaction in + + } + } } } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Project.swift b/Projects/Feature/GSMAuthenticationFormFeature/Project.swift index d699693a..63488bef 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Project.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Project.swift @@ -6,5 +6,7 @@ let project = Project.makeModule( name: ModulePaths.Feature.GSMAuthenticationFormFeature.rawValue, product: .staticLibrary, targets: [.interface, .unitTest, .demo], - internalDependencies: [] + internalDependencies: [ + .Feature.BaseFeature + ] ) diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift new file mode 100644 index 00000000..1c4069b4 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift @@ -0,0 +1,7 @@ +final class GSMAuthenticationFormIntent: GSMAuthenticationFormIntentProtocol { + weak var model: (any GSMAuthenticationFormActionProtocol)? + + init(model: any GSMAuthenticationFormActionProtocol) { + self.model = model + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift new file mode 100644 index 00000000..f58b3616 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift @@ -0,0 +1,3 @@ +protocol GSMAuthenticationFormIntentProtocol { + +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift new file mode 100644 index 00000000..948e1010 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift @@ -0,0 +1,7 @@ +final class GSMAuthenticationFormModel: GSMAuthenticationFormStateProtocol { + +} + +extension GSMAuthenticationFormModel: GSMAuthenticationFormActionProtocol { + +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift new file mode 100644 index 00000000..1b20355c --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift @@ -0,0 +1,8 @@ + +protocol GSMAuthenticationFormStateProtocol { + +} + +protocol GSMAuthenticationFormActionProtocol: AnyObject { + +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift new file mode 100644 index 00000000..fd7c9823 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift @@ -0,0 +1,39 @@ +import Foundation + +// swiftlint: disable nesting +struct GSMAuthenticationFormUIModel { + let areas: [Area] + + struct Area { + let title: String + let files: [File] + let sections: [Section] + + struct File { + let name: String + let url: String + } + + struct Section { + let title: String + let description: String + let currentFieldCount: Int + let fields: [Field] + + struct Field { + let key: String + let type: FieldType + let placeholder: String? + + enum FieldType { + case text(value: String?) + case number(value: Int?) + case boolean(isSelcted: Bool?) + case file(fileName: String?) + case select(selectedValue: String?, values: [String]) + } + } + } + } +} +// swiftlint: enable nesting diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift new file mode 100644 index 00000000..b2e30c20 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift @@ -0,0 +1,240 @@ +import DesignSystem +import Foundation +import SwiftUI + +enum FieldChanges { + case text(String) + case number(Int) + case boolean(Bool) + case file(URL) + case select(String) +} + +enum FieldInteraction { + case fieldChanges(key: String, fieldChanges: FieldChanges) + case fieldAdd(area: Int, section: Int, field: Int) +} + +struct GSMAuthenticationFormBuilderView: View { + @Environment(\.fileFieldPresenter) var fileFieldPresenter + @Environment(\.optionPickerPresenter) var optionPickerPresenter + private let uiModel: GSMAuthenticationFormUIModel + private let onFieldInteraction: (FieldInteraction) -> Void + private typealias Area = GSMAuthenticationFormUIModel.Area + private typealias Section = Area.Section + private typealias Field = Section.Field + + init( + uiModel: GSMAuthenticationFormUIModel, + onFieldInteraction: @escaping (FieldInteraction) -> Void + ) { + self.uiModel = uiModel + self.onFieldInteraction = onFieldInteraction + } + + var body: some View { + ScrollView { + areaList(areas: uiModel.areas) + } + } + + @ViewBuilder + private func areaList(areas: [Area]) -> some View { + LazyVStack(spacing: 16) { + ForEach(uiModel.areas, id: \.title) { area in + HStack(spacing: 16) { + SMSText(area.title, font: .title1) + + Spacer() + + SMSIcon(.downChevron) + .rotationEffect(false ? .degrees(90) : .degrees(0)) + .buttonWrapper { + } + + SMSIcon(.xmarkOutline) + .buttonWrapper { + } + } + + sectionList(sections: area.sections) + } + } + .padding(20) + } + + @ViewBuilder + private func sectionList(sections: [Section]) -> some View { + LazyVStack(spacing: 16) { + ForEach(sections, id: \.title) { section in + VStack { + ForEach(0.. some View { + LazyVStack(spacing: 16) { + ForEach(fields, id: \.key) { field in + fieldView(field: field) + } + } + } + + @ViewBuilder + private func fieldView(field: Field) -> some View { + switch field.type { + case let .text(value): + textTypeFieldView(key: field.key, placeholder: field.placeholder, text: value) + + case let .number(value): + numberTypeFieldView(key: field.key, placeholder: field.placeholder, number: value) + + case let .boolean(isSelected): + booleanTypeFieldView(key: field.key, isSelected: isSelected) + + case let .file(fileName): + fileTypeFieldView(key: field.key, placeholder: field.placeholder, fileName: fileName) + + case let .select(selectedValue, values): + selectTypeFieldView( + key: field.key, + placeholder: field.placeholder, + selectedValue: selectedValue, + values: values + ) + } + } + + @ViewBuilder + private func textTypeFieldView( + key: String, + placeholder: String?, + text: String? + ) -> some View { + SMSTextField( + placeholder ?? "", + text: Binding( + get: { text ?? "" }, + set: { newValue in + onFieldInteraction(.fieldChanges(key: key, fieldChanges: .text(newValue))) + } + ) + ) + } + + @ViewBuilder + private func numberTypeFieldView( + key: String, + placeholder: String?, + number: Int? + ) -> some View { + SMSTextField( + placeholder ?? "", + text: Binding( + get: { + if let number { + "\(number)" + } else { + "" + } + }, + set: { newValue in + guard let numberValue = Int(newValue) else { return } + onFieldInteraction(.fieldChanges(key: key, fieldChanges: .number(numberValue))) + } + ) + ) + } + + @ViewBuilder + private func booleanTypeFieldView( + key: String, + isSelected: Bool? + ) -> some View { + SMSSegmentedControl( + options: ["True", "False"], + selectedOption: (isSelected ?? false) ? "True" : "False" + ) { option in + switch option { + case "True": + onFieldInteraction(.fieldChanges(key: key, fieldChanges: .boolean(true))) + + case "False": + onFieldInteraction(.fieldChanges(key: key, fieldChanges: .boolean(false))) + + default: + return + } + } + } + + @ViewBuilder + private func fileTypeFieldView( + key: String, + placeholder: String?, + fileName: String? + ) -> some View { + SMSTextField( + placeholder ?? "", + text: Binding( + get: { fileName ?? "" }, + set: { _ in } + ) + ) + .disabled(true) + .overlay(alignment: .trailing) { + SMSIcon(.downChevron) + .padding(.trailing, 12) + } + } + + @ViewBuilder + private func selectTypeFieldView( + key: String, + placeholder: String?, + selectedValue: String?, + values: [String] + ) -> some View { + SMSTextField( + placeholder ?? "", + text: Binding( + get: { selectedValue ?? "" }, + set: { _ in } + ) + ) + .disabled(true) + .overlay(alignment: .trailing) { + SMSIcon(.downChevron) + .padding(.trailing, 12) + } + .simultaneousGesture( + TapGesture() + .onEnded { + optionPickerPresenter.presentOptionPicker( + options: values, + onOptionSelect: { selectedOption in + onFieldInteraction(.fieldChanges(key: key, fieldChanges: .select(selectedOption))) + } + ) + } + ) + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/FileFieldPresenter.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/FileFieldPresenter.swift new file mode 100644 index 00000000..c6663dac --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/FileFieldPresenter.swift @@ -0,0 +1,44 @@ +import Combine +import SwiftUI + +internal final class FileFieldPresenter { + private let isPresentedSubject = PassthroughSubject() + private let fileSelectSubject = PassthroughSubject() + private var onFileSelect: ((URL) -> Void)? + private var subscription = Set() + private var isPresentedPublisher: AnyPublisher { + isPresentedSubject.eraseToAnyPublisher() + } + private var fileSelectPublisher: AnyPublisher { + fileSelectSubject.eraseToAnyPublisher() + } + + fileprivate static let shared = FileFieldPresenter() + + private init() { + fileSelectSubject.sink { [weak self] url in + self?.onFileSelect?(url) + } + .store(in: &subscription) + } + + deinit { + subscription.removeAll() + } + + func presentFileImporter(onFileSelect: @escaping (URL) -> Void) { + self.onFileSelect = onFileSelect + isPresentedSubject.send(true) + } + + func sendSelectedFile(url: URL) { + fileSelectSubject.send(url) + isPresentedSubject.send(false) + } +} + +internal extension EnvironmentValues { + var fileFieldPresenter: FileFieldPresenter { + FileFieldPresenter.shared + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/OptionSelectPresenter.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/OptionSelectPresenter.swift new file mode 100644 index 00000000..5a0cf265 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/OptionSelectPresenter.swift @@ -0,0 +1,52 @@ +import Combine +import SwiftUI + +internal final class OptionPickerPresenter { + private let isPresentedSubject = PassthroughSubject() + private let optionsSubject = PassthroughSubject<[String], Never>() + private let optionSelectSubject = PassthroughSubject() + private var onOptionSelect: ((String) -> Void)? + private var subscription = Set() + var isPresentedPublisher: AnyPublisher { + isPresentedSubject.eraseToAnyPublisher() + } + var optionsPublisher: AnyPublisher<[String], Never> { + optionsSubject.eraseToAnyPublisher() + } + var optionSelectPublisher: AnyPublisher { + optionSelectSubject.eraseToAnyPublisher() + } + + fileprivate static let shared = OptionPickerPresenter() + + private init() { + optionSelectSubject.sink { [weak self] option in + self?.onOptionSelect?(option) + } + .store(in: &subscription) + } + + deinit { + subscription.removeAll() + } + + func presentOptionPicker( + options: [String], + onOptionSelect: @escaping (String) -> Void + ) { + self.onOptionSelect = onOptionSelect + optionsSubject.send(options) + isPresentedSubject.send(true) + } + + func sendSelectedOption(option: String) { + optionSelectSubject.send(option) + isPresentedSubject.send(false) + } +} + +internal extension EnvironmentValues { + var optionPickerPresenter: OptionPickerPresenter { + OptionPickerPresenter.shared + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift new file mode 100644 index 00000000..a177ee0c --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift @@ -0,0 +1,7 @@ +import SwiftUI + +struct GSMAuthenticationFormView: View { + var body: some View { + EmptyView() + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Source.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Source.swift deleted file mode 100644 index b1853ce6..00000000 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Source.swift +++ /dev/null @@ -1 +0,0 @@ -// This is for Tuist From 3fb25241acb5da9b16c351528ebd91b524c0155d Mon Sep 17 00:00:00 2001 From: baegteun Date: Sun, 26 May 2024 21:25:35 +0900 Subject: [PATCH 2/4] =?UTF-8?q?:seedling:=20::=20[#334]=20GSMAuthenticatio?= =?UTF-8?q?nForm=20Feature=20=EB=AA=A8=EB=93=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dependency+Target.swift | 8 ++++++ .../ModulePaths.swift | 1 + Projects/App/Project.swift | 1 + .../Demo/Resources/LaunchScreen.storyboard | 25 +++++++++++++++++++ .../Demo/Sources/AppDelegate.swift | 19 ++++++++++++++ .../Interface/Interface.swift | 1 + .../Project.swift | 10 ++++++++ .../Sources/Source.swift | 1 + .../GSMAuthenticationFormFeatureTest.swift | 11 ++++++++ 9 files changed, 77 insertions(+) create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Demo/Resources/LaunchScreen.storyboard create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Interface/Interface.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Project.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Source.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Tests/GSMAuthenticationFormFeatureTest.swift diff --git a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift index 9a7b9161..bde3afbc 100644 --- a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift +++ b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Target.swift @@ -9,6 +9,14 @@ public extension TargetDependency { } public extension TargetDependency.Feature { + static let GSMAuthenticationFormFeatureInterface = TargetDependency.project( + target: ModulePaths.Feature.GSMAuthenticationFormFeature.targetName(type: .interface), + path: .relativeToFeature(ModulePaths.Feature.GSMAuthenticationFormFeature.rawValue) + ) + static let GSMAuthenticationFormFeature = TargetDependency.project( + target: ModulePaths.Feature.GSMAuthenticationFormFeature.targetName(type: .sources), + path: .relativeToFeature(ModulePaths.Feature.GSMAuthenticationFormFeature.rawValue) + ) static let InputAuthenticationFeatureInterface = TargetDependency.project( target: ModulePaths.Feature.InputAuthenticationFeature.targetName(type: .interface), path: .relativeToFeature(ModulePaths.Feature.InputAuthenticationFeature.rawValue) diff --git a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift index 62d4fe3a..c417767c 100644 --- a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift +++ b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift @@ -10,6 +10,7 @@ public enum ModulePaths { public extension ModulePaths { enum Feature: String { + case GSMAuthenticationFormFeature case InputAuthenticationFeature case InputTeacherInfoFeature case InputPrizeInfoFeature diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index 89bd63eb..ae7c7c78 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -48,6 +48,7 @@ let targets: [Target] = [ .Feature.InputProjectInfoFeature, .Feature.InputTeacherInfoFeature, .Feature.InputAuthenticationFeature, + .Feature.GSMAuthenticationFormFeature, .Feature.MainFeature, .Feature.SplashFeature, .Feature.TechStackAppendFeature, diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Resources/LaunchScreen.storyboard b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Resources/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift new file mode 100644 index 00000000..ef2bae04 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift @@ -0,0 +1,19 @@ +import UIKit + +@main +final class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + window = UIWindow(frame: UIScreen.main.bounds) + let viewController = UIViewController() + viewController.view.backgroundColor = .yellow + window?.rootViewController = viewController + window?.makeKeyAndVisible() + + return true + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Interface/Interface.swift b/Projects/Feature/GSMAuthenticationFormFeature/Interface/Interface.swift new file mode 100644 index 00000000..b1853ce6 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Interface/Interface.swift @@ -0,0 +1 @@ +// This is for Tuist diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Project.swift b/Projects/Feature/GSMAuthenticationFormFeature/Project.swift new file mode 100644 index 00000000..d699693a --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Project.swift @@ -0,0 +1,10 @@ +import ProjectDescription +import ProjectDescriptionHelpers +import DependencyPlugin + +let project = Project.makeModule( + name: ModulePaths.Feature.GSMAuthenticationFormFeature.rawValue, + product: .staticLibrary, + targets: [.interface, .unitTest, .demo], + internalDependencies: [] +) diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Source.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Source.swift new file mode 100644 index 00000000..b1853ce6 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Source.swift @@ -0,0 +1 @@ +// This is for Tuist diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Tests/GSMAuthenticationFormFeatureTest.swift b/Projects/Feature/GSMAuthenticationFormFeature/Tests/GSMAuthenticationFormFeatureTest.swift new file mode 100644 index 00000000..534eeb86 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Tests/GSMAuthenticationFormFeatureTest.swift @@ -0,0 +1,11 @@ +import XCTest + +final class GSMAuthenticationFormFeatureTests: XCTestCase { + override func setUpWithError() throws {} + + override func tearDownWithError() throws {} + + func testExample() { + XCTAssertEqual(1, 1) + } +} From 7776dba016f4cc4927067487108c540100d38ba7 Mon Sep 17 00:00:00 2001 From: baegteun Date: Wed, 29 May 2024 01:05:04 +0900 Subject: [PATCH 3/4] =?UTF-8?q?:lipstick:=20::=20[#334]=20GSMAuthenticatio?= =?UTF-8?q?nFormBuilderView=20/=20UIModel=EC=97=90=20=EB=94=B0=EB=9D=BC=20?= =?UTF-8?q?UI=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Demo/Sources/AppDelegate.swift | 72 +++++- .../Project.swift | 4 +- .../Intent/GSMAuthenticationFormIntent.swift | 7 + .../GSMAuthenticationFormIntentProtocol.swift | 3 + .../Model/GSMAuthenticationFormModel.swift | 7 + .../GSMAuthenticationFormModelProtocol.swift | 8 + .../Model/GSMAuthenticationFormUIModel.swift | 39 +++ .../GSMAuthenticationFormBuilderView.swift | 240 ++++++++++++++++++ .../Environment/FileFieldPresenter.swift | 44 ++++ .../Environment/OptionSelectPresenter.swift | 52 ++++ .../Scene/GSMAuthenticationFormView.swift | 7 + .../Sources/Source.swift | 1 - 12 files changed, 468 insertions(+), 16 deletions(-) create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/FileFieldPresenter.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/OptionSelectPresenter.swift create mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift delete mode 100644 Projects/Feature/GSMAuthenticationFormFeature/Sources/Source.swift diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift index ef2bae04..7d456c3e 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Demo/Sources/AppDelegate.swift @@ -1,19 +1,63 @@ -import UIKit +import SwiftUI +@testable import GSMAuthenticationFormFeature @main -final class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? +struct GSMAuthenticationFormDemoApp: App { + var body: some Scene { + WindowGroup { + let uiModel = GSMAuthenticationFormUIModel( + areas: [ + .init( + title: "Area", + files: [], + sections: [ + .init( + title: "Section1", + description: "Description", + currentFieldCount: 2, + fields: [ + .init( + key: "text", + type: .text(value: nil), + placeholder: "text placeholder" + ), + .init( + key: "number", + type: .number(value: nil), + placeholder: "number placeholder" + ), + .init( + key: "file", + type: .file(fileName: nil), + placeholder: "file placeholder" + ) + ] + ), + .init( + title: "Section2", + description: "Description", + currentFieldCount: 3, + fields: [ + .init( + key: "boolean", + type: .boolean(isSelcted: false), + placeholder: nil + ), + .init( + key: "select", + type: .select(selectedValue: nil, values: ["a", "b", "c"]), + placeholder: "select placeholder" + ) + ] + ) + ] + ) + ] + ) - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool { - window = UIWindow(frame: UIScreen.main.bounds) - let viewController = UIViewController() - viewController.view.backgroundColor = .yellow - window?.rootViewController = viewController - window?.makeKeyAndVisible() - - return true + GSMAuthenticationFormBuilderView(uiModel: uiModel) { interaction in + + } + } } } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Project.swift b/Projects/Feature/GSMAuthenticationFormFeature/Project.swift index d699693a..63488bef 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Project.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Project.swift @@ -6,5 +6,7 @@ let project = Project.makeModule( name: ModulePaths.Feature.GSMAuthenticationFormFeature.rawValue, product: .staticLibrary, targets: [.interface, .unitTest, .demo], - internalDependencies: [] + internalDependencies: [ + .Feature.BaseFeature + ] ) diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift new file mode 100644 index 00000000..1c4069b4 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntent.swift @@ -0,0 +1,7 @@ +final class GSMAuthenticationFormIntent: GSMAuthenticationFormIntentProtocol { + weak var model: (any GSMAuthenticationFormActionProtocol)? + + init(model: any GSMAuthenticationFormActionProtocol) { + self.model = model + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift new file mode 100644 index 00000000..f58b3616 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Intent/GSMAuthenticationFormIntentProtocol.swift @@ -0,0 +1,3 @@ +protocol GSMAuthenticationFormIntentProtocol { + +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift new file mode 100644 index 00000000..948e1010 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModel.swift @@ -0,0 +1,7 @@ +final class GSMAuthenticationFormModel: GSMAuthenticationFormStateProtocol { + +} + +extension GSMAuthenticationFormModel: GSMAuthenticationFormActionProtocol { + +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift new file mode 100644 index 00000000..1b20355c --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormModelProtocol.swift @@ -0,0 +1,8 @@ + +protocol GSMAuthenticationFormStateProtocol { + +} + +protocol GSMAuthenticationFormActionProtocol: AnyObject { + +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift new file mode 100644 index 00000000..fd7c9823 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Model/GSMAuthenticationFormUIModel.swift @@ -0,0 +1,39 @@ +import Foundation + +// swiftlint: disable nesting +struct GSMAuthenticationFormUIModel { + let areas: [Area] + + struct Area { + let title: String + let files: [File] + let sections: [Section] + + struct File { + let name: String + let url: String + } + + struct Section { + let title: String + let description: String + let currentFieldCount: Int + let fields: [Field] + + struct Field { + let key: String + let type: FieldType + let placeholder: String? + + enum FieldType { + case text(value: String?) + case number(value: Int?) + case boolean(isSelcted: Bool?) + case file(fileName: String?) + case select(selectedValue: String?, values: [String]) + } + } + } + } +} +// swiftlint: enable nesting diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift new file mode 100644 index 00000000..b2e30c20 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift @@ -0,0 +1,240 @@ +import DesignSystem +import Foundation +import SwiftUI + +enum FieldChanges { + case text(String) + case number(Int) + case boolean(Bool) + case file(URL) + case select(String) +} + +enum FieldInteraction { + case fieldChanges(key: String, fieldChanges: FieldChanges) + case fieldAdd(area: Int, section: Int, field: Int) +} + +struct GSMAuthenticationFormBuilderView: View { + @Environment(\.fileFieldPresenter) var fileFieldPresenter + @Environment(\.optionPickerPresenter) var optionPickerPresenter + private let uiModel: GSMAuthenticationFormUIModel + private let onFieldInteraction: (FieldInteraction) -> Void + private typealias Area = GSMAuthenticationFormUIModel.Area + private typealias Section = Area.Section + private typealias Field = Section.Field + + init( + uiModel: GSMAuthenticationFormUIModel, + onFieldInteraction: @escaping (FieldInteraction) -> Void + ) { + self.uiModel = uiModel + self.onFieldInteraction = onFieldInteraction + } + + var body: some View { + ScrollView { + areaList(areas: uiModel.areas) + } + } + + @ViewBuilder + private func areaList(areas: [Area]) -> some View { + LazyVStack(spacing: 16) { + ForEach(uiModel.areas, id: \.title) { area in + HStack(spacing: 16) { + SMSText(area.title, font: .title1) + + Spacer() + + SMSIcon(.downChevron) + .rotationEffect(false ? .degrees(90) : .degrees(0)) + .buttonWrapper { + } + + SMSIcon(.xmarkOutline) + .buttonWrapper { + } + } + + sectionList(sections: area.sections) + } + } + .padding(20) + } + + @ViewBuilder + private func sectionList(sections: [Section]) -> some View { + LazyVStack(spacing: 16) { + ForEach(sections, id: \.title) { section in + VStack { + ForEach(0.. some View { + LazyVStack(spacing: 16) { + ForEach(fields, id: \.key) { field in + fieldView(field: field) + } + } + } + + @ViewBuilder + private func fieldView(field: Field) -> some View { + switch field.type { + case let .text(value): + textTypeFieldView(key: field.key, placeholder: field.placeholder, text: value) + + case let .number(value): + numberTypeFieldView(key: field.key, placeholder: field.placeholder, number: value) + + case let .boolean(isSelected): + booleanTypeFieldView(key: field.key, isSelected: isSelected) + + case let .file(fileName): + fileTypeFieldView(key: field.key, placeholder: field.placeholder, fileName: fileName) + + case let .select(selectedValue, values): + selectTypeFieldView( + key: field.key, + placeholder: field.placeholder, + selectedValue: selectedValue, + values: values + ) + } + } + + @ViewBuilder + private func textTypeFieldView( + key: String, + placeholder: String?, + text: String? + ) -> some View { + SMSTextField( + placeholder ?? "", + text: Binding( + get: { text ?? "" }, + set: { newValue in + onFieldInteraction(.fieldChanges(key: key, fieldChanges: .text(newValue))) + } + ) + ) + } + + @ViewBuilder + private func numberTypeFieldView( + key: String, + placeholder: String?, + number: Int? + ) -> some View { + SMSTextField( + placeholder ?? "", + text: Binding( + get: { + if let number { + "\(number)" + } else { + "" + } + }, + set: { newValue in + guard let numberValue = Int(newValue) else { return } + onFieldInteraction(.fieldChanges(key: key, fieldChanges: .number(numberValue))) + } + ) + ) + } + + @ViewBuilder + private func booleanTypeFieldView( + key: String, + isSelected: Bool? + ) -> some View { + SMSSegmentedControl( + options: ["True", "False"], + selectedOption: (isSelected ?? false) ? "True" : "False" + ) { option in + switch option { + case "True": + onFieldInteraction(.fieldChanges(key: key, fieldChanges: .boolean(true))) + + case "False": + onFieldInteraction(.fieldChanges(key: key, fieldChanges: .boolean(false))) + + default: + return + } + } + } + + @ViewBuilder + private func fileTypeFieldView( + key: String, + placeholder: String?, + fileName: String? + ) -> some View { + SMSTextField( + placeholder ?? "", + text: Binding( + get: { fileName ?? "" }, + set: { _ in } + ) + ) + .disabled(true) + .overlay(alignment: .trailing) { + SMSIcon(.downChevron) + .padding(.trailing, 12) + } + } + + @ViewBuilder + private func selectTypeFieldView( + key: String, + placeholder: String?, + selectedValue: String?, + values: [String] + ) -> some View { + SMSTextField( + placeholder ?? "", + text: Binding( + get: { selectedValue ?? "" }, + set: { _ in } + ) + ) + .disabled(true) + .overlay(alignment: .trailing) { + SMSIcon(.downChevron) + .padding(.trailing, 12) + } + .simultaneousGesture( + TapGesture() + .onEnded { + optionPickerPresenter.presentOptionPicker( + options: values, + onOptionSelect: { selectedOption in + onFieldInteraction(.fieldChanges(key: key, fieldChanges: .select(selectedOption))) + } + ) + } + ) + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/FileFieldPresenter.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/FileFieldPresenter.swift new file mode 100644 index 00000000..c6663dac --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/FileFieldPresenter.swift @@ -0,0 +1,44 @@ +import Combine +import SwiftUI + +internal final class FileFieldPresenter { + private let isPresentedSubject = PassthroughSubject() + private let fileSelectSubject = PassthroughSubject() + private var onFileSelect: ((URL) -> Void)? + private var subscription = Set() + private var isPresentedPublisher: AnyPublisher { + isPresentedSubject.eraseToAnyPublisher() + } + private var fileSelectPublisher: AnyPublisher { + fileSelectSubject.eraseToAnyPublisher() + } + + fileprivate static let shared = FileFieldPresenter() + + private init() { + fileSelectSubject.sink { [weak self] url in + self?.onFileSelect?(url) + } + .store(in: &subscription) + } + + deinit { + subscription.removeAll() + } + + func presentFileImporter(onFileSelect: @escaping (URL) -> Void) { + self.onFileSelect = onFileSelect + isPresentedSubject.send(true) + } + + func sendSelectedFile(url: URL) { + fileSelectSubject.send(url) + isPresentedSubject.send(false) + } +} + +internal extension EnvironmentValues { + var fileFieldPresenter: FileFieldPresenter { + FileFieldPresenter.shared + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/OptionSelectPresenter.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/OptionSelectPresenter.swift new file mode 100644 index 00000000..5a0cf265 --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Environment/OptionSelectPresenter.swift @@ -0,0 +1,52 @@ +import Combine +import SwiftUI + +internal final class OptionPickerPresenter { + private let isPresentedSubject = PassthroughSubject() + private let optionsSubject = PassthroughSubject<[String], Never>() + private let optionSelectSubject = PassthroughSubject() + private var onOptionSelect: ((String) -> Void)? + private var subscription = Set() + var isPresentedPublisher: AnyPublisher { + isPresentedSubject.eraseToAnyPublisher() + } + var optionsPublisher: AnyPublisher<[String], Never> { + optionsSubject.eraseToAnyPublisher() + } + var optionSelectPublisher: AnyPublisher { + optionSelectSubject.eraseToAnyPublisher() + } + + fileprivate static let shared = OptionPickerPresenter() + + private init() { + optionSelectSubject.sink { [weak self] option in + self?.onOptionSelect?(option) + } + .store(in: &subscription) + } + + deinit { + subscription.removeAll() + } + + func presentOptionPicker( + options: [String], + onOptionSelect: @escaping (String) -> Void + ) { + self.onOptionSelect = onOptionSelect + optionsSubject.send(options) + isPresentedSubject.send(true) + } + + func sendSelectedOption(option: String) { + optionSelectSubject.send(option) + isPresentedSubject.send(false) + } +} + +internal extension EnvironmentValues { + var optionPickerPresenter: OptionPickerPresenter { + OptionPickerPresenter.shared + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift new file mode 100644 index 00000000..a177ee0c --- /dev/null +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/GSMAuthenticationFormView.swift @@ -0,0 +1,7 @@ +import SwiftUI + +struct GSMAuthenticationFormView: View { + var body: some View { + EmptyView() + } +} diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Source.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Source.swift deleted file mode 100644 index b1853ce6..00000000 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Source.swift +++ /dev/null @@ -1 +0,0 @@ -// This is for Tuist From 79491e271f3be182e428f266c4fced2d339c78f0 Mon Sep 17 00:00:00 2001 From: baegteun Date: Fri, 31 May 2024 01:24:54 +0900 Subject: [PATCH 4/4] =?UTF-8?q?:sparkles:=20::=20[#334]=20GSMAuthenticatio?= =?UTF-8?q?nFormBuilder=EC=97=90=20file=20interaction=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{FileField.swift => SMSFileField.swift} | 36 ++++++++----------- .../GSMAuthenticationFormBuilderView.swift | 22 ++++++------ 2 files changed, 26 insertions(+), 32 deletions(-) rename Projects/Core/DesignSystem/Sources/File/{FileField.swift => SMSFileField.swift} (66%) diff --git a/Projects/Core/DesignSystem/Sources/File/FileField.swift b/Projects/Core/DesignSystem/Sources/File/SMSFileField.swift similarity index 66% rename from Projects/Core/DesignSystem/Sources/File/FileField.swift rename to Projects/Core/DesignSystem/Sources/File/SMSFileField.swift index ae94de89..1c389f60 100644 --- a/Projects/Core/DesignSystem/Sources/File/FileField.swift +++ b/Projects/Core/DesignSystem/Sources/File/SMSFileField.swift @@ -2,24 +2,25 @@ import SwiftUI import ViewUtil import UniformTypeIdentifiers -public struct FileField: View { - @State var fileText: String - @State var isShow: Bool - @State var isError: Bool - let errorText: String - let allowedContentTypes: [UTType] - let action: (URL) -> Void +public struct SMSFileField: View { + private let placeholder: String? + private let fileText: String? + @State private var isShow: Bool = false + private let isError: Bool + private let errorText: String + private let allowedContentTypes: [UTType] + private let action: (Result) -> Void public init( - fileText: String, - isShow: Bool, + _ placeholder: String? = nil, + fileText: String?, isError: Bool = false, errorText: String = "", allowedContentTypes: [UTType] = [.content], - action: @escaping (URL) -> Void + action: @escaping (Result) -> Void ) { + self.placeholder = placeholder self.fileText = fileText - self.isShow = isShow self.isError = isError self.errorText = errorText self.allowedContentTypes = allowedContentTypes @@ -28,9 +29,9 @@ public struct FileField: View { public var body: some View { SMSTextField( - "", + placeholder ?? "", text: Binding( - get: { fileText }, + get: { fileText ?? "" }, set: { _ in } ), errorText: errorText, @@ -57,14 +58,7 @@ public struct FileField: View { ), allowedContentTypes: allowedContentTypes ) { result in - switch result { - case .success(let url): - fileText = url.lastPathComponent - action(url) - - case .failure: - self.isError = true - } + action(result) } } } diff --git a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift index b2e30c20..cfcdad1a 100644 --- a/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift +++ b/Projects/Feature/GSMAuthenticationFormFeature/Sources/Scene/Components/GSMAuthenticationFormBuilderView.swift @@ -192,17 +192,17 @@ struct GSMAuthenticationFormBuilderView: View { placeholder: String?, fileName: String? ) -> some View { - SMSTextField( - placeholder ?? "", - text: Binding( - get: { fileName ?? "" }, - set: { _ in } - ) - ) - .disabled(true) - .overlay(alignment: .trailing) { - SMSIcon(.downChevron) - .padding(.trailing, 12) + SMSFileField( + placeholder, + fileText: fileName + ) { result in + switch result { + case let .success(url): + onFieldInteraction(.fieldChanges(key: key, fieldChanges: .file(url))) + + default: + return + } } }