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 에 해당하지 않는 오브젝트들을 저장하기 위함임을 배울 수 있었다..!