Skip to content
Closed
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
Expand Up @@ -1519,3 +1519,30 @@ Find the villagers you have visited and tap the home icon on the villager's page
"api_error_invalid_data" = "Invalid data.";
"api_error_invalid_url" = "Invalid URL.\nURL: %@";
"api_error_parsing" = "Unknown error occurred while parsing JSON.";


// MARK: - Accessibility
"checked" = "Checked";
"unchecked" = "Unchecked";
"double_tap_to_toggle_check" = "Double tap to toggle check status";
"item_checked" = "Item checked";
"item_unchecked" = "Item unchecked";
"acquired" = "Acquired";
"not_acquired" = "Not acquired";
"double_tap_to_toggle_acquisition" = "Double tap to toggle acquisition status";
"item_acquired" = "Item acquired";
"item_not_acquired" = "Item not acquired";
// "liked" key is already defined above (as "Liked")
"resident" = "Resident";
"double_tap_for_details" = "Double tap for details";
"added_to_favorites" = "Added to favorites";
"removed_from_favorites" = "Removed from favorites";
"added_to_residents" = "Added to residents";
"removed_from_residents" = "Removed from residents";
"add_to_favorites" = "Add to favorites";
"remove_from_favorites" = "Remove from favorites";
"add_to_residents" = "Add to residents";
"remove_from_residents" = "Remove from residents";
"toggle_acquisition" = "Toggle acquisition";
"add_acquisition" = "Mark as acquired";
"remove_acquisition" = "Remove from acquired";
Original file line number Diff line number Diff line change
Expand Up @@ -1524,3 +1524,30 @@
"api_error_invalid_data" = "데이터가 유효하지 않습니다.";
"api_error_invalid_url" = "URL이 잘못되었습니다.\nURL: %@";
"api_error_parsing" = "JSON으로 파싱하는 도중 알 수 없는 오류가 발생했습니다.";


