[웹개발자의 IOS 탐방기] 7. UserDefaults를 사용한 로컬기기에 알림 권한설정 값 저장하기

서론

UserDefaults는 간단한 데이터, 설정, 환경설정 등을 로컬에 저장할 때 사용되는 인터페이스다. UserDefaults에 저장하는 데이터는 키-값(key-value) 쌍으로 관리된다.

예를 들어, 여러 곳에서 사용되는 UserDefaults의 키를 하드코딩하면 오타가 발생할 위험이 있고, 키 관리가 어려워질 수 있기 때문에 이를 방지하기 위해 UserDefaultsKeys를 사용하면 각 키에 대한 참조를 한 곳에서 관리할 수 있으며, 이후 코드에서는 이러한 참조를 재사용함으로써 안전성과 유지보수성을 높일 수 있다.

본 포스팅에서는 UserDefaultsKeys를 사용하여 디바이스 로컬 기기에 설정한 값들을 저장 및 불러와서 기계가 꺼져도 설정 내용이 변하지 않게 SettingView.Swfit 파일에 구현할 것이다.

UserDefaultsKeys.swift 파일 생성

UserDefaultsKeys와 같은 상수 집합은 앱 전반에 걸쳐 사용되기 때문에 접근성이 중요하다. 일반적으로 이러한 키를 관리하는 가장 좋은 방법은 관련 상수만을 포함하는 별도의 Swift 파일을 만드는 것인데, 나는 이 파일을 "UserDefaultsKeys.swift"와 같이 명명할 것이다.

이 파일은 프로젝트의 다른 코드에서 쉽게 참조할 수 있어야 한다. 프로젝트의 "Utilities"나 "Helpers" 폴더 내에 위치시키는 것이 좋다. 그러나 프로젝트의 구조와 팀의 선호도에 따라 다르게 배치할 수도 있다.

UserDefaultsKeys.swift 파일을 만든 후에는, 앱 내 어디서나 UserDefaultsKeys 구조체를 가져와서 (import) 사용할 수 있다. 이렇게 하면 UserDefaults를 사용하는 모든 코드에서 일관성을 유지하고, 키 관리를 훨씬 쉽게 할 수 있다.

//
//  UserDefaultsKeys.swift
//  webApp
//
//  Created by mingyukim on 11/7/23.
//

import Foundation

struct UserDefaultsKeys {
    static let isToggledCamera = "isToggledCamera"
    static let isToggledAlarm = "isToggledAlarm"
}

NotificationView 코드 수정

기존에 만들었었던 SettingView.swift 파일 내부에 있는 알림 설정 관련 구조체인 NotificationView 코드에 알림 설정 관련 코드를 추가한다. "알림 설정"이 되어 있으면 설정 화면 진입시, 알림 설정 토글이 on 되어 나오게 만들고 토글을 off하였을때는 알림 설정 권한을 끄게 만들고 싶었다.

하지만 NotificationManager 객체를 사용하여 알림 설정을 끄는 로직은 iOS 시스템 설정 내에서만 가능하다. 앱 내부에서 직접적으로 사용자의 알림 설정(시스템 설정에 있는 알림 권한)을 변경하는 것은 iOS의 보안 정책에 의해 허용되지 않기 때문이다.

다만, 사용자가 토글을 끌 때 알림 설정 화면으로 안내하거나, 특정 알림을 더 이상 보내지 않도록 구성하는 방법을 제공할 수 있다. 사용자가 토글을 끄면 알림을 받지 않겠다는 의사를 표현하는 것이므로, NotificationManager 내부에서 해당 정보를 참고하여 실제로 알림을 보내지 않도록 해야 한다.

시스템 알림 설정으로 사용자를 안내하는 것이 일반적인 접근 방식이고, 이를 위해 사용자가 토글을 끌 때 설정 앱의 알림 설정 화면으로 안내하는 방법은 아래와 같다.

struct NotificationView: View {
    // 알림 관련
    @ObservedObject var notificationManager = NotificationManager()
    
    // 앱의 상태 변화를 감지하기 위한 NotificationCenter
    private let notificationCenter = NotificationCenter.default
    
    // 토글 버튼 관리용 변수
    @State private var isToggledCamera = false
    @State private var isToggledAlarm = false
    // ...

    var body: some View {
        // ...
        HStack {
            Text("알림 권한 설정")
            Toggle(isOn: $isToggledAlarm) {}
            .onChange(of: isToggledAlarm) { newValue in
                UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.isToggledAlarm)
                if !newValue {
                    // 토글이 꺼졌을 때 필요한 추가 작업을 수행한다.
                    // 예: `NotificationManager` 내부 로직을 통해 알림을 비활성화 한다.
                }
            }
        }
        // ...
    }
    
    // ...
}

'onChange(of:perform:)' was deprecated in iOS 17.0

하지만 iOS 17에서 Apple은 onChange(of:perform) 뷰 수정자를 더 이상 사용하지 않고 두 가지 새로운 변형으로 대체했다. 위처럼 .onChange(of: isToggledAlarm)을 사용했을 때, newValue 만 넣는다면 아래 경고문구가 발생한다.

'onChange(of:perform:)' was deprecated in iOS 17.0: Use onChange with a two or zero parameter action closure instead.

이를 해결하기 위해서는 IOS 17.0 버전에서 지원하는 추적 이전값과 이후값 둘 다 사용하는 것이 좋다. 아래 코드는 Toggle의 old값과 new값을 가져와 off시켰을 때 IOS 설정 앱을 열어 설정을 취소하게 도와준다.

HStack {
    Text("알림 권한 설정")
    Toggle(isOn: $isToggledAlarm) {}
        .onChange(of: isToggledAlarm, initial: true) { oldValue,newValue in
            
            if !newValue && oldValue {
                // 토글을 끌 때 설정으로 안내
                if let url = URL(string: UIApplication.openSettingsURLString),
                   UIApplication.shared.canOpenURL(url) {
                    // iOS 설정 앱을 열기
                    UIApplication.shared.open(url, options: [:], completionHandler: nil)
                }
            }
            
            UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.isToggledAlarm)
        }
}

IOS 17 이상 버전과 16 이하에서 동작하는 onChange가 다르기 때문에 #available 을 사용하여 코드를 분기처리 해준다. 코드가 상당히 지저분해지는데, 이는 추후 IOS 플랫폼 버전별 코드 분리 포스팅을 통해 수정해보도록 하겠다.

if #available(iOS 17, *) {
  Toggle(isOn: $isToggledAlarm) {}
      .onChange(of: isToggledAlarm, initial: true) { oldValue,newValue in
          
          if !newValue && oldValue {
              // 토글을 끌 때 설정으로 안내
              if let url = URL(string: UIApplication.openSettingsURLString),
                 UIApplication.shared.canOpenURL(url) {
                  // iOS 설정 앱을 열기
                  UIApplication.shared.open(url, options: [:], completionHandler: nil)
              }
          }
          
          UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.isToggledAlarm)
      }
} else {
  Toggle(isOn: $isToggledAlarm) {}
      .onChange(of: isToggledAlarm) { newValue in
          // 토글이 꺼졌고, 이전에는 켜져 있었을 때 설정창으로 이동
          if !newValue && previousIsToggledAlarm {
              if let url = URL(string: UIApplication.openSettingsURLString),
                 UIApplication.shared.canOpenURL(url) {
                  UIApplication.shared.open(url)
              }
          }
          // 변경 사항을 UserDefaults에 저장
          UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.isToggledAlarm)
          // 변경 전 상태를 업데이트
          previousIsToggledAlarm = newValue
      }
}

결과물