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
18 changes: 6 additions & 12 deletions .github/workflows/build_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@ jobs:
- name: Set Xcode version
run: sudo xcode-select -s /Applications/Xcode_16.4.app

- name: Install Mise and Tuist
run: |
curl -fsSL https://mise.jdx.dev/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
mise install tuist
mise use -g tuist
- name: Install Tuist
run: brew install tuist

- name: Generate xcconfig
run: |
Expand All @@ -32,9 +28,8 @@ jobs:

- name: Generate Xcode project with Tuist
run: |
export PATH="$HOME/.local/bin:$PATH"
mise exec -- tuist install
mise exec -- tuist generate --no-open
tuist install
tuist generate --no-open
Comment on lines +31 to +32
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, examine the workflow file
cat -n .github/workflows/build_test.yml | head -50

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 1476


🏁 Script executed:

# Search for Tuist-related configuration files
fd -e lock -e lockfile -e "tuist" 2>/dev/null | head -20

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 50


🏁 Script executed:

# Check for any Tuist configuration files
find . -name ".tuist*" -o -name "Tuist.json" -o -name "tuist.lock" 2>/dev/null

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 50


🌐 Web query:

Tuist lockfile deterministic dependency resolution CI reproducibility

💡 Result:

Tuist’s dependency “lockfile” story is essentially SwiftPM’s Package.resolved:

  • What locks versions (incl. transitives): Tuist resolves dependencies via Swift Package Manager and relies on Package.resolved to pin the exact versions (including transitive dependencies). Ensure Package.resolved is committed (not gitignored), otherwise CI/dev machines may pick up newer transitive releases and break builds. [1]
  • How to make CI deterministic: On CI, run:
    • tuist install --force-resolved-versions
      This forces resolution to the exact versions in Package.resolved to avoid non-determinism in resolution. [2]
  • When versions change: tuist install effectively runs swift package resolve (generally respecting Package.resolved), while tuist install --update runs swift package update and can move versions forward within the ranges in Package.swift. [3]
  • CI reproducibility + caching: If you cache dependencies, Tuist recommends caching the SwiftPM clone/build directory (often .build) and keying it off the Package.resolved path used by Xcode/SwiftPM; you can also pass -clonedSourcePackagesDirPath .build via Tuist config / xcodebuild to keep clones in a predictable location for caching. [4]

Practical checklist for reproducible CI

  1. Commit Package.resolved.
  2. Use tuist install --force-resolved-versions in CI.
  3. Only run updates intentionally (e.g., tuist install --update in a dedicated “deps update” workflow/PR).
  4. Cache .build (or your configured clone dir) keyed by Package.resolved. [1][2][4]

Sources: Tuist best practices (CI flag) [2], Tuist community threads on transitive locking & install/update behavior [1][3], Tuist registry CI/caching guide [4].


🏁 Script executed:

# Check for Package.resolved file
fd "Package.resolved" 2>/dev/null

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 90


🏁 Script executed:

# Check for Package.swift to understand dependency management
fd "Package.swift" 2>/dev/null

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 87


tuist install--force-resolved-versions 플래그 추가 필요

리포지토리에 Tuist/Package.resolved 파일이 있으므로 의존성 버전이 잠금되어 있습니다. 다만, 현재 CI 워크플로우의 tuist install 명령어는 플래그 없이 실행되어 매번 해상도 과정을 거칠 수 있습니다.

CI 재현성을 보장하려면 다음과 같이 수정하세요:

tuist install --force-resolved-versions
tuist generate --no-open

이 플래그는 Package.resolved의 정확한 버전(이행 의존성 포함)을 강제하여 CI의 결정적 동작을 보장합니다.

🤖 Prompt for AI Agents
In @.github/workflows/build_test.yml around lines 33 - 34, The CI run calls
"tuist install" then "tuist generate --no-open" while the repo contains
Tuist/Package.resolved, so make "tuist install" deterministic by adding the
--force-resolved-versions flag; update the workflow to run "tuist install
--force-resolved-versions" followed by "tuist generate --no-open" to enforce the
exact versions in Tuist/Package.resolved (including transitive deps).


- name: Build with xcodebuild
run: |
Expand All @@ -43,6 +38,5 @@ jobs:
-workspace Bitnagil.xcworkspace \
-scheme App \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' \
clean build | xcpretty

-destination 'generic/platform=iOS Simulator' \
clean build
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ final class PrimaryButton: UIButton {
self.buttonState = buttonState
super.init(frame: .zero)
configureAttribute(buttonTitle: buttonTitle)
self.isEnabled = buttonState != .disabled
}

