Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC7",
"green" : "0xF5",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "connect_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "connect_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "connect_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "grow_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "grow_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "grow_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "outside_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "outside_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "outside_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "rest_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "rest_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "rest_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "shine_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "shine_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "shine_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "wakeup_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "wakeup_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wakeup_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
166 changes: 166 additions & 0 deletions Projects/Presentation/Sources/Common/Component/RoutineCardView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
//
// RoutineCardView.swift
// Presentation
//
// Created by 최정인 on 8/16/25.
//

import Domain
import SnapKit
import UIKit

final class RoutineCardView: UIView {
private enum Layout {
static let horizontalMargin: CGFloat = 16
static let cornerRadius: CGFloat = 12
static let headerInfoStackViewSpacing: CGFloat = 10
static let headerInfoStackViewTopSpacing: CGFloat = 14
static let subRoutineStackViewSpacing: CGFloat = 2
static let subRoutineStackViewTopSpacing: CGFloat = 10
static let subRoutineStackViewBottomSpacing: CGFloat = 14
static let subRoutineLabelHeight: CGFloat = 20
static let categoryIconSize: CGFloat = 32
static let plusImageSize: CGFloat = 24
static let plusButtonTopSpacing: CGFloat = 14
static let plusButtonTrailingSpacing: CGFloat = 7
static let plusButtonSize: CGFloat = 32
static let grayLineTopSpacing: CGFloat = 10
static let grayLineHeight: CGFloat = 1
}

private let headerInfoStackView = UIStackView()
private let categoryIconView = RoutineCategoryIcon(routineCategory: .connection)
private let titleLabel = UILabel()
private let editButton = UIButton()
private let deleteButton = UIButton()
private let plusButton = UIButton()
private let grayLine = UIView()
private let subRoutineLabel = UILabel()
private let subRoutineStackView = UIStackView()

init() {
super.init(frame: .zero)
configureAttribute()
configureLayout()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func configureAttribute() {
backgroundColor = .white
layer.masksToBounds = true
layer.cornerRadius = Layout.cornerRadius

headerInfoStackView.axis = .horizontal
headerInfoStackView.spacing = Layout.headerInfoStackViewSpacing

titleLabel.text = "개운하게 일어나기"
titleLabel.font = BitnagilFont(style: .body1, weight: .semiBold).font
titleLabel.textColor = BitnagilColor.gray10

let plusImage = BitnagilIcon.plusIcon?
.resizeAspectFit(to: CGSize(width: Layout.plusImageSize, height: Layout.plusImageSize))
plusButton.setImage(plusImage, for: .normal)
plusButton.tintColor = BitnagilColor.gray10

grayLine.backgroundColor = BitnagilColor.gray97

subRoutineStackView.axis = .vertical
subRoutineStackView.spacing = Layout.subRoutineStackViewSpacing

subRoutineLabel.text = "세부 루틴"
subRoutineLabel.font = BitnagilFont(style: .body2, weight: .medium).font
subRoutineLabel.textColor = BitnagilColor.gray40
subRoutineLabel.snp.makeConstraints { make in
make.height.equalTo(Layout.subRoutineLabelHeight)
}
subRoutineStackView.addArrangedSubview(subRoutineLabel)

Comment on lines +76 to +80

@coderabbitai coderabbitai Bot Aug 17, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

UIStackView에 추가되기 전에 제약을 설치하고 있어 런타임 크래시 위험이 있습니다.

SnapKit 제약은 공통 슈퍼뷰가 있어야 설치 가능합니다. subRoutineLabel을 arrangedSubview로 추가한 뒤 제약을 설정하세요.

아래와 같이 순서를 교체해 주세요.

-        subRoutineLabel.snp.makeConstraints { make in
-            make.height.equalTo(Layout.subRoutineLabelHeight)
-        }
-        subRoutineStackView.addArrangedSubview(subRoutineLabel)
+        subRoutineStackView.addArrangedSubview(subRoutineLabel)
+        subRoutineLabel.snp.makeConstraints { make in
+            make.height.equalTo(Layout.subRoutineLabelHeight)
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
subRoutineLabel.snp.makeConstraints { make in
make.height.equalTo(Layout.subRoutineLabelHeight)
}
subRoutineStackView.addArrangedSubview(subRoutineLabel)
subRoutineStackView.addArrangedSubview(subRoutineLabel)
subRoutineLabel.snp.makeConstraints { make in
make.height.equalTo(Layout.subRoutineLabelHeight)
}
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/Component/RoutineCardView.swift around
lines 76–80, the code installs SnapKit constraints on subRoutineLabel before it
is added to the UIStackView which can cause a runtime crash because constraints
require a common superview; move
subRoutineStackView.addArrangedSubview(subRoutineLabel) to come before
subRoutineLabel.snp.makeConstraints { ... } so the label is in the stack view
(common superview) when constraints are activated.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

superView에 접근하는 것은 아니라 괜찮을 것 같은데, 문제가 될까요?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Let's examine the SnapKit behavior and look for similar patterns in the codebase
echo "=== Searching for similar SnapKit constraint patterns in the codebase ==="
rg -n "\.snp\.makeConstraints" -A 3 -B 1 | head -20

echo -e "\n=== Looking for UIStackView addArrangedSubview patterns ==="
rg -n "addArrangedSubview" -A 2 -B 2 | head -15

echo -e "\n=== Checking for height constraint patterns ==="
rg -n "make\.height\.equalTo" -A 1 -B 1 | head -15

Length of output: 6313


🌐 Web query:

SnapKit iOS constraints view hierarchy superview requirement UIStackView addArrangedSubview order

💡 Result:

SnapKit requires that constraints are made between views that share a common superview in the hierarchy, and for views within a UIStackView, constraints should not typically reference the stackView itself as superview; instead, you should constrain subviews within the stack to their own content, or use container views if more control is needed[3][4][7][1]. When using UIStackView’s addArrangedSubview method, the order you add subviews determines their display order, and each addArrangedSubview also makes the view a subview of the stack view—there’s no extra step needed[5][2][8].

Context and Details:

  • SnapKit Superview Requirement:
    SnapKit creates constraints between views, usually to their superview, using makeConstraints or equalToSuperview() syntax[4][7][1].
    Example:

    box.snp.makeConstraints { make in
        make.edges.equalToSuperview().inset(20)
    }

    The constrained view must already be in its superview's hierarchy before constraints are defined; otherwise, layout errors may occur[4][7][10].

  • UIStackView and Constraints:
    UIStackView manages its arrangedSubviews’ layout automatically. If you directly constrain an arrangedSubview’s edges to the stackView (e.g. make.left.right.equalToSuperview() inside the stackView’s arrangedSubview), only certain constraints will work; others may be ignored or conflict, because the stackView itself only manages the placement of arrangedSubviews and not their internal layout[3][5].

    • If you need more control (e.g. pin left/right), the recommended approach is:
      • Add a container UIView as an arrangedSubview to the stackView.
      • Add your custom view inside the container.
      • Apply SnapKit constraints between your custom view and the container[3].

    Example (from [3]):

    let containerView = UIView()
    let customView = UIView()
    containerView.addSubview(customView)
    customView.snp.makeConstraints { make in
        make.left.right.equalToSuperview().inset(15)
        make.height.equalTo(44)
    }
    stackView.addArrangedSubview(containerView)
  • arrangedSubviews and Hierarchy:
    When you use addArrangedSubview(view) or insertArrangedSubview(view, at:), UIKit automatically adds the view as a subview of the stackView (i.e., you do not need to call addSubview yourself)[5].
    The order in which you call addArrangedSubview or insertArrangedSubview determines how the views appear in the stack (top-to-bottom or left-to-right depending on stack axis)[2][5][8].

  • Removal:

    • To remove from the arranged subview list:
      stackView.removeArrangedSubview(view) (removes layout involvement, but not from the view hierarchy)[5].
    • To remove the view completely:
      view.removeFromSuperview()[5].
  • Constraint Pitfalls:

    • Directly pinning edges of a view inside a UIStackView to the stackView (e.g. make.left.right.equalToSuperview()) may not always work as expected, due to how UIStackView manages the frames of its arrangedSubviews[3].
    • Use container views for complex layout needs, or rely on StackView’s alignment/distribution/content hugging and compression resistance properties for simpler cases[3][2][5].

Summary Table: SnapKit + UIStackView Behavior

Normal UIView Constraints UIStackView arrangedSubview Constraints
Superview requirement Must be in superview for SnapKit ArrangedSubview is auto-added as subview
addArrangedSubview N/A Adds view as subview and as arrangedSubview
Constraint location To direct superview or siblings NOT directly to UIStackView; use containers
Order of appearance addSubview order is z-index addArrangedSubview order is display order

In summary:

  • Use SnapKit’s constraints only once views are in appropriate superview hierarchy[4][7][10].
  • With UIStackView, add views via addArrangedSubview for display order[5][2].
  • For custom constraints inside stack views, add a container view as arrangedSubview, and constrain your actual view inside that container[3].
  • Direct constraint of arrangedSubview's edges to the stackView is not supported by UIKit; rely on stackView’s properties or containers for customization[3][2][5].

Citations:


@taipaise 좋은 질문입니다! 기술적으로 살펴보면 다음과 같습니다:

현재 코드가 당장 크래시를 일으키지 않을 수 있는 이유:

  • make.height.equalTo(Layout.subRoutineLabelHeight)는 다른 뷰를 참조하지 않는 intrinsic constraint입니다
  • 단순한 높이 제약이므로 상대적으로 안전할 수 있습니다

하지만 여전히 문제가 될 수 있는 이유:

  1. SnapKit의 내부 동작: SnapKit은 제약을 설치할 때 뷰 계층에서 적절한 위치를 찾아야 합니다
  2. 일관성 부족: 코드베이스의 다른 부분들을 보면 대부분 뷰를 슈퍼뷰에 추가한 후 제약을 설정하고 있습니다
  3. iOS 버전별 차이: iOS 버전이나 SnapKit 버전에 따라 동작이 달라질 수 있습니다

권장하는 순서:

// 안전하고 일관된 방법
subRoutineStackView.addArrangedSubview(subRoutineLabel)
subRoutineLabel.snp.makeConstraints { make in
    make.height.equalTo(Layout.subRoutineLabelHeight)
}

현재 코드가 동작할 가능성이 높지만, 베스트 프랙티스를 따르는 것이 더 안전하고 유지보수에 좋습니다. 어떻게 생각하시나요?


✏️ Learnings added
Learnt from: taipaise
PR: YAPP-Github/Bitnagil-iOS#51
File: Projects/Presentation/Sources/Common/Component/RoutineCardView.swift:76-80
Timestamp: 2025-08-17T13:30:29.321Z
Learning: In SnapKit, even height constraints (intrinsic constraints) should be applied after adding the view to its superview hierarchy. While simple height constraints might work before adding to superview, it's not guaranteed and goes against best practices. The recommended approach is to call addArrangedSubview first, then apply constraints.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

["물 마시기", "물 마시기", "물 마시기"].forEach {
let subRoutineTitleLabel = UILabel()
subRoutineTitleLabel.text = "• \($0)"
subRoutineTitleLabel.font = BitnagilFont(style: .body2, weight: .medium).font
subRoutineTitleLabel.textColor = BitnagilColor.gray40
subRoutineTitleLabel.snp.makeConstraints { make in
make.height.equalTo(Layout.subRoutineLabelHeight)
}
subRoutineStackView.addArrangedSubview(subRoutineTitleLabel)
}
Comment on lines +81 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

반복 구간에서도 동일한 제약 설치 순서 문제 발생

각 subRoutineTitleLabel도 UIStackView에 추가하기 전에 제약을 설치하고 있습니다. 순서를 바꾸세요. 또한 더미 텍스트는 다음 PR에서 실제 데이터 바인딩으로 교체 예정이므로 TODO 표기 권장합니다.

수정 예시는 다음과 같습니다.

-        ["물 마시기", "물 마시기", "물 마시기"].forEach {
+        // TODO(T3-167-followup): 실제 서버 v2 연동 시 바인딩된 세부 루틴 목록으로 교체
+        ["물 마시기", "물 마시기", "물 마시기"].forEach {
             let subRoutineTitleLabel = UILabel()
             subRoutineTitleLabel.text = "• \($0)"
             subRoutineTitleLabel.font = BitnagilFont(style: .body2, weight: .medium).font
             subRoutineTitleLabel.textColor = BitnagilColor.gray40
-            subRoutineTitleLabel.snp.makeConstraints { make in
-                make.height.equalTo(Layout.subRoutineLabelHeight)
-            }
-            subRoutineStackView.addArrangedSubview(subRoutineTitleLabel)
+            subRoutineStackView.addArrangedSubview(subRoutineTitleLabel)
+            subRoutineTitleLabel.snp.makeConstraints { make in
+                make.height.equalTo(Layout.subRoutineLabelHeight)
+            }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
["물 마시기", "물 마시기", "물 마시기"].forEach {
let subRoutineTitleLabel = UILabel()
subRoutineTitleLabel.text = "\($0)"
subRoutineTitleLabel.font = BitnagilFont(style: .body2, weight: .medium).font
subRoutineTitleLabel.textColor = BitnagilColor.gray40
subRoutineTitleLabel.snp.makeConstraints { make in
make.height.equalTo(Layout.subRoutineLabelHeight)
}
subRoutineStackView.addArrangedSubview(subRoutineTitleLabel)
}
// TODO(T3-167-followup): 실제 서버 v2 연동 시 바인딩된 세부 루틴 목록으로 교체
["물 마시기", "물 마시기", "물 마시기"].forEach {
let subRoutineTitleLabel = UILabel()
subRoutineTitleLabel.text = "\($0)"
subRoutineTitleLabel.font = BitnagilFont(style: .body2, weight: .medium).font
subRoutineTitleLabel.textColor = BitnagilColor.gray40
subRoutineStackView.addArrangedSubview(subRoutineTitleLabel)
subRoutineTitleLabel.snp.makeConstraints { make in
make.height.equalTo(Layout.subRoutineLabelHeight)
}
}
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/Component/RoutineCardView.swift around
lines 81 to 90, each subRoutineTitleLabel currently has constraints applied
before being added to the UIStackView; move the constraint installation to after
you add the label to subRoutineStackView. Create the label, set its properties
(text should be a TODO placeholder comment instead of real dummy text), add it
with subRoutineStackView.addArrangedSubview(subRoutineTitleLabel), then call
subRoutineTitleLabel.snp.makeConstraints { make in
make.height.equalTo(Layout.subRoutineLabelHeight) } so constraints are applied
after insertion into the stack; also replace the duplicate hard-coded text with
a TODO marker to indicate future data binding.

}

private func configureLayout() {
[categoryIconView, titleLabel].forEach {
headerInfoStackView.addArrangedSubview($0)
}
addSubview(headerInfoStackView)
addSubview(plusButton)
addSubview(grayLine)
addSubview(subRoutineStackView)

categoryIconView.snp.makeConstraints { make in
make.size.equalTo(Layout.categoryIconSize)
}

headerInfoStackView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(Layout.headerInfoStackViewTopSpacing)
make.leading.equalToSuperview().offset(Layout.horizontalMargin)
}

plusButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(Layout.plusButtonTopSpacing)
make.trailing.equalToSuperview().inset(Layout.plusButtonTrailingSpacing)
make.size.equalTo(Layout.plusButtonSize)
}

grayLine.snp.makeConstraints { make in
make.top.equalTo(headerInfoStackView.snp.bottom).offset(Layout.grayLineTopSpacing)
make.leading.equalToSuperview().offset(Layout.horizontalMargin)
make.trailing.equalToSuperview().inset(Layout.horizontalMargin)
make.height.equalTo(Layout.grayLineHeight)
}

subRoutineStackView.snp.makeConstraints { make in
make.top.equalTo(grayLine.snp.bottom).offset(Layout.subRoutineStackViewTopSpacing)
make.leading.equalToSuperview().offset(Layout.horizontalMargin)
make.trailing.equalToSuperview().inset(Layout.horizontalMargin)
make.bottom.equalToSuperview().inset(Layout.subRoutineStackViewBottomSpacing)
}
}
}

fileprivate class RoutineCategoryIcon: UIView {
private enum Layout {
static let cornerRadius: CGFloat = 3.76
static let iconSize: CGFloat = 24
}

private let routineCategoryIcon = UIImageView()
private let routineCategory: RoutineCategoryType
init(routineCategory: RoutineCategoryType) {
self.routineCategory = routineCategory
super.init(frame: .zero)
configureAttribute()
configureLayout()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func configureAttribute() {
layer.masksToBounds = true
layer.cornerRadius = Layout.cornerRadius
backgroundColor = routineCategory.iconBackgroundColor ?? BitnagilColor.yellow10
routineCategoryIcon.image = routineCategory.iconImage ?? BitnagilIcon.shineIcon
}

private func configureLayout() {
addSubview(routineCategoryIcon)
routineCategoryIcon.snp.makeConstraints { make in
make.size.equalTo(Layout.iconSize)
make.center.equalToSuperview()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by 최정인 on 7/24/25.
//

import SnapKit
import UIKit

final class SelectableItemCell: UITableViewCell {
Expand All @@ -28,7 +29,7 @@ final class SelectableItemCell: UITableViewCell {

private func configureAttribute() {
titleLabel.font = BitnagilFont(style: .body1, weight: .regular).font
titleLabel.textColor = .black
titleLabel.textColor = BitnagilColor.gray10

let checkImage = BitnagilIcon.checkIcon?
.resizeAspectFit(to: CGSize(width: Layout.checkIconSize, height: Layout.checkIconSize))?
Expand All @@ -54,7 +55,24 @@ final class SelectableItemCell: UITableViewCell {
}

func configureCell(item: SelectableItem, isSelected: Bool) {
titleLabel.text = item.title
checkIcon.isHidden = !isSelected

guard let displayName = item.displayName else {
titleLabel.text = item.description
return
}
let attributedString = NSMutableAttributedString(string: item.description)
attributedString.addAttribute(
.font,
value: BitnagilFont(style: .body1, weight: .regular).font,
range: NSRange(location: 0, length: item.description.count))

if let range = item.description.range(of: displayName) {
let nsRange = NSRange(range, in: item.description)
attributedString.addAttributes([
.font: BitnagilFont(style: .body1, weight: .semiBold).font
], range: nsRange)
}
titleLabel.attributedText = attributedString
}
Comment on lines +60 to 77

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

String.count로 NSRange 구성 → 크래시 가능성, 그리고 타이포그래피 속성 누락

  • NSRange(location: 0, length: item.description.count)는 Swift String(문자 수)과 NSAttributedString(UTF-16 코드 유닛 수) 차이로 인해 이모지/조합형 문자 포함 시 크래시가 발생할 수 있습니다.
  • 또한 기본 폰트만 지정되어 BitnagilFont가 제공하는 lineHeight/letterSpacing(kerning) 등 타이포그래피 속성이 반영되지 않습니다. 제공된 BitnagilFont(style:weight:).attributedString(text:)를 활용하면 설계된 토큰을 일관되게 적용할 수 있습니다.

아래와 같이 수정하면 안전한 범위 계산과 디자인 토큰 일관 적용을 동시에 해결할 수 있습니다.

-        guard let displayName = item.displayName else {
-            titleLabel.text = item.description
-            return
-        }
-        let attributedString = NSMutableAttributedString(string: item.description)
-        attributedString.addAttribute(
-            .font,
-            value: BitnagilFont(style: .body1, weight: .regular).font,
-            range: NSRange(location: 0, length: item.description.count))
-
-        if let range = item.description.range(of: displayName) {
-            let nsRange = NSRange(range, in: item.description)
-            attributedString.addAttributes([
-                .font: BitnagilFont(style: .body1, weight: .semiBold).font
-            ], range: nsRange)
-        }
-        titleLabel.attributedText = attributedString
+        // 기본 타이포그래피(라인하이트/커닝 포함)를 BitnagilFont 토큰으로 구성
+        let baseFont = BitnagilFont(style: .body1, weight: .regular)
+        let attributedString = NSMutableAttributedString(
+            attributedString: baseFont.attributedString(text: item.description)
+        )
+
+        // displayName이 존재하면 해당 구간만 강조(세미볼드)
+        if let displayName = item.displayName,
+           let range = item.description.range(of: displayName) {
+            let nsRange = NSRange(range, in: item.description) // UTF-16 안전 변환
+            attributedString.addAttributes([
+                .font: BitnagilFont(style: .body1, weight: .semiBold).font
+            ], range: nsRange)
+        }
+        titleLabel.attributedText = attributedString
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
guard let displayName = item.displayName else {
titleLabel.text = item.description
return
}
let attributedString = NSMutableAttributedString(string: item.description)
attributedString.addAttribute(
.font,
value: BitnagilFont(style: .body1, weight: .regular).font,
range: NSRange(location: 0, length: item.description.count))
if let range = item.description.range(of: displayName) {
let nsRange = NSRange(range, in: item.description)
attributedString.addAttributes([
.font: BitnagilFont(style: .body1, weight: .semiBold).font
], range: nsRange)
}
titleLabel.attributedText = attributedString
}
// 기본 타이포그래피(라인하이트/커닝 포함)를 BitnagilFont 토큰으로 구성
let baseFont = BitnagilFont(style: .body1, weight: .regular)
let attributedString = NSMutableAttributedString(
attributedString: baseFont.attributedString(text: item.description)
)
// displayName이 존재하면 해당 구간만 강조(세미볼드)
if let displayName = item.displayName,
let range = item.description.range(of: displayName) {
let nsRange = NSRange(range, in: item.description) // UTF-16 안전 변환
attributedString.addAttributes([
.font: BitnagilFont(style: .body1, weight: .semiBold).font
], range: nsRange)
}
titleLabel.attributedText = attributedString
}
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift
around lines 60 to 77, the code builds an NSRange using item.description.count
(unsafe for extended grapheme clusters) and constructs the attributed string by
manually setting only the font which misses BitnagilFont's typography tokens;
fix by creating the base attributed string with BitnagilFont(style: .body1,
weight: .regular).attributedString(text: item.description) to inherit
lineHeight/kerning, compute the highlight range using NSString (let nsString =
item.description as NSString; let nsRange = nsString.range(of: displayName)) so
ranges are UTF-16 safe, and then apply BitnagilFont(style: .body1, weight:
.semiBold).font (or its attributed variant) to that nsRange before assigning to
titleLabel.attributedText.

}
Loading
Loading