throws
키워드가 붙는다..!try?
: 에러가 발생하면 nil
을 반환try!
: 에러가 발생하면 프로그램이 터짐rethrow
: 에러를 던지는 다른 함수 안에서 사용 func foo() throws {
try sthThatThrows()
}
do {
try functionThatThrows()
} catch let error {
// handle the thrown error here!
}
catch
할 에러를 더 구체적으로 지정할 수도 있음FileManger.default
를 사용한다 (b/c thread-safe).urls(for: , in: )
메서드에서 반드시 .userDomainMask
옵션 사용Data
타입을 사용object(e.g. struct)
를 file system
에 저장할 수 있게 해준다..!
주의할 점은 enum
의 경우 associated data
가 있으면 별도의 처리가 필요
object
의 모든 변수가 Codable
해서 그 자체 또한 Codable
하다면 JSON
(혹은 다른 포맷도 가능)으로 인코딩해서 해당 struct
를 나타내는 Data blob(i.e. JSON representation of my object
으로 변환할 수 있다!
JSON
: 데이터를 나타내는 데 사용되는 표준 양식 codable
하게 해주려면 encoding
과 decoding
을 해줘야 함 Property List
만 저장할 수 있음 Property List
는 일종의 개념으로 String, Int, Bool, floating point, Date, Data, Array, Dictionary
의 조합을 의미Codable
프로토콜을 이용해 임의의 struct
를 Data
로 변환할 수 있으므로 사실상 무엇이든 저장할 수 있음file manager 객체
는 파일 시스템에 접근하고, 변경하는 것을 가능하게 한다. 예를 들어 파일/디렉토리에 접근해 정보를 기져오거나 파일/디렉토리를 생성, 복사, 이동, 변경할 수 있게 해준다! FileManager.default
를 사용하면 항상 동일한 shared file manager object
에 접근할 수 있다.EmojiArt 오브젝트
를 저장하기 위해서는 먼저 저장할 위치를 지정해줘야 하며, url
이 내부적으로 더 효율적이라 보통 url
을 사용한다고 한다! FileManager
로 접근할 수 있는 디렉토리는 한정되어 있는 데, 그 중 유저가 생성했으며, 유저에게 항상 보여지는 것들을 저장하려면 Document directory
에 저장해야 한다. FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
와 같이 선언해 iOS 파일 시스템 내부의 Document directory
의 주소를 얻을 수 있다.Document Directory
를 찾아 주소를 반환한다. URL 메서드
인 appendingPathComponent(_ pathComponent: String)
을 이용해 EmojiArt
를 저장할 디렉토리를 생성한다!class EmojiArtDocument: ObservableObject {
...
private struct Autosave {
static let filename = "Autosaved.emojiart"
static var url: URL? {
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
return documentDirectory?.appendingPathComponent(filename)
}
static let coalescingInterval = 5.0
}
...
}
Data 타입
을 이용해야 한다.EmojiArtModel
을 Data 타입
으로 바꿔주는 encoding
과정이 필요하기 때문에 업계 표준 형식인 JSON 데이터 형식
으로 바꿔줄 것이다!JSONEncoder 클래스
를 제공하는데, 인코딩하려는 객체가 Codable
프로토콜에 순응하기만 하면 JSONEncoder
의 encode 메서드
를 이용해 JSON 데이터
로 인코딩할 수 있다!Codable
에 순응해 단순히 덧붙여주기만 하면 된다는 것이다...! 몇 안되는 예외가 associated data
를 가진 enum
이었으나 강의 이후 불과 몇 개월만에 킹짱빛 스위프트에서 해당 케이스까지 바로 Codable
하도록 업데이트해줬다...!struct EmojiArtModel: Codable {
...
func json() throws -> Data {
return try JSONEncoder().encode(self)
}
...
}
ViewModel
에서 인코딩한 데이터를 아까 구한 파일 시스템 상의 주소에 저장해주면 저장 끝이다...! 이때 주소를 구하는 과정이나 인코딩 과정에서 에러를 던질 수 있으므로 do-catch
문으로 에러도 처리해주면 진짜 끝...let thisfunction = "\(String(describing: self)).\(#function)"
와 같이 선언하면 에러 메시지 출력 시 어디서 좀 더 편리하게 나타낼 수 있다...!class EmojiArtDocument: ObservableObject {
...
private func save(to url: URL) {
let thisfunction = "\(String(describing: self)).\(#function)"
do {
let data: Data = try emojiArt.json() // 인코딩
print("\(thisfunction) json = \(String(data: data, encoding: .utf8) ?? "nil")")
try data.write(to: url)
print("\(thisfunction) success!")
} catch let encodingError where encodingError is EncodingError {
print("\(thisfunction) couldn't encode EmojiArt as JSON because \(encodingError.localizedDescription)")
} catch {
print("\(thisfunction) error = \(error)")
}
...
}
}
Codable
에 자동으로 순응하지 않는 타입을 순응하게 하려면 encoding
과 decoding
과정을 다음과 같이 직접 지정해주면 된다!인코딩과 디코딩이란 결국 각각 인코더/디코더에서 컨테이너를 가져와서 오브젝트의 타입에 따라 key
를 지정해 이를 변환한 데이터를 넣고 빼는 과정이다.
conainer
들은 딕셔너리와 유사한데, enum
을 사용해서 해당 container
가 어떠한 key
들을 갖는 지 알려줘야 한다! CodingKey
를 사용할 수 있는데, String 타입
의 raw value
를 선언하면 인코딩/디코딩 시 key
를 커스텀 해 사용할 수도 있다!디코딩 시에는 각 key
별로 디코딩을 해보면서 실패하면(해당 케이스가 아니라면) 다음 케이스로 넘어가는 방식
왜 타입을 인자로 넘길 때는
TypeName.self
와 같은 형식을 사용할까? 궁금했는데 공식문서를 살펴봐도 그냥 그렇게 쓴다고 얘기할 뿐 따로 설명이 없다...
extension EmojiArtModel {
enum Background: Equatable, Codable {
case blank
case url(URL)
case imageData(Data)
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let url = try? container.decode(URL.self, forKey: .url) {
self = .url(url)
} else if let imageData = try? container.decode(Data.self, forKey: .imageData) {
self = .imageData(imageData)
} else {
self = .blank
}
}
enum CodingKeys: String, CodingKey {
case url = "theURL"
case imageData
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .url(let url): try container.encode(url, forKey: .url)
case .imageData(let data): try container.encode(data, forKey: .imageData)
case .blank: break
}
}
...
}
save 함수
를 만들었으니 언제 저장해야 할 지도 정해야 하는데 그냥 모델에 변화가 있을 때마다 자동 저장하도록 구현해 볼 것이다...! 매우 간단하게 그냥 didset
을 활용해서 모델이 바뀔 때마다 autosave 함수
를 호출하면 된다class EmojiArtDocument: ObservableObject
{
@Published private(set) var emojiArt: EmojiArtModel {
didSet {
scheduleAutosave()
...
}
private struct Autosave {
static let filename = "Autosaved.emojiart"
static var url: URL? {
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
return documentDirectory?.appendingPathComponent(filename)
}
static let coalescingInterval = 5.0
}
private func autosave() {
if let url = Autosave.url {
save(to: url)
}
}
...
}
didset
에서 호출하고 있는 건 scheduledAutosave 함수
다...! 이모지를 여러 개 추가하는 등 변화가 여러번 있을 때 이를 매번 저장하는 것은 교수님 말씀에 의하면 too aggressive
하므로 연속적으로 일어난 변화는 코알리싱해서 한 번만 저장하는 것...!Timer
를 이용할건데, didset
을 사용해서 모델에 변화가 있을 때마다, Timer
가 이미 있다면 무효화하고, 없는 경우 바로 새 타이머를 설정해서 시간이 다 되면 autosave 함수
를 호출하는 방식이다! scheduledAutosave 함수
를 보내면 내부에서 self
를 호출함에도 weak
처리를 하지 않았다...! 이는 우리가 해당 document
를 닫았을 때 바로 self
가 바로 프리되는 게 아니라 닫았더라도 타이머가 다 돼서 자동 저장이 된 다음에 프리되기를 원하기 때문이다...!class EmojiArtDocument: ObservableObject
{
...
private var autosaveTimer: Timer?
private func scheduleAutosave() {
autosaveTimer?.invalidate()
autosaveTimer = Timer.scheduledTimer(withTimeInterval: Autosave.coalescingInterval, repeats: false) { _ in
self.autosave()
}
}
...
}
EmojiArtModel
이 JSON Data
형식으로 저장되어 있으므로 1) 주소를 이용해 데이터를 불러와서 2) EmojiArtModel 타입
으로 디코딩 해주면 된다!struct EmojiArtModel: Codable {
...
init(url: URL) throws {
let data = try Data(contentsOf: url)
self = try EmojiArtModel(json: data)
}
init(json: Data) throws {
self = try JSONDecoder().decode(EmojiArtModel.self, from: json)
}
...
}
그러고 나서 ViewModel
에서 init
할 때 파일 시스템에 저장된 Model
이 있는지 확인해서 있으면 불러오고 없으면 새로운 Model
객체를 생성한다!
Model
은 배경 이미지를 URL
혹은 Data
형태로 저장하고 있으므로 실제로 적용해주기 위해서는 Model
을 불러온 후 fetchBackgroundImageDataIfNecessary 함수
를 호출해줘야 한다...! classs EmojiArtDocument: ObservableObject {
init() {
if let url = Autosave.url, let autosavedEmojiArt = try? EmojiArtModel(url: url) {
emojiArt = autosavedEmojiArt
fetchBackgroundImageDataIfNecessary()
} else {
emojiArt = EmojiArtModel()
}
}
}
print문
을 사용해서 콘솔로 결과를 체크할 때, 키워드를 입력해서 일치하는 결과만 필터링 할 수 있다...!이번 강의에서 처음으로 ViewModel
을 여러 개 적용하는 법을 실습했다...! UI 하단에 EmojiArt
에 추가할 수 있는 이모지들이 테마별로 나타나는 데 이러한 이모지 팔레트들은 저장하는 PaletteStore 클래스
와 개별 Palette 구조체
를 정의했다...! 아마도 다음 강의에서 View
와 연동하는 방법을 배울 것 같아서 자세한 설명은 생략...
remove 함수
위에 보면 @discardableResult
가 붙어있는데, 말그대로 리턴 값을 버릴 수 있다는 의미다. 즉, 리턴 값을 사용하지 않아도 warning
을 띄우지 않도록 하는 역할...!
struct Palette: Identifiable, Codable, Hashable {
var name: String
var emojis: String
var id: Int
fileprivate init(name: String, emojis: String, id: Int) {
self.name = name
self.emojis = emojis
self.id = id
}
}
class PaletteStore: ObservableObject {
let name: String
@Published var palettes = [Palette]()
init(named name: String) {
self.name = name
if palettes.isEmpty {
insertPalette(named: "Vehicles", emojis: "🚙🚗🚘🚕🚖🏎🚚🛻🚛🚐🚓🚔🚑🚒🚀✈️🛫🛬🛩🚁🛸🚲🏍🛶⛵️🚤🛥🛳⛴🚢🚂🚝🚅🚆🚊🚉🚇🛺🚜")
insertPalette(named: "Sports", emojis: "🏈⚾️🏀⚽️🎾🏐🥏🏓⛳️🥅🥌🏂⛷🎳")
insertPalette(named: "Music", emojis: "🎼🎤🎹🪘🥁🎺🪗🪕🎻")
insertPalette(named: "Animals", emojis: "🐥🐣🐂🐄🐎🐖🐏🐑🦙🐐🐓🐁🐀🐒🦆🦅🦉🦇🐢🐍🦎🦖🦕🐅🐆🦓🦍🦧🦣🐘🦛🦏🐪🐫🦒🦘🦬🐃🦙🐐🦌🐕🐩🦮🐈🦤🦢🦩🕊🦝🦨🦡🦫🦦🦥🐿🦔")
insertPalette(named: "Animal Faces", emojis: "🐵🙈🙊🙉🐶🐱🐭🐹🐰🦊🐻🐼🐻❄️🐨🐯🦁🐮🐷🐸🐲")
insertPalette(named: "Flora", emojis: "🌲🌴🌿☘️🍀🍁🍄🌾💐🌷🌹🥀🌺🌸🌼🌻")
insertPalette(named: "Weather", emojis: "☀️🌤⛅️🌥☁️🌦🌧⛈🌩🌨❄️💨☔️💧💦🌊☂️🌫🌪")
insertPalette(named: "COVID", emojis: "💉🦠😷🤧🤒")
insertPalette(named: "Faces", emojis: "😀😃😄😁😆😅😂🤣🥲☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩🥳😏😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤯😳🥶😥😓🤗🤔🤭🤫🤥😬🙄😯😧🥱😴🤮😷🤧🤒🤠")
}
}
// MARK: - Intent
func palette(at index: Int) -> Palette {
let safeIndex = min(max(index, 0), palettes.count - 1)
return palettes[safeIndex]
}
@discardableResult
func removePalette(at index: Int) -> Int {
if palettes.count > 1, palettes.indices.contains(index) {
palettes.remove(at: index)
}
return index % palettes.count
}
func insertPalette(named name: String, emojis: String? = nil, at index: Int = 0) {
let unique = (palettes.max(by: { $0.id < $1.id })?.id ?? 0) + 1
let palette = Palette(name: name, emojis: emojis ?? "", id: unique)
let safeIndex = min(max(index, 0), palettes.count)
palettes.insert(palette, at: safeIndex)
}
}
palettes
는 유저가 document
로 인식하지는 않지만 저장될 필요가 있는 간단한 정보이므로 UserDefaults
에 저장하기 매우 적합하다...!UserDefaults
는 이름 그대로 유저의 선택에 따른 설정값들을 딕셔너리 형태로 저장해놓고 앱이 다시 실행될 때마다 저장된 값들을 불러워 디폴트 상태를 구축하게 도와주는 인터페이스다!file system
에 데이터를 저장했을 때와 유사한 과정으로 데이터를 저장한다. 그러나 인코딩을 해주는 이유가 좀 다르다palettes 배열
이 담고 있는 개별 Palette
는 구조체인데, 구조체는 UserDefaults
가 저장할 수 있는 Property List
에 포함되지 않는다...Data
는 Property List
에 포함되므로 UserDefaults
에 palettes 배열
을 저장하기 위해서는 Palette 구조체
를 Data
로 인코딩해줘야한다...!set(_:forkey:) 메서드
를 이용해 앞서 선언한 userDefaultsKey
의 밸류를 JSON Data
로 설정해주면 끝!palettes
가 바뀔 때마다 didSet
을 이용해 다시 저장하도록 하면 된다!class PaletteStore: ObservableObject {
let name: String
@Published var palettes = [Palette]() {
didSet {
storeInUserDefaults()
}
}
private var userDefaultsKey: String {
"PaletteStore:" + name
}
private func storeInUserDefaults() {
UserDefaults.standard.set(try? JSONEncoder().encode(palettes), forKey: userDefaultsKey)
}
...
}
palettes 타입
로 디코딩해서 palettes 변수
에 할당해주면 된다!palettes
가 비어있으므로 init
에서 이를 체크해서 미리 정의해 둔palette
들을 넣어준다 class PaletteStore: ObservableObject {
...
private func restoreFromUserDefaults() {
if let jsonData = UserDefaults.standard.data(forKey: userDefaultsKey),
let decodedPalettes = try? JSONDecoder().decode(Array<Palette>.self, from: jsonData) {
palettes = decodedPalettes
}
}
init(named name: String) {
self.name = name
restoreFromUserDefaults()
if palettes.isEmpty {
// default Palettes to insert
}
}
...
}
File System
을 언제 쓰면 좋을 지에 대해, 그리고 유닉스 파일 시스템 에 대해 더 공부를 해 보면 좋을 것 같다.... ViewModel
들을 쓰는 것을 보면서 늘 ViewModel
을 분리하는 기준이라던가, 여러 개를 사용하는 법이 궁금했는데 이번 주차에 궁금증이 해소될 것 같아 기대가 된다!UserDefaults
는 사실 이전에도 몇 번 써본 적 있었는데 부끄럽지만 그냥 시키니까 저장하고 불러올 때 인코딩/디코딩 했었다...그런데 Property List
에 해당하지 않는 오브젝트들을 저장하기 위함임을 배울 수 있었다..!