// MARK: - Accessibility
"checked" = "체크됨";
"unchecked" = "체크 안됨";
"double_tap_to_toggle_check" = "두 번 탭하여 체크 상태 전환";
"item_checked" = "아이템 체크됨";
"item_unchecked" = "아이템 체크 해제됨";
"acquired" = "보유 중";
"not_acquired" = "미보유";
"double_tap_to_toggle_acquisition" = "두 번 탭하여 보유 상태 전환";
"item_acquired" = "아이템 보유 등록됨";
"item_not_acquired" = "아이템 보유 해제됨";
// "liked" key is already defined above (as "Liked")
"resident" = "거주 중";
"double_tap_for_details" = "두 번 탭하여 상세 정보 보기";
"added_to_favorites" = "좋아요 추가됨";
"removed_from_favorites" = "좋아요 해제됨";
"added_to_residents" = "거주자로 등록됨";
"removed_from_residents" = "거주자에서 해제됨";
"add_to_favorites" = "좋아요 추가";
"remove_from_favorites" = "좋아요 해제";
"add_to_residents" = "거주자로 등록";
"remove_from_residents" = "거주자에서 해제";
"toggle_acquisition" = "보유 상태 전환";
"add_acquisition" = "보유 등록";
"remove_acquisition" = "보유 해제";
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import RxCocoa
final class VillagersCell: UICollectionViewCell {

private var disposeBag = DisposeBag()
private var characterName: String?
private var isLiked: Bool = false
private var isResident: Bool = false
private var isNPC: Bool = false
private var hasReceivedInitialLikeState: Bool = false
private var hasReceivedInitialResidentState: Bool = false

@IBOutlet weak var iconImage: UIImageView!
@IBOutlet weak var nameLabel: UILabel!
Expand All @@ -23,10 +29,66 @@ final class VillagersCell: UICollectionViewCell {
contentView.backgroundColor = .acSecondaryBackground
contentView.layer.cornerRadius = 14
nameLabel.font = .preferredFont(for: .footnote, weight: .semibold)
nameLabel.adjustsFontForContentSizeCategory = true
likeButton.setImage(UIImage(systemName: "heart"), for: .normal)
likeButton.setTitle(nil, for: .normal)
houseButton.setImage(UIImage(systemName: "house"), for: .normal)
houseButton.setTitle(nil, for: .normal)
setupAccessibility()
}

private func setupAccessibility() {
isAccessibilityElement = true
accessibilityTraits = .button
likeButton.isAccessibilityElement = false
houseButton.isAccessibilityElement = false
updateAccessibilityCustomActions()
}

private func updateAccessibilityCustomActions() {
var actions: [UIAccessibilityCustomAction] = []

// 좋아요 토글 액션
let likeActionName = isLiked ? "remove_from_favorites".localized : "add_to_favorites".localized
let likeAction = UIAccessibilityCustomAction(name: likeActionName) { [weak self] _ in
self?.likeButton.sendActions(for: .touchUpInside)
return true
}
actions.append(likeAction)

// 거주자 토글 액션 (NPC가 아닌 경우에만)
if !isNPC {
let residentActionName = isResident ? "remove_from_residents".localized : "add_to_residents".localized
let residentAction = UIAccessibilityCustomAction(name: residentActionName) { [weak self] _ in
self?.houseButton.sendActions(for: .touchUpInside)
return true
}
actions.append(residentAction)
}

accessibilityCustomActions = actions
}

private func updateAccessibilityLabel() {
guard let name = characterName else {
accessibilityLabel = nil
accessibilityHint = nil
return
}

var statusParts: [String] = []

if isLiked {
statusParts.append("liked".localized)
}

if !isNPC && isResident {
statusParts.append("resident".localized)
}

let status = statusParts.isEmpty ? "" : ", " + statusParts.joined(separator: ", ")
accessibilityLabel = "\(name)\(status)"
accessibilityHint = "double_tap_for_details".localized
}

override func prepareForReuse() {
Expand All @@ -37,19 +99,34 @@ final class VillagersCell: UICollectionViewCell {
houseButton.setImage(nil, for: .normal)
houseButton.setTitle(nil, for: .normal)
disposeBag = DisposeBag()
characterName = nil
isLiked = false
isResident = false
isNPC = false
hasReceivedInitialLikeState = false
hasReceivedInitialResidentState = false
updateAccessibilityLabel()
}

func setUp(_ npc: NPC) {
iconImage.setImage(with: npc.iconImage)
nameLabel.text = npc.translations.localizedName()
let localizedName = npc.translations.localizedName()
nameLabel.text = localizedName
characterName = localizedName
isNPC = true
updateAccessibilityLabel()
bind(reactor: NPCCellReactor(npc: npc))
houseButton.isHidden = true
likeButton.isHidden = false
}

func setUp(_ villager: Villager) {
iconImage.setImage(with: villager.iconImage)
nameLabel.text = villager.translations.localizedName()
let localizedName = villager.translations.localizedName()
nameLabel.text = localizedName
characterName = localizedName
isNPC = false
updateAccessibilityLabel()
bind(reactor: VillagersCellReactor(villager: villager))
houseButton.isHidden = false
likeButton.isHidden = false
Expand All @@ -75,18 +152,50 @@ final class VillagersCell: UICollectionViewCell {
reactor.state.map { $0.isLiked }
.compactMap { $0 }
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] isLiked in
self?.likeButton.setImage(UIImage(systemName: isLiked ? "heart.fill" : "heart"), for: .normal)
.subscribe(onNext: { [weak self] isLiked in
guard let self = self else { return }
let previousState = self.isLiked
let isInitialState = !self.hasReceivedInitialLikeState
self.hasReceivedInitialLikeState = true
self.isLiked = isLiked
self.updateAccessibilityLabel()
self.updateAccessibilityCustomActions()
self.likeButton.setImage(UIImage(systemName: isLiked ? "heart.fill" : "heart"), for: .normal)

// 좋아요 상태 변경 시 접근성 알림 (초기 로딩 시에는 알림하지 않음)
if !isInitialState && previousState != isLiked {
self.announceAccessibilityChange(
isAdded: isLiked,
addedKey: "added_to_favorites",
removedKey: "removed_from_favorites"
)
}
}).disposed(by: disposeBag)

reactor.state.map { $0.isResident }
.compactMap { $0 }
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] isResident in
self?.houseButton.setImage(UIImage(systemName: isResident ? "house.fill" : "house"), for: .normal)
.subscribe(onNext: { [weak self] isResident in
guard let self = self else { return }
let previousState = self.isResident
let isInitialState = !self.hasReceivedInitialResidentState
self.hasReceivedInitialResidentState = true
self.isResident = isResident
self.updateAccessibilityLabel()
self.updateAccessibilityCustomActions()
self.houseButton.setImage(UIImage(systemName: isResident ? "house.fill" : "house"), for: .normal)

// 거주 상태 변경 시 접근성 알림 (초기 로딩 시에는 알림하지 않음)
if !isInitialState && previousState != isResident {
self.announceAccessibilityChange(
isAdded: isResident,
addedKey: "added_to_residents",
removedKey: "removed_from_residents"
)
}
}).disposed(by: disposeBag)
}

private func bind(reactor: NPCCellReactor) {
Observable.just(NPCCellReactor.Action.fetch)
.bind(to: reactor.action)
Expand All @@ -101,9 +210,31 @@ final class VillagersCell: UICollectionViewCell {
reactor.state.map { $0.isLiked }
.compactMap { $0 }
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] isLiked in
self?.likeButton.setImage(UIImage(systemName: isLiked ? "heart.fill" : "heart"), for: .normal)
.subscribe(onNext: { [weak self] isLiked in
guard let self = self else { return }
let previousState = self.isLiked
let isInitialState = !self.hasReceivedInitialLikeState
self.hasReceivedInitialLikeState = true
self.isLiked = isLiked
self.updateAccessibilityLabel()
self.updateAccessibilityCustomActions()
self.likeButton.setImage(UIImage(systemName: isLiked ? "heart.fill" : "heart"), for: .normal)

// 좋아요 상태 변경 시 접근성 알림 (초기 로딩 시에는 알림하지 않음)
if !isInitialState && previousState != isLiked {
self.announceAccessibilityChange(
isAdded: isLiked,
addedKey: "added_to_favorites",
removedKey: "removed_from_favorites"
)
}
}).disposed(by: disposeBag)
}

// MARK: - Accessibility Helpers

private func announceAccessibilityChange(isAdded: Bool, addedKey: String, removedKey: String) {
let announcement = isAdded ? addedKey.localized : removedKey.localized
UIAccessibility.post(notification: .announcement, argument: announcement)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import RxSwift
final class CatalogCell: UICollectionViewCell {

private var disposeBag = DisposeBag()
private var itemName: String?
private var isItemAcquired: Bool = false
private var hasReceivedInitialAcquiredState: Bool = false

@IBOutlet private weak var backgroundStackView: UIStackView!
@IBOutlet private weak var iconImageView: UIImageView!
Expand All @@ -34,6 +37,24 @@ final class CatalogCell: UICollectionViewCell {
contentView.backgroundColor = .acSecondaryBackground
contentView.layer.cornerRadius = 14
nameLabel.font = .preferredFont(for: .footnote, weight: .bold)
nameLabel.adjustsFontForContentSizeCategory = true
setupAccessibility()
}

private func setupAccessibility() {
isAccessibilityElement = true
accessibilityTraits = .button
checkButton.isAccessibilityElement = false
updateAccessibilityCustomActions()
}

private func updateAccessibilityCustomActions() {
let actionName = isItemAcquired ? "remove_acquisition".localized : "add_acquisition".localized
let toggleAction = UIAccessibilityCustomAction(name: actionName) { [weak self] _ in
self?.checkButton.sendActions(for: .touchUpInside)
return true
}
accessibilityCustomActions = [toggleAction]
}

override func prepareForReuse() {
Expand All @@ -51,6 +72,21 @@ final class CatalogCell: UICollectionViewCell {
),
for: .normal
)
itemName = nil
isItemAcquired = false
hasReceivedInitialAcquiredState = false
updateAccessibilityLabel()
}

private func updateAccessibilityLabel() {
guard let name = itemName else {
accessibilityLabel = nil
accessibilityHint = nil
return
}
let acquiredStatus = isItemAcquired ? "acquired".localized : "not_acquired".localized
accessibilityLabel = "\(name), \(acquiredStatus)"
accessibilityHint = "double_tap_to_toggle_acquisition".localized
}

private func configure() {
Expand Down Expand Up @@ -84,14 +120,28 @@ final class CatalogCell: UICollectionViewCell {
.compactMap { $0 }
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] isAcquired in
guard let self = self else { return }
let previousState = self.isItemAcquired
let isInitialState = !self.hasReceivedInitialAcquiredState
self.hasReceivedInitialAcquiredState = true
self.isItemAcquired = isAcquired
self.updateAccessibilityLabel()
self.updateAccessibilityCustomActions()

let config = UIImage.SymbolConfiguration(font: .preferredFont(forTextStyle: .title2))
self?.checkButton.setImage(
self.checkButton.setImage(
UIImage(
systemName: isAcquired ? "checkmark.seal.fill" : "checkmark.seal",
withConfiguration: config
),
for: .normal
)

// 체크 상태 변경 시 접근성 알림 (초기 로딩 시에는 알림하지 않음)
if !isInitialState && previousState != isAcquired {
let announcement = isAcquired ? "item_acquired".localized : "item_not_acquired".localized
UIAccessibility.post(notification: .announcement, argument: announcement)
}
}).disposed(by: disposeBag)
}
}
Expand All @@ -100,7 +150,10 @@ extension CatalogCell {

func setUp(_ item: Item) {
setUpIconImage(item)
nameLabel.text = item.translations.localizedName()
let localizedName = item.translations.localizedName()
nameLabel.text = localizedName
itemName = localizedName
updateAccessibilityLabel()
bind(reactor: CatalogCellReactor(item: item, category: item.category, state: .init(item: item, category: item.category)))
var priceView: ItemBellsView
switch item.category {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ final class NpcsView: UIView {
) { _, npcInfo, cell in
let (npc, isChecked) = npcInfo
cell.setImage(url: npc.iconImage)
cell.setAccessibilityInfo(name: npc.translations.localizedName(), isChecked: isChecked)
cell.setChecked(isChecked)
}.disposed(by: disposeBag)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,10 @@ final class TodaysTasksView: UIView {
cellType: IconCell.self
)
) { _, item, cell in
let isCompleted = item.task.progressList[item.progressIndex]
cell.setImage(icon: item.task.icon)
item.task.progressList[item.progressIndex] ? cell.setAlpha(1) : cell.setAlpha(0.5)
cell.setAccessibilityInfo(name: item.task.name.localized, isChecked: isCompleted)
isCompleted ? cell.setAlpha(1) : cell.setAlpha(0.5)
}.disposed(by: disposeBag)

reactor.state.map { $0.tasks }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ final class VillagersView: UIView {
) { _, villagerInfo, cell in
let (villager, isChecked) = villagerInfo
cell.setImage(url: villager.iconImage)
cell.setAccessibilityInfo(name: villager.translations.localizedName(), isChecked: isChecked)
cell.setChecked(isChecked)
}.disposed(by: disposeBag)

Expand Down
Loading