required init?(coder: NSCoder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ final class WithdrawViewController: BaseViewController<WithdrawViewModel> {
private let withdrawReasonView = UIView()
private let withdrawReasonLabel = UILabel()
private let withdrawReasonStackView = UIStackView()
private var withdrawButtons: [WithdrawReason: BitnagilChoiceButton] = [:]
private var withdrawReasonButtons: [WithdrawReason: BitnagilChoiceButton] = [:]
private let withdrawReasonTextBackgroundView = UIView()
private let withdrawReasonTextViewPlaceholder = UILabel()
private let withdrawReasonTextView = UITextView()
Expand All @@ -63,6 +63,11 @@ final class WithdrawViewController: BaseViewController<WithdrawViewModel> {
}
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
removeKeyboardNotification()
}
Comment on lines +66 to +69
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

키보드 알림 등록/해제 시점 불일치 가능성

configureKeyboardNotification()configureAttribute()에서 한 번만 호출되지만, removeKeyboardNotification()viewWillDisappear에서 매번 호출됩니다. 만약 이 화면이 navigation stack에서 다시 나타나는 경우, 키보드 알림이 재등록되지 않아 키보드 처리가 동작하지 않을 수 있습니다.

탈퇴 플로우 특성상 화면을 벗어났다가 다시 돌아오는 경우가 드물지만, 일관성을 위해 viewWillAppear에서 등록하고 viewWillDisappear에서 해제하는 패턴을 권장합니다.

제안하는 수정 방법
+ override func viewWillAppear(_ animated: Bool) {
+     super.viewWillAppear(animated)
+     configureKeyboardNotification()
+ }
+
  override func viewWillDisappear(_ animated: Bool) {
      super.viewWillDisappear(animated)
      removeKeyboardNotification()
  }

그리고 configureAttribute()에서 configureKeyboardNotification() 호출을 제거합니다.

Also applies to: 160-160, 311-323

🤖 Prompt for AI Agents
In `@Projects/Presentation/Sources/Withdraw/View/WithdrawViewController.swift`
around lines 66 - 69, viewWillDisappear에서만 removeKeyboardNotification()을 호출하고
configureKeyboardNotification()는 configureAttribute()에서 한 번만 호출되어 화면이 다시 나타날 때
키보드 알림이 등록되지 않을 수 있으니, 키보드 알림의 등록/해제를 일관된 생명주기로 옮기세요: configureAttribute()에서
configureKeyboardNotification() 호출을 제거하고 viewWillAppear(_: )에서
configureKeyboardNotification()를 등록하며 기존 viewWillDisappear(_:)의
removeKeyboardNotification()은 유지하여 알림 등록/해제 패턴을 일치시킵니다; 대상 함수/메서드:
configureKeyboardNotification(), removeKeyboardNotification(),
configureAttribute(), viewWillAppear(_:), viewWillDisappear(_:).


private func updateConstraint() {
let height = view.bounds.height
if height <= 667 {
Expand Down Expand Up @@ -121,7 +126,7 @@ final class WithdrawViewController: BaseViewController<WithdrawViewModel> {
make.height.equalTo(Layout.withdrawChoiceButtonHeight)
}
withdrawReasonStackView.addArrangedSubview(withdrawChoiceButton)
withdrawButtons[withdrawReason] = withdrawChoiceButton
withdrawReasonButtons[withdrawReason] = withdrawChoiceButton
}

withdrawReasonTextBackgroundView.backgroundColor = BitnagilColor.gray99
Expand All @@ -147,6 +152,12 @@ final class WithdrawViewController: BaseViewController<WithdrawViewModel> {
self?.viewModel.action(input: .withdrawService)
},
for: .touchUpInside)

let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
tapGesture.cancelsTouchesInView = false
view.addGestureRecognizer(tapGesture)

configureKeyboardNotification()
}

override func configureLayout() {
Expand Down Expand Up @@ -284,7 +295,7 @@ final class WithdrawViewController: BaseViewController<WithdrawViewModel> {
}

private func updateWithdrawReason(selectedWithdrawReason: WithdrawReason?) {
withdrawButtons.forEach { withdrawReason in
withdrawReasonButtons.forEach { withdrawReason in
let isSelected = withdrawReason.key == selectedWithdrawReason
withdrawReason.value.updateButtonState(isChecked: isSelected)
}
Expand All @@ -296,11 +307,74 @@ final class WithdrawViewController: BaseViewController<WithdrawViewModel> {
withdrawReasonMaxLengthLabel.isHidden = true
}
}

private func configureKeyboardNotification() {
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillAppear),
name: UIResponder.keyboardWillShowNotification,
object: nil)

NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillDisappear),
name: UIResponder.keyboardWillHideNotification,
object: nil)
}

private func removeKeyboardNotification() {
NotificationCenter.default.removeObserver(
self,
name: UIResponder.keyboardWillShowNotification,
object: nil)

NotificationCenter.default.removeObserver(
self,
name: UIResponder.keyboardWillHideNotification,
object: nil)
}

@objc private func dismissKeyboard() {
view.endEditing(true)
}

@objc private func keyboardWillAppear(_ sender: Notification) {
guard
let keyboardFrame = sender.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
let duration = sender.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
else { return }

let keyboardHeight = keyboardFrame.height
let buttonFrame = withdrawButton.convert(withdrawButton.bounds, to: view)
let buttonBottom = buttonFrame.maxY
let visibleHeight = view.frame.height - keyboardHeight

if buttonBottom > visibleHeight {
let offset = buttonBottom - visibleHeight + 50

UIView.animate(withDuration: duration) {
self.view.frame.origin.y = -offset
}
}
}

@objc private func keyboardWillDisappear(_ sender: Notification) {
guard let duration = sender.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
else { return }

UIView.animate(withDuration: duration) {
self.view.frame.origin.y = 0
}
}
}

extension WithdrawViewController: UITextViewDelegate {
func textViewDidBeginEditing(_ textView: UITextView) {
viewModel.action(input: .choiceWithdrawReason(reason: nil))

if !textView.text.isEmpty {
viewModel.action(input: .inputWithdrawReason(reason: textView.text))
}
}

func textViewDidChange(_ textView: UITextView) {
Expand Down