Skip to content

Commit

Permalink
💄 :: [#334] GSMAuthenticationFormBuilderView / UIModel에 따라 UI 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
baekteun committed May 30, 2024
1 parent 3fb2524 commit 7776dba
Show file tree
Hide file tree
Showing 12 changed files with 468 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -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

}
}
}
}
4 changes: 3 additions & 1 deletion Projects/Feature/GSMAuthenticationFormFeature/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ let project = Project.makeModule(
name: ModulePaths.Feature.GSMAuthenticationFormFeature.rawValue,
product: .staticLibrary,
targets: [.interface, .unitTest, .demo],
internalDependencies: []
internalDependencies: [
.Feature.BaseFeature
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
final class GSMAuthenticationFormIntent: GSMAuthenticationFormIntentProtocol {
weak var model: (any GSMAuthenticationFormActionProtocol)?

init(model: any GSMAuthenticationFormActionProtocol) {
self.model = model
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
protocol GSMAuthenticationFormIntentProtocol {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
final class GSMAuthenticationFormModel: GSMAuthenticationFormStateProtocol {

}

extension GSMAuthenticationFormModel: GSMAuthenticationFormActionProtocol {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

protocol GSMAuthenticationFormStateProtocol {

}

protocol GSMAuthenticationFormActionProtocol: AnyObject {

}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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..<section.currentFieldCount, id: \.self) { index in
fieldList(fields: section.fields)

if section.currentFieldCount != 1 {
HStack {
SMSChip("추가") {
}

Spacer()

SMSIcon(.trash)
}
}
}
}
.titleWrapper(section.title)
.frame(maxWidth: .infinity)
}
}
}

@ViewBuilder
private func fieldList(fields: [Field]) -> 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)))
}
)
}
)
}
}
Loading

0 comments on commit 7776dba

Please sign in to comment.