오늘은 UIMenu
와 UserDefaults
를 이용해 온도 단위(섭씨/화씨) 설정 기능을 구현한 과정을 기록하려한다.
이 기능은 사용자 경험을 개선하기 위해 사용자가 선택한 온도 단위를 앱 전반에 동기화하고 유지하도록 구현을 했다.
아래 사진처럼 아이폰의 날씨 앱에서 설정버튼을 만들기 위함인데,
UIMenu
를 활용해 설정 버튼을 누르면 온도 단위를 선택할 수 있는 메뉴를 표시한 뒤,
선택한 온도 단위를 UserDefaults
에 저장해서 앱을 종료하고 다시 실행해도 설정이 유지되도록 하는 것.
그리고 선택한 단위에 따라 메뉴의 체크 표시가 즉시 업데이트되도록 구현하고 향후에 화면 전반의 온도 표기 변경을 위해 API와 컨트롤러 사이에 바인딩 로직을 추가할 계획이다.
UIMenu
로 온도 단위 선택 메뉴 생성UIMenu
를 사용하면 iOS에서 버튼을 클릭했을 때 옵션 메뉴를 간편하게 생성할 수 있었다.
https://ios-development.tistory.com/735 해당 블로그에서 어떤식으로 사용이 가능한지 파악을 해보고
온도 단위(섭씨/화씨)를 선택하는 메뉴만 간단하게 구현해보기 위해 MenuHelper
라는 헬퍼 구조체를 작성했다.
import UIKit
struct MenuHelper {
static func createSettingsMenu() -> UIMenu {
let celsiusAction = UIAction(
title: "섭씨",
image: UIImage(systemName: "degreesign.celsius"),
state: SettingsManager.getTempUnit() == .celsius ? .on : .off
) { _ in
SettingsManager.saveTempUnit(.celsius)
NotificationCenter.default.post(name: .tempUnitChanged, object: nil)
}
let fahrenheitAction = UIAction(
title: "화씨",
image: UIImage(systemName: "degreesign.fahrenheit"),
state: SettingsManager.getTempUnit() == .fahrenheit ? .on : .off
) { _ in
SettingsManager.saveTempUnit(.fahrenheit)
NotificationCenter.default.post(name: .tempUnitChanged, object: nil)
}
return UIMenu(
title: "온도 설정",
options: .displayInline,
children: [celsiusAction, fahrenheitAction]
)
}
}
UIAction
각각 섭씨와 화씨 선택 항목을 생성하고 선택된 상태에 따라 .on
또는 .off
로 설정해서 체크 표시를 관리했다. 삼항 연산자로 간단하게 구현할 수 있었다.
NotificationCenter
선택한 값이 저장된 후에 NotificationCenter
를 통해서 변경 사항을 앱 전반에 전파해준다.
UIMenu
섭씨와 화씨 메뉴 항목을 포함한 메뉴를 생성했다.
UserDefaults
와 SettingsManager
온도 단위 설정은 팀원들과의 회의에서 UserDefaults
를 이용해 저장하기로 했다.
유저 디폴츠로 간단하게 구현해도 앱을 재시작하게 될 때 사용자의 선택 값이 유지되니 굳이 코어데이터를 사용하지 않아도 될 것 같아서 코어데이터보다는 덜 머리아프게 구현했던 것 같다.
그리고 저장과 불러오는 작업은 SettingsManager
라는 구조체에서 관리하도록 구현했다.
import Foundation
enum TempUnit: String {
case celsius = "섭씨"
case fahrenheit = "화씨"
}
struct SettingsManager {
private static let tempUnitKey = "TemperatureUnit"
static func saveTempUnit(_ unit: TempUnit) {
UserDefaults.standard.set(unit.rawValue, forKey: tempUnitKey)
}
static func getTempUnit() -> TempUnit {
let value = UserDefaults.standard.string(forKey: tempUnitKey) ?? TempUnit.celsius.rawValue
return TempUnit(rawValue: value) ?? .celsius
}
}
saveTempUnit
선택한 단위를 UserDefaults
에 저장한다.
getTempUnit
그리고 UserDefaults
에서 저장된 값을 불러오고, 만약 저장된 값이 없으면 기본값으로 섭씨를 반환시켰다.
메뉴의 체크 표시 상태가 즉시 반영될 수 있게 NotificationCenter
를 활용해서 온도 단위가 변경될 때 설정 버튼의 메뉴를 다시 생성하고 업데이트를 시켜줬다.
private func setupNotificationObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(updateMenu), name: .tempUnitChanged, object: nil)
}
@objc private func updateMenu() {
if let settingsButton = navigationItem.leftBarButtonItem {
settingsButton.menu = MenuHelper.createSettingsMenu()
}
}
setupNotificationObservers
tempUnitChanged
알림을 감지해서 메뉴를 업데이트하는 옵저버를 등록했다.
updateMenu
변경된 상태를 반영한 새로운 메뉴를 생성해 설정 버튼에 다시 할당해준다.
UIMenu
와 UserDefaults
를 활욯해서 온도 단위 설정이 앱 전반에서 동기화되고 유지되도록 구현만 했다.
선택한 단위는 앱 종료 후에도 유지가 되고 메뉴의 체크 표시로 현재 상태를 바로바로 직관적으로 확인할 수 있다.
그러니까 지금은 UI와 설정 동기화까지만 구현된 상태인 거고 다음단계에서는 API와 컨트롤러 사이의 바인딩 로직을 추가해서
앱 전체 화면에서 섭씨와 화씨 단위로 온도 데이터를 변환 및 표기하는 기능을 넣어볼 계획이다.
이번 구현을 하면서 UIMenu
와 UserDefaults
를 이용한 설정 관리의 기초를 다시 다져보는 시간이었다.
사용자가 선택한 설정을 앱 전반에 적용하고 유지할 수 있는 기능은 사용자 경험을 개선한다고 하니
마지막까지 신경을 잘 써서 추후 API 바인딩까지 연결되면 완성된 기능으로 한번 더 글을 써봐야겠다.
혹시라도 더 나은 방법이나 개선 사항이 있다면 댓글로 공유해주세요 선배님들
오.. 저 메뉴 화면 UIMenu랑 UIAction으로 구현하는 거였군여! 링크 블로그에서 설명한 것처럼 코드가 뭔가 Alert이랑 비슷해 보이네용