diff --git a/Handy/Handy-Storybook/Atom/TextFieldViewController.swift b/Handy/Handy-Storybook/Atom/TextFieldViewController.swift new file mode 100644 index 0000000..41c9f81 --- /dev/null +++ b/Handy/Handy-Storybook/Atom/TextFieldViewController.swift @@ -0,0 +1,85 @@ +// +// TextFieldViewController.swift +// Handy +// +// Created by 정민지 on 11/18/24. +// + +import UIKit +import SnapKit + +import Handy + +final class TextFieldViewController: BaseViewController { + + private let defaultField: HandyTextFieldView = { + let textField = HandyTextFieldView() + textField.placeholder = "Input text" + textField.fieldLabelText = "Label" + textField.helperLabelText = "Helper text" + return textField + }() + + private let filledField: HandyTextFieldView = { + let textField = HandyTextFieldView() + textField.text = "Text Inputting" + textField.placeholder = "Input text" + textField.fieldLabelText = "Label" + textField.helperLabelText = "Helper text" + return textField + }() + + private let errorField: HandyTextFieldView = { + let textField = HandyTextFieldView() + textField.placeholder = "Input text" + textField.fieldLabelText = "Label" + textField.helperLabelText = "Helper text" + textField.isNegative = true + return textField + }() + + private let disabledField: HandyTextFieldView = { + let textField = HandyTextFieldView() + textField.placeholder = "Input text" + textField.fieldLabelText = "Label" + textField.helperLabelText = "Helper text" + textField.isDisabled = true + return textField + }() + + override func viewDidLoad() { + super.viewDidLoad() + setViewLayouts() + } + + override func setViewHierarchies() { + [ + defaultField, + filledField, + errorField, + disabledField + ].forEach { + view.addSubview($0) + } + } + + override func setViewLayouts() { + defaultField.snp.makeConstraints { + $0.bottom.equalTo(filledField.snp.top).offset(-16) + $0.horizontalEdges.equalToSuperview().inset(20) + } + filledField.snp.makeConstraints { + $0.centerY.equalToSuperview().offset(-50) + $0.top.equalTo(defaultField.snp.bottom).offset(16) + $0.horizontalEdges.equalToSuperview().inset(20) + } + errorField.snp.makeConstraints { + $0.top.equalTo(filledField.snp.bottom).offset(16) + $0.horizontalEdges.equalToSuperview().inset(20) + } + disabledField.snp.makeConstraints { + $0.top.equalTo(errorField.snp.bottom).offset(16) + $0.horizontalEdges.equalToSuperview().inset(20) + } + } +} diff --git a/Handy/Handy.xcodeproj/project.pbxproj b/Handy/Handy.xcodeproj/project.pbxproj index fd42298..a210df7 100644 --- a/Handy/Handy.xcodeproj/project.pbxproj +++ b/Handy/Handy.xcodeproj/project.pbxproj @@ -38,6 +38,10 @@ 02ED764C2C57BD09001569F1 /* HandyBoxButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02ED764B2C57BD09001569F1 /* HandyBoxButtonViewController.swift */; }; 2D41E8142C5A21930043161D /* FabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D41E8132C5A21930043161D /* FabViewController.swift */; }; 2D41E8162C5A21B50043161D /* HandyFab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D41E8152C5A21B50043161D /* HandyFab.swift */; }; + 2D8811892D2642A900B0B517 /* HandyTextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8811882D2642A800B0B517 /* HandyTextFieldView.swift */; }; + 2D88118B2D2642BD00B0B517 /* HandyTextFieldConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D88118A2D2642BC00B0B517 /* HandyTextFieldConstants.swift */; }; + 2D88118D2D2642CE00B0B517 /* HandyBaseTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D88118C2D2642CE00B0B517 /* HandyBaseTextField.swift */; }; + 2D88118F2D2642F900B0B517 /* TextFieldViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D88118E2D2642F900B0B517 /* TextFieldViewController.swift */; }; A56B3DE22C4E51D300C3610A /* HandyChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56B3DE12C4E51D300C3610A /* HandyChip.swift */; }; A5A12A7E2C57A6D900996916 /* ChipViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A12A7C2C57A6C200996916 /* ChipViewController.swift */; }; A5A12A7F2C57A92000996916 /* HandySematic.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5D02AFC2C46C5A70056CE7B /* HandySematic.swift */; }; @@ -115,6 +119,10 @@ 02ED764B2C57BD09001569F1 /* HandyBoxButtonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyBoxButtonViewController.swift; sourceTree = ""; }; 2D41E8132C5A21930043161D /* FabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FabViewController.swift; sourceTree = ""; }; 2D41E8152C5A21B50043161D /* HandyFab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyFab.swift; sourceTree = ""; }; + 2D8811882D2642A800B0B517 /* HandyTextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyTextFieldView.swift; sourceTree = ""; }; + 2D88118A2D2642BC00B0B517 /* HandyTextFieldConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyTextFieldConstants.swift; sourceTree = ""; }; + 2D88118C2D2642CE00B0B517 /* HandyBaseTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyBaseTextField.swift; sourceTree = ""; wrapsLines = 0; }; + 2D88118E2D2642F900B0B517 /* TextFieldViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldViewController.swift; sourceTree = ""; }; A56B3DE12C4E51D300C3610A /* HandyChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyChip.swift; sourceTree = ""; }; A5A12A7C2C57A6C200996916 /* ChipViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipViewController.swift; sourceTree = ""; }; A5F6D36A2C96F32D00FB961F /* HandyDivider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandyDivider.swift; sourceTree = ""; }; @@ -173,6 +181,7 @@ 2D41E8132C5A21930043161D /* FabViewController.swift */, 02ED764B2C57BD09001569F1 /* HandyBoxButtonViewController.swift */, A5A12A7C2C57A6C200996916 /* ChipViewController.swift */, + 2D88118E2D2642F900B0B517 /* TextFieldViewController.swift */, A5F6D36C2C97099C00FB961F /* DividerViewController.swift */, E51FBF9A2C5399A00097B0DA /* CheckBoxViewController.swift */, E51FBFA12C54CD350097B0DA /* RadioButtonViewController.swift */, @@ -228,6 +237,7 @@ 029E47FE2C49FD2E00D2F3B7 /* Atom */ = { isa = PBXGroup; children = ( + 2D8811872D26428500B0B517 /* HandyTextField */, 02ED762F2C52849A001569F1 /* HandyButton */, 029E47FC2C49FD1A00D2F3B7 /* HandyLabel.swift */, 2D41E8152C5A21B50043161D /* HandyFab.swift */, @@ -313,6 +323,16 @@ path = Extension; sourceTree = ""; }; + 2D8811872D26428500B0B517 /* HandyTextField */ = { + isa = PBXGroup; + children = ( + 2D8811882D2642A800B0B517 /* HandyTextFieldView.swift */, + 2D88118C2D2642CE00B0B517 /* HandyBaseTextField.swift */, + 2D88118A2D2642BC00B0B517 /* HandyTextFieldConstants.swift */, + ); + path = HandyTextField; + sourceTree = ""; + }; E5650D412C4D30B9002790CC /* Asset */ = { isa = PBXGroup; children = ( @@ -455,6 +475,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2D88118F2D2642F900B0B517 /* TextFieldViewController.swift in Sources */, 02150E4C2CCABAEB00EE690E /* SnackbarViewController.swift in Sources */, 2D41E8142C5A21930043161D /* FabViewController.swift in Sources */, A5A12A812C57A93C00996916 /* HandyPrimitive.swift in Sources */, @@ -486,6 +507,7 @@ E5D02AFD2C46C5A70056CE7B /* HandySematic.swift in Sources */, E5D02B002C480A180056CE7B /* HandyPrimitive.swift in Sources */, E51FBFA02C54CB260097B0DA /* HandyRadioButton.swift in Sources */, + 2D88118B2D2642BD00B0B517 /* HandyTextFieldConstants.swift in Sources */, E5669A3F2C443E7300DABC21 /* HandyBasicColor.swift in Sources */, 02ED76312C5284BB001569F1 /* HandyButtonProtocol.swift in Sources */, 02ED76352C5284F3001569F1 /* HandyTextButton.swift in Sources */, @@ -494,10 +516,12 @@ 02ED764A2C5779C3001569F1 /* UIImage+.swift in Sources */, 029E48002C49FD4000D2F3B7 /* HandyTypography.swift in Sources */, E5650D432C4D326D002790CC /* HandyCheckBox.swift in Sources */, + 2D88118D2D2642CE00B0B517 /* HandyBaseTextField.swift in Sources */, 029E47FD2C49FD1A00D2F3B7 /* HandyLabel.swift in Sources */, A56B3DE22C4E51D300C3610A /* HandyChip.swift in Sources */, E5650D472C512B07002790CC /* HandyIcon.swift in Sources */, E5650D472C512B07002790CC /* HandyIcon.swift in Sources */, + 2D8811892D2642A900B0B517 /* HandyTextFieldView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Handy/Handy/Source/Atom/HandyTextField/HandyBaseTextField.swift b/Handy/Handy/Source/Atom/HandyTextField/HandyBaseTextField.swift new file mode 100644 index 0000000..644a66a --- /dev/null +++ b/Handy/Handy/Source/Atom/HandyTextField/HandyBaseTextField.swift @@ -0,0 +1,226 @@ +// +// HandyBaseTextField.swift +// Handy +// +// Created by 정민지 on 11/18/24. +// + +import UIKit + +public class HandyBaseTextField: UITextField { + + // MARK: - 외부에서 지정할 수 있는 속성 + + /** + 텍스트 필드를 비활성화 시킬 때 사용합니다. + */ + @Invalidating(.layout) public var isDisabled: Bool = false { + didSet { + updateState() + } + } + + /** + 텍스트 필드의 오류 상태를 나타낼 때 사용합니다. + */ + @Invalidating(.layout) public var isNegative: Bool = false { + didSet { + updateState() + } + } + + // MARK: - 내부에서 사용되는 뷰 + + /** + 텍스트 필드 내의 입력을 초기화할 때 사용하는 Clear 버튼입니다. + */ + private let clearButton: UIButton = { + let button = UIButton(type: .system) + button.setImage(HandyIcon.cancelFilled, for: .normal) + button.tintColor = HandySemantic.iconBasicTertiary + button.isHidden = true + return button + }() + + // MARK: - 초기화 + + /** + 초기화 메소드입니다. 기본적인 텍스트 필드 속성과 Clear 버튼을 설정합니다. + */ + public init() { + super.init(frame: .zero) + setupTextField() + updatePlaceholderColorAndFont() + setupClearButton() + self.delegate = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - 설정 + + /** + 텍스트 필드의 기본 속성을 설정합니다. + - 테두리 색상, 패딩, 기본 배경색 등을 포함합니다. + */ + private func setupTextField() { + self.tintColor = HandySemantic.lineStatusPositive + self.layer.cornerRadius = HandySemantic.radiusM + self.layer.borderWidth = 1 + self.layer.borderColor = HandySemantic.bgBasicLight.cgColor + self.backgroundColor = HandySemantic.bgBasicLight + self.clipsToBounds = true + self.font = HandyFont.B1Rg16 + + let leftPaddingView = UIView(frame: CGRect(x: 0, y: 0, width: HandyTextFieldConstants.Dimension.leftMargin, height: 0)) + self.leftView = leftPaddingView + self.leftViewMode = .always + + let rightPaddingView = UIView(frame: CGRect(x: 0, y: 0, width: HandyTextFieldConstants.Dimension.rightMargin, height: 0)) + self.rightView = rightPaddingView + self.rightViewMode = .always + + self.snp.makeConstraints { + $0.height.greaterThanOrEqualTo(HandyTextFieldConstants.Dimension.textFieldHeight) + } + } + + /** + 플레이스홀더의 색상과 폰트를 업데이트합니다. + - 기본적으로 `HandyFont.B1Rg16`를 사용하며, `color` 매개변수를 통해 색상을 지정할 수 있습니다. + */ + private func updatePlaceholderColorAndFont(color: UIColor = HandySemantic.textBasicTertiary) { + let attributes: [NSAttributedString.Key: Any] = [ + .foregroundColor: color, + .font: HandyFont.B1Rg16 + ] + + if let placeholder = self.placeholder { + self.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: attributes) + } + } + + /** + Clear 버튼을 설정합니다. + - Clear 버튼은 텍스트 필드 오른쪽에 위치하며, 텍스트 입력 상태에 따라 표시됩니다. + */ + private func setupClearButton() { + addSubview(clearButton) + clearButton.addTarget(self, action: #selector(clearText), for: .touchUpInside) + + clearButton.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.trailing.equalToSuperview().inset(HandyTextFieldConstants.Dimension.clearButtonDefaultRightMargin) + $0.width.height.equalTo(HandyTextFieldConstants.Dimension.clearButtonSize) + } + + addTarget(self, action: #selector(textDidChange), for: .editingChanged) + } + + // MARK: - 상태 관리 + + /** + 텍스트 필드의 상태에 따라 UI를 업데이트합니다. + - `isDisabled`: 비활성화 상태를 나타냅니다. + - `isNegative`: 오류 상태를 나타냅니다. + */ + private func updateState() { + if isDisabled { + self.isUserInteractionEnabled = false + self.backgroundColor = HandySemantic.bgBasicLight + self.layer.borderColor = HandySemantic.bgBasicLight.cgColor + self.textColor = HandySemantic.textBasicDisabled + updatePlaceholderColorAndFont(color: HandySemantic.textBasicDisabled) + clearButton.isHidden = true + return + } + + if isNegative { + self.isUserInteractionEnabled = true + self.layer.borderColor = HandySemantic.lineStatusNegative.cgColor + self.textColor = HandySemantic.textBasicSecondary + updatePlaceholderColorAndFont(color: HandySemantic.textBasicTertiary) + clearButton.isHidden = false + return + } + + self.isUserInteractionEnabled = true + self.layer.borderColor = HandySemantic.bgBasicLight.cgColor + self.textColor = HandySemantic.textBasicPrimary + updatePlaceholderColorAndFont(color: HandySemantic.textBasicTertiary) + clearButton.isHidden = self.text?.isEmpty ?? true + } + + // MARK: - Clear 버튼 동작 + + /** + 텍스트 필드의 텍스트를 초기화합니다. + - Clear 버튼이 눌렸을 때 호출됩니다. + */ + @objc private func clearText() { + self.text = "" + clearButton.isHidden = true + } + + /** + 텍스트 필드의 텍스트 변경 시 호출됩니다. + - 텍스트가 입력되거나 삭제될 때 Clear 버튼의 표시 상태를 업데이트합니다. + */ + @objc private func textDidChange() { + clearButton.isHidden = self.text?.isEmpty ?? true + } + + // MARK: - Overridden Methods + + /** + Placeholder 및 텍스트 레이아웃을 설정합니다. + */ + public override func textRect(forBounds bounds: CGRect) -> CGRect { + return bounds.inset(by: UIEdgeInsets( + top: 0, + left: HandyTextFieldConstants.Dimension.leftMargin, + bottom: 0, + right: HandyTextFieldConstants.Dimension.rightMargin + )) + } + + /** + 텍스트 입력 시 레이아웃을 설정합니다. + */ + public override func editingRect(forBounds bounds: CGRect) -> CGRect { + return bounds.inset(by: UIEdgeInsets( + top: 0, + left: HandyTextFieldConstants.Dimension.leftMargin, + bottom: 0, + right: HandyTextFieldConstants.Dimension.rightMargin + )) + } +} + +// MARK: - UITextFieldDelegate + +extension HandyBaseTextField: UITextFieldDelegate { + /** + 텍스트 필드가 편집을 시작할 때 호출됩니다. + - isNegative 상태가 아닐 경우, 테두리 색상을 긍정 상태 색상으로 변경합니다. + - 편집 중일 때 시각적 피드백을 제공합니다. + - 호출 시점: 사용자가 텍스트 필드에 포커스를 줄 때. + */ + public func textFieldDidBeginEditing(_ textField: UITextField) { + if !isNegative { + self.layer.borderColor = HandySemantic.lineStatusPositive.cgColor + } + } + + /** + 텍스트 필드의 편집이 종료될 때 호출됩니다. + - 상태를 다시 업데이트하여 현재 상태에 맞는 UI를 반영합니다. + - 호출 시점: 사용자가 텍스트 필드의 포커스를 해제할 때. + */ + public func textFieldDidEndEditing(_ textField: UITextField) { + updateState() + } +} + diff --git a/Handy/Handy/Source/Atom/HandyTextField/HandyTextFieldConstants.swift b/Handy/Handy/Source/Atom/HandyTextField/HandyTextFieldConstants.swift new file mode 100644 index 0000000..311ef79 --- /dev/null +++ b/Handy/Handy/Source/Atom/HandyTextField/HandyTextFieldConstants.swift @@ -0,0 +1,48 @@ +// +// HandyTextFieldConstants.swift +// Handy +// +// Created by 정민지 on 11/18/24. +// + +import UIKit + +internal struct HandyTextFieldConstants { + internal enum Dimension { + + /** + 텍스트 필드 좌측 마진값입니다. + */ + static let leftMargin: CGFloat = 16 + + /** + 텍스트 필드 우측 마진값입니다. + */ + static let rightMargin: CGFloat = (clearButtonDefaultRightMargin * 2) + clearButtonSize + + /** + 텍스트 필드 높이입니다. + */ + static let textFieldHeight: CGFloat = 48 + + /** + Label, TextField, Helper text 내부 요소 간 간격입니다. + */ + static let subviewSpacing: CGFloat = 4 + + /** + clearButton과 TextField 사이 값 (=clearButton의 우측 마진)입니다. + */ + static let clearButtonDefaultRightMargin: CGFloat = 12 + + /** + clearButton 크기입니다. + */ + static let clearButtonSize: CGFloat = 20 + + /** + Label, Helper text가 TextField보다 왼쪽으로 더 들어가있는 Inset 값 입니다. + */ + static let labelInsetWidth: CGFloat = 4 + } +} diff --git a/Handy/Handy/Source/Atom/HandyTextField/HandyTextFieldView.swift b/Handy/Handy/Source/Atom/HandyTextField/HandyTextFieldView.swift new file mode 100644 index 0000000..5ee4cb6 --- /dev/null +++ b/Handy/Handy/Source/Atom/HandyTextField/HandyTextFieldView.swift @@ -0,0 +1,170 @@ +// +// HandyTextField.swift +// Handy +// +// Created by 정민지 on 11/18/24. +// + +import UIKit +import SnapKit + +public class HandyTextFieldView: UIView { + + // MARK: - 외부에서 지정할 수 있는 속성 + + /** + 텍스트 필드를 비활성화 시킬 때 사용합니다. + */ + @Invalidating(.layout) public var isDisabled: Bool = false { + didSet { + updateState() + } + } + + /** + 텍스트 필드의 오류 상태를 표현할 때 사용합니다. + */ + @Invalidating(.layout) public var isNegative: Bool = false { + didSet { + updateState() + } + } + + /** + 텍스트 필드의 텍스트를 설정하거나 가져올 때 사용합니다. + */ + public var text: String? { + get { return textField.text } + set { textField.text = newValue } + } + + /** + 텍스트 필드의 Placeholder를 설정할 때 사용합니다. + */ + public var placeholder: String? { + get { return textField.placeholder } + set { textField.placeholder = newValue } + } + + /** + 상단 라벨 텍스트를 설정하거나 가져올 때 사용합니다. + - 값이 `nil`일 경우 라벨이 숨겨집니다. + */ + public var fieldLabelText: String? { + get { return fieldLabel.text } + set { + fieldLabel.text = newValue + fieldLabel.isHidden = newValue == nil + } + } + + /** + 하단 헬퍼 라벨 텍스트를 설정하거나 가져올 때 사용합니다. + - 값이 `nil`일 경우 라벨이 숨겨집니다. + */ + public var helperLabelText: String? { + get { return helperLabel.text } + set { + helperLabel.text = newValue + helperLabel.isHidden = newValue == nil + } + } + + // MARK: - UI 구성 요소 + + /** + 텍스트 필드와 라벨들을 담고 있는 스택 뷰입니다. + */ + private let stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = HandyTextFieldConstants.Dimension.subviewSpacing + stackView.alignment = .fill + return stackView + }() + + /** + 텍스트 필드 상단에 위치한 라벨입니다. + */ + private let fieldLabelContainer = UIView() + + private let fieldLabel = HandyLabel(style: .B5Rg12) + + /** + 사용자 입력을 위한 기본 텍스트 필드입니다. + - 내부적으로 `HandyBaseTextField`를 사용하여 Clear 버튼 및 상태 관리를 포함합니다. + */ + public let textField = HandyBaseTextField() + + /** + 텍스트 필드 하단에 위치한 헬퍼 라벨입니다. + */ + private let helperLabelContainer = UIView() + + private let helperLabel = HandyLabel(style: .B5Rg12) + + // MARK: - 초기화 + + /** + 초기화 메소드입니다. 기본적으로 뷰의 UI 구성 요소를 설정합니다. + */ + public init() { + super.init(frame: .zero) + setupView() + updateState() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - 뷰 구성 + + /** + 뷰의 기본 UI 요소를 설정하고 제약 조건을 추가합니다. + */ + private func setupView() { + addSubview(stackView) + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + stackView.addArrangedSubview(fieldLabelContainer) + stackView.addArrangedSubview(textField) + stackView.addArrangedSubview(helperLabelContainer) + + fieldLabelContainer.addSubview(fieldLabel) + fieldLabel.snp.makeConstraints { + $0.leading.equalToSuperview().inset(HandyTextFieldConstants.Dimension.labelInsetWidth) + $0.trailing.verticalEdges.equalToSuperview() + } + + helperLabelContainer.addSubview(helperLabel) + helperLabel.snp.makeConstraints { + $0.leading.equalToSuperview().inset(HandyTextFieldConstants.Dimension.labelInsetWidth) + $0.trailing.verticalEdges.equalToSuperview() + } + + self.snp.makeConstraints { + $0.height.greaterThanOrEqualTo(HandyTextFieldConstants.Dimension.textFieldHeight) + } + } + + // MARK: - 상태 관리 + + /** + `isDisabled` 및 `isNegative` 속성에 따라 라벨과 텍스트 필드 상태를 업데이트합니다. + */ + private func updateState() { + textField.isDisabled = isDisabled + textField.isNegative = isNegative + + fieldLabel.textColor = HandySemantic.textBasicTertiary + helperLabel.textColor = HandySemantic.textBasicTertiary + + if isNegative { + helperLabel.textColor = HandySemantic.lineStatusNegative + } + } +} +