From 3abdab15d30d525e7d65c3cf39b1ee276e309c6b Mon Sep 17 00:00:00 2001 From: karen Date: Thu, 14 Sep 2023 17:06:00 +0900 Subject: [PATCH 01/15] =?UTF-8?q?=E2=9C=B4=EF=B8=8F=20feat:=20CoreLocation?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=9C=84=EC=B9=98=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DiaryDetailViewController.swift | 6 +++- Diary/Controller/DiaryViewController.swift | 30 ++++++++++++++++++- Diary/Resource/Info.plist | 2 ++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Diary/Controller/DiaryDetailViewController.swift b/Diary/Controller/DiaryDetailViewController.swift index 721abe1c8..c1094d2c7 100644 --- a/Diary/Controller/DiaryDetailViewController.swift +++ b/Diary/Controller/DiaryDetailViewController.swift @@ -12,6 +12,8 @@ final class DiaryDetailViewController: UIViewController, Shareable { private let diary: Diary private let isUpdated: Bool + private var latitude: Double? + private var longitude: Double? private let contentTextView = { let textView = UITextView() @@ -34,9 +36,11 @@ final class DiaryDetailViewController: UIViewController, Shareable { saveDiary() } - init(diary: Diary, isUpdated: Bool = true) { + init(diary: Diary, isUpdated: Bool = true, latitude: Double? = nil, longitude: Double? = nil) { self.diary = diary self.isUpdated = isUpdated + self.latitude = latitude + self.longitude = longitude super.init(nibName: nil, bundle: nil) } diff --git a/Diary/Controller/DiaryViewController.swift b/Diary/Controller/DiaryViewController.swift index d14d6733d..631a0562a 100644 --- a/Diary/Controller/DiaryViewController.swift +++ b/Diary/Controller/DiaryViewController.swift @@ -5,15 +5,20 @@ // import UIKit +import CoreLocation final class DiaryViewController: UIViewController, Shareable { typealias Contents = String private var tableView = UITableView() private var diaryList = [Diary]() + private var locationManager = CLLocationManager() + private var latitude: Double? + private var longitude: Double? override func viewDidLoad() { super.viewDidLoad() + configureLocationManager() configure() } @@ -101,6 +106,24 @@ private extension DiaryViewController { } } +extension DiaryViewController: CLLocationManagerDelegate { + private func configureLocationManager() { + locationManager.delegate = self + locationManager.requestWhenInUseAuthorization() + locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.startUpdatingLocation() + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let coordinate = locations.last?.coordinate else { + return + } + + latitude = coordinate.latitude + longitude = coordinate.longitude + } +} + private extension DiaryViewController { func configure() { configureRootView() @@ -118,7 +141,12 @@ private extension DiaryViewController { func configureNavigation() { let action = UIAction { [weak self] _ in let diary = CoreDataManager.shared.create() - let diaryDetailViewController = DiaryDetailViewController(diary: diary, isUpdated: false) + let diaryDetailViewController = DiaryDetailViewController( + diary: diary, + isUpdated: false, + latitude: self?.latitude, + longitude: self?.longitude + ) self?.navigationController?.pushViewController(diaryDetailViewController, animated: true) } diff --git a/Diary/Resource/Info.plist b/Diary/Resource/Info.plist index dd3c9afda..a4de3ecfa 100644 --- a/Diary/Resource/Info.plist +++ b/Diary/Resource/Info.plist @@ -2,6 +2,8 @@ + NSLocationWhenInUseUsageDescription + 위치 기반 날씨 정보를 위해 GPS정보가 필요합니다. UIApplicationSceneManifest UIApplicationSupportsMultipleScenes From bee30219c21b9bbf144f5d0ee2eae4e9f868e81d Mon Sep 17 00:00:00 2001 From: Hoon94 Date: Thu, 14 Sep 2023 18:17:03 +0900 Subject: [PATCH 02/15] =?UTF-8?q?=E2=9C=B4=EF=B8=8F=20feat:=20decodeData?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84=20=EB=B0=8F?= =?UTF-8?q?=20=EB=AA=A8=EB=8D=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Diary.xcodeproj/project.pbxproj | 36 +++++++++++ Diary/Network/DecodingManager.swift | 20 ++++++ Diary/Network/Error/DecodingError.swift | 17 +++++ Diary/Network/Model/Location.swift | 83 +++++++++++++++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 Diary/Network/DecodingManager.swift create mode 100644 Diary/Network/Error/DecodingError.swift create mode 100644 Diary/Network/Model/Location.swift diff --git a/Diary.xcodeproj/project.pbxproj b/Diary.xcodeproj/project.pbxproj index be8e88b60..44dd1815e 100644 --- a/Diary.xcodeproj/project.pbxproj +++ b/Diary.xcodeproj/project.pbxproj @@ -11,6 +11,9 @@ 78274D9A2A9E26DF00AD4F50 /* DiaryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78274D992A9E26DF00AD4F50 /* DiaryCell.swift */; }; 78274DA12A9E3E8E00AD4F50 /* DiaryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78274DA02A9E3E8E00AD4F50 /* DiaryModel.swift */; }; 78274DA42A9F3F0A00AD4F50 /* DateFormatter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78274DA32A9F3F0A00AD4F50 /* DateFormatter+.swift */; }; + 784F30C92AB2F88F009A895B /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784F30C82AB2F88F009A895B /* Location.swift */; }; + 784F30CB2AB2FE87009A895B /* DecodingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784F30CA2AB2FE87009A895B /* DecodingManager.swift */; }; + 784F30CE2AB30072009A895B /* DecodingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784F30CD2AB30072009A895B /* DecodingError.swift */; }; 92FB9B8365F3B707B891705F /* Pods_Diary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EEBF20D2433AEB2D78258668 /* Pods_Diary.framework */; }; 9C1529112AAE140900F3203E /* Shareable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C1529102AAE140800F3203E /* Shareable.swift */; }; 9C184BBF2AA21892003863E7 /* CellIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C184BBE2AA21892003863E7 /* CellIdentifier.swift */; }; @@ -33,6 +36,9 @@ 78274D992A9E26DF00AD4F50 /* DiaryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryCell.swift; sourceTree = ""; }; 78274DA02A9E3E8E00AD4F50 /* DiaryModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryModel.swift; sourceTree = ""; }; 78274DA32A9F3F0A00AD4F50 /* DateFormatter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+.swift"; sourceTree = ""; }; + 784F30C82AB2F88F009A895B /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; + 784F30CA2AB2FE87009A895B /* DecodingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingManager.swift; sourceTree = ""; }; + 784F30CD2AB30072009A895B /* DecodingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingError.swift; sourceTree = ""; }; 9C1529102AAE140800F3203E /* Shareable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shareable.swift; sourceTree = ""; }; 9C184BBE2AA21892003863E7 /* CellIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellIdentifier.swift; sourceTree = ""; }; 9C4621EB2AB06890004ED11A /* PersistentContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentContainer.swift; sourceTree = ""; }; @@ -122,6 +128,32 @@ path = Extension; sourceTree = ""; }; + 784F30C62AB2F7B9009A895B /* Network */ = { + isa = PBXGroup; + children = ( + 784F30C72AB2F7F2009A895B /* Model */, + 784F30CC2AB3004E009A895B /* Error */, + 784F30CA2AB2FE87009A895B /* DecodingManager.swift */, + ); + path = Network; + sourceTree = ""; + }; + 784F30C72AB2F7F2009A895B /* Model */ = { + isa = PBXGroup; + children = ( + 784F30C82AB2F88F009A895B /* Location.swift */, + ); + path = Model; + sourceTree = ""; + }; + 784F30CC2AB3004E009A895B /* Error */ = { + isa = PBXGroup; + children = ( + 784F30CD2AB30072009A895B /* DecodingError.swift */, + ); + path = Error; + sourceTree = ""; + }; 9C15290F2AAE13ED00F3203E /* Protocol */ = { isa = PBXGroup; children = ( @@ -162,6 +194,7 @@ C739AE23284DF28600741E8F /* Diary */ = { isa = PBXGroup; children = ( + 784F30C62AB2F7B9009A895B /* Network */, 9C5A1DD72AAB704700162C98 /* CoreData */, 78274D9B2A9E319500AD4F50 /* Model */, 78274D9C2A9E31A200AD4F50 /* View */, @@ -316,7 +349,10 @@ 78274DA42A9F3F0A00AD4F50 /* DateFormatter+.swift in Sources */, C739AE27284DF28600741E8F /* SceneDelegate.swift in Sources */, 9C4621EC2AB06890004ED11A /* PersistentContainer.swift in Sources */, + 784F30C92AB2F88F009A895B /* Location.swift in Sources */, + 784F30CB2AB2FE87009A895B /* DecodingManager.swift in Sources */, 9C5A1DD62AAA3F5B00162C98 /* UITextView+.swift in Sources */, + 784F30CE2AB30072009A895B /* DecodingError.swift in Sources */, 9C75C4CA2A9F448400C5D1CB /* DiaryDetailViewController.swift in Sources */, 9C184BBF2AA21892003863E7 /* CellIdentifier.swift in Sources */, 78274D9A2A9E26DF00AD4F50 /* DiaryCell.swift in Sources */, diff --git a/Diary/Network/DecodingManager.swift b/Diary/Network/DecodingManager.swift new file mode 100644 index 000000000..370d4401a --- /dev/null +++ b/Diary/Network/DecodingManager.swift @@ -0,0 +1,20 @@ +// +// DecodingManager.swift +// Diary +// +// Created by hoon, karen on 2023/09/14. +// + +import Foundation + +struct DecodingManager { + static func decodeData(from data: Data) throws -> T { + let decoder = JSONDecoder() + + guard let decodedData = try? decoder.decode(T.self, from: data) else { + throw DecodingError.decodingFailure + } + + return decodedData + } +} diff --git a/Diary/Network/Error/DecodingError.swift b/Diary/Network/Error/DecodingError.swift new file mode 100644 index 000000000..fd75e3cec --- /dev/null +++ b/Diary/Network/Error/DecodingError.swift @@ -0,0 +1,17 @@ +// +// DecodingError.swift +// Diary +// +// Created by hoon, karen on 2023/09/14. +// + +enum DecodingError: Error { + case decodingFailure + + var description: String { + switch self { + case .decodingFailure: + return "디코딩을 실패하였습니다." + } + } +} diff --git a/Diary/Network/Model/Location.swift b/Diary/Network/Model/Location.swift new file mode 100644 index 000000000..fc226f020 --- /dev/null +++ b/Diary/Network/Model/Location.swift @@ -0,0 +1,83 @@ +// +// Location.swift +// Diary +// +// Created by hoon, karen on 2023/09/14. +// + +struct Location: Decodable { + let coordinate: Coordinate + let weather: [Weather] + let base: String + let main: Main + let visibility: Int + let wind: Wind + let rain: Rain + let clouds: Clouds + let date: Int + let sys: Sys + let timezone, id: Int + let name: String + let cod: Int + + private enum CodingKeys: String, CodingKey { + case coordinate = "coord" + case weather, base, main, visibility, wind, rain, clouds + case date = "dt" + case sys, timezone, id, name, cod + } +} + +struct Coordinate: Decodable { + let longitude: Double + let latitude: Double + + private enum CodingKeys: String, CodingKey { + case longitude = "lon" + case latitude = "lat" + } +} + +struct Weather: Decodable { + let id: Int + let main, description, icon: String +} + +struct Clouds: Decodable { + let all: Int +} + +struct Main: Decodable { + let temp, feelsLike, tempMin, tempMax: Double + let pressure, humidity, seaLevel, grndLevel: Int + + enum CodingKeys: String, CodingKey { + case temp + case feelsLike = "feels_like" + case tempMin = "temp_min" + case tempMax = "temp_max" + case pressure, humidity + case seaLevel = "sea_level" + case grndLevel = "grnd_level" + } +} + +struct Rain: Decodable { + let the1H: Double + + enum CodingKeys: String, CodingKey { + case the1H = "1h" + } +} + +struct Sys: Decodable { + let type, id: Int + let country: String + let sunrise, sunset: Int +} + +struct Wind: Decodable { + let speed: Double + let deg: Int + let gust: Double +} From 2c4dcc2fa7d1baa5ab6aa33a2b66aeeb5b488dac Mon Sep 17 00:00:00 2001 From: karen Date: Thu, 14 Sep 2023 21:44:08 +0900 Subject: [PATCH 03/15] =?UTF-8?q?=E2=9C=B4=EF=B8=8F=20feat:=20NetworkManag?= =?UTF-8?q?er=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20fetchWeather=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Diary.xcodeproj/project.pbxproj | 8 ++++ .../DiaryDetailViewController.swift | 27 +++++++++++ Diary/Network/Error/NetworkError.swift | 26 +++++++++++ Diary/Network/Model/Location.swift | 3 +- Diary/Network/NetworkManager.swift | 45 +++++++++++++++++++ 5 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 Diary/Network/Error/NetworkError.swift create mode 100644 Diary/Network/NetworkManager.swift diff --git a/Diary.xcodeproj/project.pbxproj b/Diary.xcodeproj/project.pbxproj index 44dd1815e..07eef2ce5 100644 --- a/Diary.xcodeproj/project.pbxproj +++ b/Diary.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ 92FB9B8365F3B707B891705F /* Pods_Diary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EEBF20D2433AEB2D78258668 /* Pods_Diary.framework */; }; 9C1529112AAE140900F3203E /* Shareable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C1529102AAE140800F3203E /* Shareable.swift */; }; 9C184BBF2AA21892003863E7 /* CellIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C184BBE2AA21892003863E7 /* CellIdentifier.swift */; }; + 9C243B302AB307E1009DBF80 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C243B2F2AB307E1009DBF80 /* NetworkManager.swift */; }; + 9C243B322AB308BC009DBF80 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C243B312AB308BC009DBF80 /* NetworkError.swift */; }; 9C4621EC2AB06890004ED11A /* PersistentContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4621EB2AB06890004ED11A /* PersistentContainer.swift */; }; 9C4621EE2AB0739F004ED11A /* Array+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4621ED2AB0739F004ED11A /* Array+.swift */; }; 9C5A1DD62AAA3F5B00162C98 /* UITextView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5A1DD52AAA3F5B00162C98 /* UITextView+.swift */; }; @@ -41,6 +43,8 @@ 784F30CD2AB30072009A895B /* DecodingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingError.swift; sourceTree = ""; }; 9C1529102AAE140800F3203E /* Shareable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shareable.swift; sourceTree = ""; }; 9C184BBE2AA21892003863E7 /* CellIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellIdentifier.swift; sourceTree = ""; }; + 9C243B2F2AB307E1009DBF80 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; + 9C243B312AB308BC009DBF80 /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; 9C4621EB2AB06890004ED11A /* PersistentContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentContainer.swift; sourceTree = ""; }; 9C4621ED2AB0739F004ED11A /* Array+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+.swift"; sourceTree = ""; }; 9C5A1DD52AAA3F5B00162C98 /* UITextView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+.swift"; sourceTree = ""; }; @@ -134,6 +138,7 @@ 784F30C72AB2F7F2009A895B /* Model */, 784F30CC2AB3004E009A895B /* Error */, 784F30CA2AB2FE87009A895B /* DecodingManager.swift */, + 9C243B2F2AB307E1009DBF80 /* NetworkManager.swift */, ); path = Network; sourceTree = ""; @@ -150,6 +155,7 @@ isa = PBXGroup; children = ( 784F30CD2AB30072009A895B /* DecodingError.swift */, + 9C243B312AB308BC009DBF80 /* NetworkError.swift */, ); path = Error; sourceTree = ""; @@ -342,6 +348,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9C243B302AB307E1009DBF80 /* NetworkManager.swift in Sources */, C739AE29284DF28600741E8F /* DiaryViewController.swift in Sources */, 9C1529112AAE140900F3203E /* Shareable.swift in Sources */, 78274DA12A9E3E8E00AD4F50 /* DiaryModel.swift in Sources */, @@ -359,6 +366,7 @@ C739AE2F284DF28600741E8F /* Diary.xcdatamodeld in Sources */, 9C5A1DD92AAB707A00162C98 /* CoreDataManager.swift in Sources */, 9C4621EE2AB0739F004ED11A /* Array+.swift in Sources */, + 9C243B322AB308BC009DBF80 /* NetworkError.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Diary/Controller/DiaryDetailViewController.swift b/Diary/Controller/DiaryDetailViewController.swift index c1094d2c7..fea60f183 100644 --- a/Diary/Controller/DiaryDetailViewController.swift +++ b/Diary/Controller/DiaryDetailViewController.swift @@ -12,6 +12,8 @@ final class DiaryDetailViewController: UIViewController, Shareable { private let diary: Diary private let isUpdated: Bool + private var weatherMain: String = "" + private var weatherIcon: String = "" private var latitude: Double? private var longitude: Double? @@ -29,6 +31,7 @@ final class DiaryDetailViewController: UIViewController, Shareable { override func viewDidLoad() { super.viewDidLoad() configure() + fetchWeather() } override func viewWillDisappear(_ animated: Bool) { @@ -181,3 +184,27 @@ private extension DiaryDetailViewController { ]) } } + +private extension DiaryDetailViewController { + func fetchWeather() { + NetworkManager.shared.fetchData { [weak self] result in + switch result { + case .success(let data): + do { + let decodedData: Location = try DecodingManager.decodeData(from: data) + + guard let currentWeather = decodedData.weather.first else { + return + } + + self?.weatherMain = currentWeather.main + self?.weatherIcon = currentWeather.icon + } catch { + print("error") + } + case .failure(let error): + print(error.description) + } + } + } +} diff --git a/Diary/Network/Error/NetworkError.swift b/Diary/Network/Error/NetworkError.swift new file mode 100644 index 000000000..eff871e07 --- /dev/null +++ b/Diary/Network/Error/NetworkError.swift @@ -0,0 +1,26 @@ +// +// NetworkError.swift +// Diary +// +// Created by hoon, karen on 2023/09/14. +// + +enum NetworkError: Error { + case invalidURL + case failureRequest + case failureResponse + case invalidDataType + + var description: String { + switch self { + case .invalidURL: + return "url형식이 잘못되었습니다" + case .failureRequest: + return "데이터 요청에 실패했습니다." + case .failureResponse: + return "응답이 없습니다." + case .invalidDataType: + return "올바르지 않는 데이터 포맷입니다" + } + } +} diff --git a/Diary/Network/Model/Location.swift b/Diary/Network/Model/Location.swift index fc226f020..f33b58c0d 100644 --- a/Diary/Network/Model/Location.swift +++ b/Diary/Network/Model/Location.swift @@ -12,7 +12,6 @@ struct Location: Decodable { let main: Main let visibility: Int let wind: Wind - let rain: Rain let clouds: Clouds let date: Int let sys: Sys @@ -22,7 +21,7 @@ struct Location: Decodable { private enum CodingKeys: String, CodingKey { case coordinate = "coord" - case weather, base, main, visibility, wind, rain, clouds + case weather, base, main, visibility, wind, clouds case date = "dt" case sys, timezone, id, name, cod } diff --git a/Diary/Network/NetworkManager.swift b/Diary/Network/NetworkManager.swift new file mode 100644 index 000000000..70c95a9a7 --- /dev/null +++ b/Diary/Network/NetworkManager.swift @@ -0,0 +1,45 @@ +// +// NetworkManager.swift +// Diary +// +// Created by hoon, karen on 2023/09/14. +// + +import Foundation + +final class NetworkManager { + static let shared = NetworkManager() + + private init() {} + + func fetchData(completion: @escaping (Result) -> Void) { + guard let url = URL(string: "") else { + completion(.failure(.invalidURL)) + return + } + + let request = URLRequest(url: url) + let task = URLSession.shared.dataTask(with: request) { data, response, error in + guard error == nil else { + completion(.failure(.failureRequest)) + return + } + + guard let response = response, + let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + completion(.failure(.failureResponse)) + return + } + + guard let data = data else { + completion(.failure(.invalidDataType)) + return + } + + completion(.success(data)) + } + + task.resume() + } +} From c0f98317586b614598dc75a7d6b41b91630b107d Mon Sep 17 00:00:00 2001 From: Hoon94 Date: Thu, 14 Sep 2023 23:01:33 +0900 Subject: [PATCH 04/15] =?UTF-8?q?=E2=9C=B4=EF=B8=8F=20feat:=20WeatherAPI?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20API=20KEY=20=EC=88=A8?= =?UTF-8?q?=EA=B8=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Diary.xcodeproj/project.pbxproj | 8 ++++ .../DiaryDetailViewController.swift | 6 ++- Diary/Network/Model/Location.swift | 5 ++- Diary/Network/Model/WeatherAPI.swift | 40 +++++++++++++++++++ Diary/Network/NetworkManager.swift | 5 ++- Diary/Resource/WeatherInfo.plist | 8 ++++ 6 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 Diary/Network/Model/WeatherAPI.swift create mode 100644 Diary/Resource/WeatherInfo.plist diff --git a/Diary.xcodeproj/project.pbxproj b/Diary.xcodeproj/project.pbxproj index 07eef2ce5..acf02fbd2 100644 --- a/Diary.xcodeproj/project.pbxproj +++ b/Diary.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 784F30C92AB2F88F009A895B /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784F30C82AB2F88F009A895B /* Location.swift */; }; 784F30CB2AB2FE87009A895B /* DecodingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784F30CA2AB2FE87009A895B /* DecodingManager.swift */; }; 784F30CE2AB30072009A895B /* DecodingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784F30CD2AB30072009A895B /* DecodingError.swift */; }; + 78D13D952AB338B000A0D28E /* WeatherInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 78D13D942AB338B000A0D28E /* WeatherInfo.plist */; }; + 78D13D972AB33B6A00A0D28E /* WeatherAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78D13D962AB33B6A00A0D28E /* WeatherAPI.swift */; }; 92FB9B8365F3B707B891705F /* Pods_Diary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EEBF20D2433AEB2D78258668 /* Pods_Diary.framework */; }; 9C1529112AAE140900F3203E /* Shareable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C1529102AAE140800F3203E /* Shareable.swift */; }; 9C184BBF2AA21892003863E7 /* CellIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C184BBE2AA21892003863E7 /* CellIdentifier.swift */; }; @@ -41,6 +43,8 @@ 784F30C82AB2F88F009A895B /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; 784F30CA2AB2FE87009A895B /* DecodingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingManager.swift; sourceTree = ""; }; 784F30CD2AB30072009A895B /* DecodingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingError.swift; sourceTree = ""; }; + 78D13D942AB338B000A0D28E /* WeatherInfo.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = WeatherInfo.plist; sourceTree = ""; }; + 78D13D962AB33B6A00A0D28E /* WeatherAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherAPI.swift; sourceTree = ""; }; 9C1529102AAE140800F3203E /* Shareable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shareable.swift; sourceTree = ""; }; 9C184BBE2AA21892003863E7 /* CellIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellIdentifier.swift; sourceTree = ""; }; 9C243B2F2AB307E1009DBF80 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; @@ -118,6 +122,7 @@ children = ( C739AE30284DF28600741E8F /* Assets.xcassets */, C739AE35284DF28600741E8F /* Info.plist */, + 78D13D942AB338B000A0D28E /* WeatherInfo.plist */, ); path = Resource; sourceTree = ""; @@ -147,6 +152,7 @@ isa = PBXGroup; children = ( 784F30C82AB2F88F009A895B /* Location.swift */, + 78D13D962AB33B6A00A0D28E /* WeatherAPI.swift */, ); path = Model; sourceTree = ""; @@ -293,6 +299,7 @@ 78274D982A9E1F7C00AD4F50 /* .swiftlint.yml in Resources */, C739AE34284DF28600741E8F /* LaunchScreen.storyboard in Resources */, C739AE31284DF28600741E8F /* Assets.xcassets in Resources */, + 78D13D952AB338B000A0D28E /* WeatherInfo.plist in Resources */, C739AE2C284DF28600741E8F /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -365,6 +372,7 @@ 78274D9A2A9E26DF00AD4F50 /* DiaryCell.swift in Sources */, C739AE2F284DF28600741E8F /* Diary.xcdatamodeld in Sources */, 9C5A1DD92AAB707A00162C98 /* CoreDataManager.swift in Sources */, + 78D13D972AB33B6A00A0D28E /* WeatherAPI.swift in Sources */, 9C4621EE2AB0739F004ED11A /* Array+.swift in Sources */, 9C243B322AB308BC009DBF80 /* NetworkError.swift in Sources */, ); diff --git a/Diary/Controller/DiaryDetailViewController.swift b/Diary/Controller/DiaryDetailViewController.swift index fea60f183..57612cc75 100644 --- a/Diary/Controller/DiaryDetailViewController.swift +++ b/Diary/Controller/DiaryDetailViewController.swift @@ -187,7 +187,9 @@ private extension DiaryDetailViewController { private extension DiaryDetailViewController { func fetchWeather() { - NetworkManager.shared.fetchData { [weak self] result in + let weather = WeatherAPI.weatherData(latitude: latitude!, longitude: longitude!) + + NetworkManager.shared.fetchData(API: weather) { [weak self] result in switch result { case .success(let data): do { @@ -200,7 +202,7 @@ private extension DiaryDetailViewController { self?.weatherMain = currentWeather.main self?.weatherIcon = currentWeather.icon } catch { - print("error") + print(error) } case .failure(let error): print(error.description) diff --git a/Diary/Network/Model/Location.swift b/Diary/Network/Model/Location.swift index f33b58c0d..c2261b1d9 100644 --- a/Diary/Network/Model/Location.swift +++ b/Diary/Network/Model/Location.swift @@ -12,6 +12,7 @@ struct Location: Decodable { let main: Main let visibility: Int let wind: Wind + let rain: Rain? let clouds: Clouds let date: Int let sys: Sys @@ -21,7 +22,7 @@ struct Location: Decodable { private enum CodingKeys: String, CodingKey { case coordinate = "coord" - case weather, base, main, visibility, wind, clouds + case weather, base, main, visibility, wind, rain, clouds case date = "dt" case sys, timezone, id, name, cod } @@ -48,7 +49,7 @@ struct Clouds: Decodable { struct Main: Decodable { let temp, feelsLike, tempMin, tempMax: Double - let pressure, humidity, seaLevel, grndLevel: Int + let pressure, humidity, seaLevel, grndLevel: Int? enum CodingKeys: String, CodingKey { case temp diff --git a/Diary/Network/Model/WeatherAPI.swift b/Diary/Network/Model/WeatherAPI.swift new file mode 100644 index 000000000..2c2a0f007 --- /dev/null +++ b/Diary/Network/Model/WeatherAPI.swift @@ -0,0 +1,40 @@ +// +// WeatherAPI.swift +// Diary +// +// Created by hoon, karen on 2023/09/14. +// + +import Foundation + +protocol URLalbe { + var url: String? { get } +} + +enum WeatherAPI: URLalbe { + case weatherData(latitude: Double, longitude: Double) + case weatherIcon(id: String) + + var url: String? { + switch self { + case .weatherData(latitude: let latitude, longitude: let longitude): + guard let APIKey = WeatherAPI.APIKey else { + return nil + } + + return "https://api.openweathermap.org/data/2.5/weather?lat=\(latitude)&lon=\(longitude)&appid=\(APIKey)" + case .weatherIcon(id: let id): + return "https://openweathermap.org/img/wn/\(id).png" + } + } + + static var APIKey: String? { + guard let file = Bundle.main.path(forResource: "WeatherInfo", ofType: "plist"), + let resource = NSDictionary(contentsOfFile: file), + let key = resource["API_KEY"] as? String else { + fatalError("⛔️ API KEY를 가져오는데 실패하였습니다.") + } + + return key + } +} diff --git a/Diary/Network/NetworkManager.swift b/Diary/Network/NetworkManager.swift index 70c95a9a7..517c1b16c 100644 --- a/Diary/Network/NetworkManager.swift +++ b/Diary/Network/NetworkManager.swift @@ -12,8 +12,9 @@ final class NetworkManager { private init() {} - func fetchData(completion: @escaping (Result) -> Void) { - guard let url = URL(string: "") else { + func fetchData(API: T, completion: @escaping (Result) -> Void) { + guard let APIUrl = API.url, + let url = URL(string: APIUrl) else { completion(.failure(.invalidURL)) return } diff --git a/Diary/Resource/WeatherInfo.plist b/Diary/Resource/WeatherInfo.plist new file mode 100644 index 000000000..eb843a8a0 --- /dev/null +++ b/Diary/Resource/WeatherInfo.plist @@ -0,0 +1,8 @@ + + + + + API_KEY + API KEY를 입력해주세요. + + From dce924ffab4d795aa5815f05796bf500a5781f54 Mon Sep 17 00:00:00 2001 From: karen Date: Fri, 15 Sep 2023 01:49:28 +0900 Subject: [PATCH 05/15] =?UTF-8?q?=E2=9C=B4=EF=B8=8F=20feat:=20CoreData=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EB=B0=8F=20fetchIconImage=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Diary.xcodeproj/project.pbxproj | 12 ++- .../DiaryDetailViewController.swift | 12 +-- Diary/Controller/DiaryViewController.swift | 3 +- .../Diary.xcdatamodeld/.xccurrentversion | 2 +- .../Diary v2.xcdatamodel/contents | 10 +++ .../MappingFile.xcmappingmodel/xcmapping.xml | 90 +++++++++++++++++++ Diary/Network/CacheManager.swift | 14 +++ Diary/View/DiaryCell.swift | 50 ++++++++++- 8 files changed, 184 insertions(+), 9 deletions(-) create mode 100644 Diary/CoreData/Diary.xcdatamodeld/Diary v2.xcdatamodel/contents create mode 100644 Diary/CoreData/MappingFile.xcmappingmodel/xcmapping.xml create mode 100644 Diary/Network/CacheManager.swift diff --git a/Diary.xcodeproj/project.pbxproj b/Diary.xcodeproj/project.pbxproj index acf02fbd2..09ed58416 100644 --- a/Diary.xcodeproj/project.pbxproj +++ b/Diary.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ 9C4621EE2AB0739F004ED11A /* Array+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4621ED2AB0739F004ED11A /* Array+.swift */; }; 9C5A1DD62AAA3F5B00162C98 /* UITextView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5A1DD52AAA3F5B00162C98 /* UITextView+.swift */; }; 9C5A1DD92AAB707A00162C98 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5A1DD82AAB707A00162C98 /* CoreDataManager.swift */; }; + 9C5C42F22AB36D2D004FF07A /* MappingFile.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 9C5C42F12AB36D2D004FF07A /* MappingFile.xcmappingmodel */; }; + 9C5C42F42AB36F6B004FF07A /* CacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5C42F32AB36F6B004FF07A /* CacheManager.swift */; }; 9C75C4CA2A9F448400C5D1CB /* DiaryDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C75C4C92A9F448400C5D1CB /* DiaryDetailViewController.swift */; }; C739AE25284DF28600741E8F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE24284DF28600741E8F /* AppDelegate.swift */; }; C739AE27284DF28600741E8F /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE26284DF28600741E8F /* SceneDelegate.swift */; }; @@ -49,10 +51,13 @@ 9C184BBE2AA21892003863E7 /* CellIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellIdentifier.swift; sourceTree = ""; }; 9C243B2F2AB307E1009DBF80 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; 9C243B312AB308BC009DBF80 /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; + 9C243B332AB34C0F009DBF80 /* Diary v2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Diary v2.xcdatamodel"; sourceTree = ""; }; 9C4621EB2AB06890004ED11A /* PersistentContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentContainer.swift; sourceTree = ""; }; 9C4621ED2AB0739F004ED11A /* Array+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+.swift"; sourceTree = ""; }; 9C5A1DD52AAA3F5B00162C98 /* UITextView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+.swift"; sourceTree = ""; }; 9C5A1DD82AAB707A00162C98 /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; + 9C5C42F12AB36D2D004FF07A /* MappingFile.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = MappingFile.xcmappingmodel; sourceTree = ""; }; + 9C5C42F32AB36F6B004FF07A /* CacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManager.swift; sourceTree = ""; }; 9C75C4C92A9F448400C5D1CB /* DiaryDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryDetailViewController.swift; sourceTree = ""; }; A06CFFD4E1642CCF7B5F0BAC /* Pods-Diary.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Diary.debug.xcconfig"; path = "Target Support Files/Pods-Diary/Pods-Diary.debug.xcconfig"; sourceTree = ""; }; C739AE21284DF28600741E8F /* Diary.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Diary.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -144,6 +149,7 @@ 784F30CC2AB3004E009A895B /* Error */, 784F30CA2AB2FE87009A895B /* DecodingManager.swift */, 9C243B2F2AB307E1009DBF80 /* NetworkManager.swift */, + 9C5C42F32AB36F6B004FF07A /* CacheManager.swift */, ); path = Network; sourceTree = ""; @@ -180,6 +186,7 @@ 9C5A1DD82AAB707A00162C98 /* CoreDataManager.swift */, 9C4621EB2AB06890004ED11A /* PersistentContainer.swift */, C739AE2D284DF28600741E8F /* Diary.xcdatamodeld */, + 9C5C42F12AB36D2D004FF07A /* MappingFile.xcmappingmodel */, ); path = CoreData; sourceTree = ""; @@ -361,6 +368,7 @@ 78274DA12A9E3E8E00AD4F50 /* DiaryModel.swift in Sources */, C739AE25284DF28600741E8F /* AppDelegate.swift in Sources */, 78274DA42A9F3F0A00AD4F50 /* DateFormatter+.swift in Sources */, + 9C5C42F42AB36F6B004FF07A /* CacheManager.swift in Sources */, C739AE27284DF28600741E8F /* SceneDelegate.swift in Sources */, 9C4621EC2AB06890004ED11A /* PersistentContainer.swift in Sources */, 784F30C92AB2F88F009A895B /* Location.swift in Sources */, @@ -371,6 +379,7 @@ 9C184BBF2AA21892003863E7 /* CellIdentifier.swift in Sources */, 78274D9A2A9E26DF00AD4F50 /* DiaryCell.swift in Sources */, C739AE2F284DF28600741E8F /* Diary.xcdatamodeld in Sources */, + 9C5C42F22AB36D2D004FF07A /* MappingFile.xcmappingmodel in Sources */, 9C5A1DD92AAB707A00162C98 /* CoreDataManager.swift in Sources */, 78D13D972AB33B6A00A0D28E /* WeatherAPI.swift in Sources */, 9C4621EE2AB0739F004ED11A /* Array+.swift in Sources */, @@ -601,9 +610,10 @@ C739AE2D284DF28600741E8F /* Diary.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 9C243B332AB34C0F009DBF80 /* Diary v2.xcdatamodel */, C739AE2E284DF28600741E8F /* Diary.xcdatamodel */, ); - currentVersion = C739AE2E284DF28600741E8F /* Diary.xcdatamodel */; + currentVersion = 9C243B332AB34C0F009DBF80 /* Diary v2.xcdatamodel */; path = Diary.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Diary/Controller/DiaryDetailViewController.swift b/Diary/Controller/DiaryDetailViewController.swift index 57612cc75..36cdc6527 100644 --- a/Diary/Controller/DiaryDetailViewController.swift +++ b/Diary/Controller/DiaryDetailViewController.swift @@ -12,8 +12,6 @@ final class DiaryDetailViewController: UIViewController, Shareable { private let diary: Diary private let isUpdated: Bool - private var weatherMain: String = "" - private var weatherIcon: String = "" private var latitude: Double? private var longitude: Double? @@ -187,7 +185,11 @@ private extension DiaryDetailViewController { private extension DiaryDetailViewController { func fetchWeather() { - let weather = WeatherAPI.weatherData(latitude: latitude!, longitude: longitude!) + guard let latitude, let longitude else { + return + } + + let weather = WeatherAPI.weatherData(latitude: latitude, longitude: longitude) NetworkManager.shared.fetchData(API: weather) { [weak self] result in switch result { @@ -199,8 +201,8 @@ private extension DiaryDetailViewController { return } - self?.weatherMain = currentWeather.main - self?.weatherIcon = currentWeather.icon + self?.diary.main = currentWeather.main + self?.diary.icon = currentWeather.icon } catch { print(error) } diff --git a/Diary/Controller/DiaryViewController.swift b/Diary/Controller/DiaryViewController.swift index 631a0562a..3d30e7917 100644 --- a/Diary/Controller/DiaryViewController.swift +++ b/Diary/Controller/DiaryViewController.swift @@ -84,7 +84,8 @@ extension DiaryViewController: UITableViewDataSource { cell.configureCell( title: diary.title, date: formattedDate, - preview: diary.body + preview: diary.body, + icon: diary.icon ) return cell diff --git a/Diary/CoreData/Diary.xcdatamodeld/.xccurrentversion b/Diary/CoreData/Diary.xcdatamodeld/.xccurrentversion index d49fecccc..1d3080f41 100644 --- a/Diary/CoreData/Diary.xcdatamodeld/.xccurrentversion +++ b/Diary/CoreData/Diary.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Diary.xcdatamodel + Diary v2.xcdatamodel diff --git a/Diary/CoreData/Diary.xcdatamodeld/Diary v2.xcdatamodel/contents b/Diary/CoreData/Diary.xcdatamodeld/Diary v2.xcdatamodel/contents new file mode 100644 index 000000000..9af51bb36 --- /dev/null +++ b/Diary/CoreData/Diary.xcdatamodeld/Diary v2.xcdatamodel/contents @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Diary/CoreData/MappingFile.xcmappingmodel/xcmapping.xml b/Diary/CoreData/MappingFile.xcmappingmodel/xcmapping.xml new file mode 100644 index 000000000..30f80e097 --- /dev/null +++ b/Diary/CoreData/MappingFile.xcmappingmodel/xcmapping.xml @@ -0,0 +1,90 @@ + + + + + + 134481920 + 7B986C4C-D136-409E-BEAD-E9533258C3DC + 108 + + + + NSPersistenceFrameworkVersion + 1251 + NSStoreModelVersionHashes + + XDDevAttributeMapping + + 0plcXXRN7XHKl5CcF+fwriFmUpON3ZtcI/AfK748aWc= + + XDDevEntityMapping + + qeN1Ym3TkWN1G6dU9RfX6Kd2ccEvcDVWHpd3LpLgboI= + + XDDevMappingModel + + EqtMzvRnVZWkXwBHu4VeVGy8UyoOe+bi67KC79kphlQ= + + XDDevPropertyMapping + + XN33V44TTGY4JETlMoOB5yyTKxB+u4slvDIinv0rtGA= + + XDDevRelationshipMapping + + akYY9LhehVA/mCb4ATLWuI9XGLcjpm14wWL1oEBtIcs= + + + NSStoreModelVersionHashesDigest + +Hmc2uYZK6og+Pvx5GUJ7oW75UG4V/ksQanTjfTKUnxyGWJRMtB5tIRgVwGsrd7lz/QR57++wbvWsr6nxwyS0A== + NSStoreModelVersionHashesVersion + 3 + NSStoreModelVersionIdentifiers + + + + + + + + + main + + + + Diary + Undefined + 1 + Diary + 1 + + + + + + icon + + + + date + + + + title + + + + Diary/CoreData/Diary.xcdatamodeld/Diary.xcdatamodel + YnBsaXN0MDDUAAEAAgADAAQABQAGAAcAClgkdmVyc2lvblkkYXJjaGl2ZXJUJHRvcFgkb2JqZWN0 +cxIAAYagXxAPTlNLZXllZEFyY2hpdmVy0QAIAAlUcm9vdIABrxCVAAsADAAbADcAOAA5AEEAQgBdAF4AXwBlAGYAcgCIAIkAigCLAIwAjQCOAI8AkACRAKoArQC0ALoAyQDYANsA6gD5APwAXAEMARsBHwEjATIBOAE5AUEBUAFZAWMBZAFlAWYBewF8AYQBhQGGAZIBpgGnAagBqQGqAasBrAGtAa4BvQHMAdsB3wHuAf0B/gINAhwCKwI3AkkCSgJLAkwCTQJOAk8CUAJfAm4CfQKMAo0CnAKrAroCwgLXAtgC4ALsAwADDwMeAy0DMQNAA08DXgNtA3wDiAOaA6kDuAPHA9YD1wPmA/UEBAQZBBoEIgQuBEIEUQRgBG8EcwSCBJEEoASvBL4EygTcBOsE+gUJBRgFJwU2BUUFRgVJBVIFVgVaBV4FZgVpBW0FblUkbnVsbNcADQAOAA8AEAARABIAEwAUABUAFgAXABgAFwAaXxAPX3hkX3Jvb3RQYWNrYWdlViRjbGFzc1xfeGRfY29tbWVudHNfEBBfeGRfbW9kZWxNYW5hZ2VyXxAVX2NvbmZpZ3VyYXRpb25zQnlOYW1lXV94ZF9tb2RlbE5hbWVfEBdfbW9kZWxWZXJzaW9uSWRlbnRpZmllcoACgJSAkYAAgJKAAICT3gAcAB0AHgAfACAAIQAiAA4AIwAkACUAJgAnACgAKQAqACsACQApABcALwAwADEAMgAzACkAKQAXXxAcWERCdWNrZXRGb3JDbGFzc2Vzd2FzRW5jb2RlZF8QGlhEQnVja2V0Rm9yUGFja2FnZXNzdG9yYWdlXxAcWERCdWNrZXRGb3JJbnRlcmZhY2Vzc3RvcmFnZV8QD194ZF9vd25pbmdNb2RlbF8QHVhEQnVja2V0Rm9yUGFja2FnZXN3YXNFbmNvZGVkVl9vd25lcl8QG1hEQnVja2V0Rm9yRGF0YVR5cGVzc3RvcmFnZVtfdmlzaWJpbGl0eV8QGVhEQnVja2V0Rm9yQ2xhc3Nlc3N0b3JhZ2VVX25hbWVfEB9YREJ1Y2tldEZvckludGVyZmFjZXN3YXNFbmNvZGVkXxAeWERCdWNrZXRGb3JEYXRhVHlwZXN3YXNFbmNvZGVkXxAQX3VuaXF1ZUVsZW1lbnRJRIAEgI+AjYABgASAAICOgJAQAIAFgAOABIAEgABQU1lFU9MAOgA7AA4APAA+AEBXTlMua2V5c1pOUy5vYmplY3RzoQA9gAahAD+AB4AlVURpYXJ53xAQAEMARABFAEYAIQBHAEgAIwBJAEoADgAlAEsATAAoAE0ATgBPACkAKQAUAFMAVAAxACkATgBXAD0ATgBaAFsAXF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZV8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc2R1cGxpY2F0ZXNfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zc3RvcmFnZVtfaXNBYnN0cmFjdIAJgCyABIAEgAKACoCKgASACYCMgAaACYCLgAgIEuTNefFXb3JkZXJlZNMAOgA7AA4AYABiAEChAGGAC6EAY4AMgCVeWERfUFN0ZXJlb3R5cGXZACEAJQBnAA4AKABoACMATQBpAD8AYQBOAG0AFwApADEAXABxXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgAeAC4AJgCuAAIAECIAN0wA6ADsADgBzAH0AQKkAdAB1AHYAdwB4AHkAegB7AHyADoAPgBCAEYASgBOAFIAVgBapAH4AfwCAAIEAggCDAIQAhQCGgBeAG4AcgB6AH4AhgCOAJoAqgCVfEBNYRFBNQ29tcG91bmRJbmRleGVzXxAQWERfUFNLX2VsZW1lbnRJRF8QGVhEUE1VbmlxdWVuZXNzQ29uc3RyYWludHN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VuRW7FbwV+xX9Ff8WARYDFgQWBRYGFgcWCRYLFgwWDRYPFhAWTxZRFlMWVRZXFlgWWRZaFlsWXRZfFmAWYRZjFmQWZxamFqgWqhasFq4WrxawFrEWsha0FrYWtxa4FroWuxb6FvwW/hcAFwIXAxcEFwUXBhcIFwoXCxcMFw4XDxdOF1AXUhdUF1YXVxdYF1kXWhdcF14XXxdgF2IXYxdsF3oXhxeVF6IXtRfMF94YKRhMGGwYjBiOGJAYkhiUGJYYlxiYGJoYmxidGJ4YoBiiGKMYpBimGKcYrBi5GL4YwBjCGMcYyRjLGM0Y8hkWGT0ZYRljGWUZZxlpGWsZbRluGXAZfRmOGZAZkhmUGZYZmBmaGZwZnhmvGbEZsxm1GbcZuRm7Gb0ZvxnBGgAaAhoEGgYaCBoJGgoaCxoMGg4aEBoRGhIaFBoVGlQaVhpYGloaXBpdGl4aXxpgGmIaZBplGmYaaBppGqgaqhqsGq4asBqxGrIasxq0GrYauBq5GroavBq9GsoayxrMGs4bDRsPGxEbExsVGxYbFxsYGxkbGxsdGx4bHxshGyIbYRtjG2UbZxtpG2obaxtsG20bbxtxG3Ibcxt1G3YbtRu3G7kbuxu9G74bvxvAG8EbwxvFG8YbxxvJG8ocCRwLHA0cDxwRHBIcExwUHBUcFxwZHBocGxwdHB4cXRxfHGEcYxxlHGYcZxxoHGkcaxxtHG4cbxxxHHIclxy7HOIdBh0IHQodDB0OHRAdEh0THRUdIh0xHTMdNR03HTkdOx09HT8dTh1QHVIdVB1WHVgdWh1cHV4dnR2fHaEdox2lHaYdpx2oHakdqx2tHa4drx2xHbId8R3zHfUd9x35Hfod+x38Hf0d/x4BHgIeAx4FHgYeRR5HHkkeSx5NHk4eTx5QHlEeUx5VHlYeVx5ZHloemR6bHp0enx6hHqIeox6kHqUepx6pHqoeqx6tHq4esR7wHvIe9B72Hvge+R76Hvse/B7+HwAfAR8CHwQfBR9EH0YfSB9KH0wfTR9OH08fUB9SH1QfVR9WH1gfWR+YH5ofnB+eH6AfoR+iH6MfpB+mH6gfqR+qH6wfrR/4IBsgOyBbIF0gXyBhIGMgZSBmIGcgaSBqIGwgbSBvIHEgciBzIHUgdiB7IIggjSCPIJEgliCYIJognCDBIOUhDCEwITIhNCE2ITghOiE8IT0hPyFMIV0hXyFhIWMhZSFnIWkhayFtIX4hgCGCIYQhhiGIIYohjCGOIZAhzyHRIdMh1SHXIdgh2SHaIdsh3SHfIeAh4SHjIeQiIyIlIiciKSIrIiwiLSIuIi8iMSIzIjQiNSI3IjgidyJ5InsifSJ/IoAigSKCIoMihSKHIogiiSKLIowimSKaIpsinSLcIt4i4CLiIuQi5SLmIuci6CLqIuwi7SLuIvAi8SMwIzIjNCM2IzgjOSM6IzsjPCM+I0AjQSNCI0QjRSOEI4YjiCOKI4wjjSOOI48jkCOSI5QjlSOWI5gjmSPYI9oj3CPeI+Aj4SPiI+Mj5CPmI+gj6SPqI+wj7SQsJC4kMCQyJDQkNSQ2JDckOCQ6JDwkPSQ+JEAkQSRmJIoksSTVJNck2STbJN0k3yThJOIk5CTxJQAlAiUEJQYlCCUKJQwlDiUdJR8lISUjJSUlJyUpJSslLSVsJW4lcCVyJXQldSV2JXcleCV6JXwlfSV+JYAlgSXAJcIlxCXGJcglySXKJcslzCXOJdAl0SXSJdQl1SYUJhYmGCYaJhwmHSYeJh8mICYiJiQmJSYmJigmKSZoJmombCZuJnAmcSZyJnMmdCZ2JngmeSZ6JnwmfSa8Jr4mwCbCJsQmxSbGJscmyCbKJswmzSbOJtAm0ScQJxInFCcWJxgnGScaJxsnHCceJyAnISciJyQnJSdkJ2YnaCdqJ2wnbSduJ28ncCdyJ3QndSd2J3gneSeEJ40njieQJ5knpCezJ74nzCfhJ/UoDCgeKCsoLCgtKC8oPCg9KD4oQChNKE4oTyhRKFooaSh2KIUolyirKMIo1CjdKN4o4CjtKO4o7yjxKPIo+ykFKQwAAAAAAAACAgAAAAAAAAVyAAAAAAAAAAAAAAAAAAApFA== + + Diary/CoreData/Diary.xcdatamodeld/Diary v2.xcdatamodel + YnBsaXN0MDDUAAEAAgADAAQABQAGAAcAClgkdmVyc2lvblkkYXJjaGl2ZXJUJHRvcFgkb2JqZWN0 +cxIAAYagXxAPTlNLZXllZEFyY2hpdmVy0QAIAAlUcm9vdIABrxDFAAsADAAbADcAOAA5AEEAQgBdAF4AXwBlAGYAcgCIAIkAigCLAIwAjQCOAI8AkACRAKoArQC0ALoAyQDYANsA6gD5APwAXAEMARsBHwEjATIBOAE5AUEBUAFZAWcBaAFpAWoBawFsAYEBggGKAYsBjAGYAawBrQGuAa8BsAGxAbIBswG0AcMB0gHhAeUB9AIDAgQCEwIiAjECPQJPAlACUQJSAlMCVAJVAlYCZQJ0AoMCkgKTAqICsQLAAsgC3QLeAuYC8gMGAxUDJAMzAzcDRgNVA2QDcwOCA44DoAOvA74DzQPcA+sD+gQJBB4EHwQnBDMERwRWBGUEdAR4BIcElgSlBLQEwwTPBOEE8AT/BQ4FHQUsBTsFSgVfBWAFaAV0BYgFlwWmBbUFuQXIBdcF5gX1BgQGEAYiBjEGQAZPBl4GbQZ8BosGoAahBqkGtQbJBtgG5wb2BvoHCQcYBycHNgdFB1EHYwdyB4EHkAefB6AHrwe+B80HzgfRB9oH3gfiB+YH7gfxB/UH9lUkbnVsbNcADQAOAA8AEAARABIAEwAUABUAFgAXABgAFwAaXxAPX3hkX3Jvb3RQYWNrYWdlViRjbGFzc1xfeGRfY29tbWVudHNfEBBfeGRfbW9kZWxNYW5hZ2VyXxAVX2NvbmZpZ3VyYXRpb25zQnlOYW1lXV94ZF9tb2RlbE5hbWVfEBdfbW9kZWxWZXJzaW9uSWRlbnRpZmllcoACgMSAwYAAgMKAAIDD3gAcAB0AHgAfACAAIQAiAA4AIwAkACUAJgAnACgAKQAqACsACQApABcALwAwADEAMgAzACkAKQAXXxAcWERCdWNrZXRGb3JDbGFzc2Vzd2FzRW5jb2RlZF8QGlhEQnVja2V0Rm9yUGFja2FnZXNzdG9yYWdlXxAcWERCdWNrZXRGb3JJbnRlcmZhY2Vzc3RvcmFnZV8QD194ZF9vd25pbmdNb2RlbF8QHVhEQnVja2V0Rm9yUGFja2FnZXN3YXNFbmNvZGVkVl9vd25lcl8QG1hEQnVja2V0Rm9yRGF0YVR5cGVzc3RvcmFnZVtfdmlzaWJpbGl0eV8QGVhEQnVja2V0Rm9yQ2xhc3Nlc3N0b3JhZ2VVX25hbWVfEB9YREJ1Y2tldEZvckludGVyZmFjZXN3YXNFbmNvZGVkXxAeWERCdWNrZXRGb3JEYXRhVHlwZXN3YXNFbmNvZGVkXxAQX3VuaXF1ZUVsZW1lbnRJRIAEgL+AvYABgASAAIC+gMAQAIAFgAOABIAEgABQU1lFU9MAOgA7AA4APAA+AEBXTlMua2V5c1pOUy5vYmplY3RzoQA9gAahAD+AB4AlVURpYXJ53xAQAEMARABFAEYAIQBHAEgAIwBJAEoADgAlAEsATAAoAE0ATgBPACkAKQAUAFMAVAAxACkATgBXAD0ATgBaAFsAXF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZV8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc2R1cGxpY2F0ZXNfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zc3RvcmFnZVtfaXNBYnN0cmFjdIAJgCyABIAEgAKACoC6gASACYC8gAaACYC7gAgIEnLku4VXb3JkZXJlZNMAOgA7AA4AYABiAEChAGGAC6EAY4AMgCVeWERfUFN0ZXJlb3R5cGXZACEAJQBnAA4AKABoACMATQBpAD8AYQBOAG0AFwApADEAXABxXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgAeAC4AJgCuAAIAECIAN0wA6ADsADgBzAH0AQKkAdAB1AHYAdwB4AHkAegB7AHyADoAPgBCAEYASgBOAFIAVgBapAH4AfwCAAIEAggC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AAIAECICx0wA6ADsADgdSB1oAQKcCPwJAAkECQgJDAkQCRYBNgE6AT4BQgFGAUoBTpwdbB1wHXQdeB18HYAdhgLKAs4C0gLWAt4C4gLmAJd8QDwCSAJMAlAAhAJUAlgCXACMAmAAOACUAmQCaACgAmwAXABcAFwanAFwAXABcADEAXACkAj8AXABcABcAXIAAgACAAICwCAgICIAagE0ICIAACN8QDwCSAJMAlAAhAJUAlgCXACMAmAAOACUAmQCaACgAmwAXAP4AFwanAFwAXABcADEAXACkAkAAXABcABcAXIAAgCKAAICwCAgICIAagE4ICIAACN8QDwCSAJMAlAAhAJUAlgCXACMAmAAOACUAmQCaACgAmwAXABcAFwanAFwAXABcADEAXACkAkEAXABcABcAXIAAgACAAICwCAgICIAagE8ICIAACN8QDwCSAJMAlAAhAJUAlgCXACMAmAAOACUAmQCaACgAmwAXB5IAFwanAFwAXABcADEAXACkAkIAXABcABcAXIAAgLaAAICwCAgICIAagFAICIAACBEDhN8QDwCSAJMAlAAhAJUAlgCXACMAmAAOACUAmQCaACgAmwAXABcAFwanAFwAXABcADEAXACkAkMAXABcABcAXIAAgACAAICwCAgICIAagFEICIAACN8QDwCSAJMAlAAhAJUAlgCXACMAmAAOACUAmQCaACgAmwAXABcAFwanAFwAXABcADEAXACkAkQAXABcABcAXIAAgACAAICwCAgICIAagFIICIAACN8QDwCSAJMAlAAhAJUAlgCXACMAmAAOACUAmQCaACgAmwAXABcAFwanAFwAXABcADEAXACkAkUAXABcABcAXIAAgACAAICwCAgICIAagFMICIAACFpkdXBsaWNhdGVz0gA7AA4HzwCsoIAZ0gCuAK8H0gfTWlhEUE1FbnRpdHmnB9QH1QfWB9cH2AfZALNaWERQTUVudGl0eV1YRFVNTENsYXNzSW1wXxASWERVTUxDbGFzc2lmaWVySW1wXxARWERVTUxOYW1lc3BhY2VJbXBfEBRYRFVNTE5hbWVkRWxlbWVudEltcF8QD1hEVU1MRWxlbWVudEltcNMAOgA7AA4H2wfcAECgoIAl0wA6ADsADgffB+AAQKCggCXTADoAOwAOB+MH5ABAoKCAJdIArgCvB+cH6F5YRE1vZGVsUGFja2FnZaYH6QfqB+sH7AftALNeWERNb2RlbFBhY2thZ2VfEA9YRFVNTFBhY2thZ2VJbXBfEBFYRFVNTE5hbWVzcGFjZUltcF8QFFhEVU1MTmFtZWRFbGVtZW50SW1wXxAPWERVTUxFbGVtZW50SW1w0gA7AA4H7wCsoIAZ0wA6ADsADgfyB/MAQKCggCVQ0gCuAK8H9wf4WVhEUE1Nb2RlbKMH9wf5ALNXWERNb2RlbAAIABkAIgAsADEAOgA/AFEAVgBbAF0B6gHwAg0CHwImAjMCRgJeAmwChgKIAooCjAKOApACkgKUAs0C7AMJAygDOgNaA2EDfwOLA6cDrQPPA/AEAwQFBAcECQQLBA0EDwQRBBMEFQQXBBkEGwQdBB8EIAQkBDEEOQREBEcESQRMBE4EUARWBJkEvQThBQQFKwVLBXIFmQW5Bd0GAQYNBg8GEQYTBhUGFwYZBhsGHQYfBiEGIwYlBicGKQYqBi8GNwZEBkcGSQZMBk4GUAZfBoQGqAbPBvMG9Qb3BvkG+wb9Bv8HAAcCBw8HIgckByYHKAcqBywHLgcwBzIHNAdHB0kHSwdNB08HUQdTB1UHVwdZB1sHcQeEB6AHvQfZB+0H/wgVCC4IbQhzCHwIiQiVCJ8IqQi0CL8IzAjUCNYI2AjaCNwI3QjeCN8I4AjiCOQI5QjmCOgI6QjyCPMI9Qj+CQkJEgkhCSgJMAk5CUIJVQleCXEJiAmaCdkJ2wndCd8J4QniCeMJ5AnlCecJ6QnqCesJ7QnuCi0KLwoxCjMKNQo2CjcKOAo5CjsKPQo+Cj8KQQpCCksKTApOCo0KjwqRCpMKlQqWCpcKmAqZCpsKnQqeCp8KoQqiCuEK4wrlCucK6QrqCusK7ArtCu8K8QryCvMK9Qr2Cv8LAAsCC0ELQwtFC0cLSQtKC0sLTAtNC08LUQtSC1MLVQtWC1cLlguYC5oLnAueC58LoAuhC6ILpAumC6cLqAuqC6sLuAu5C7oLvAvFC9sL4gvvDC4MMAwyDDQMNgw3DDgMOQw6DDwMPgw/DEAMQgxDDFwMXgxgDGIMYwxlDHwMhQyTDKAMrgzDDNcM7g0ADT8NQQ1DDUUNRw1IDUkNSg1LDU0NTw1QDVENUw1UDV0Ncg2BDZYNpA25Dc0N5A32DgMODg4QDhIOFA4WDhgOIw4lDicOKQ4rDi0OLw40DjkOPg5EDkkOlA63DtcO9w75DvsO/Q7/DwEPAg8DDwUPBg8IDwkPCw8NDw4PDw8RDxIPGw8oDy0PLw8xDzYPOA86DzwPUQ9mD4sPrw/WD/oP/A/+EAAQAhAEEAYQBxAJEBYQJxApECsQLRAvEDEQMxA1EDcQSBBKEEwQThBQEFIQVBBWEFgQWhB4EJYQqRC9ENIQ7xEDERkRWBFaEVwRXhFgEWERYhFjEWQRZhFoEWkRahFsEW0RrBGuEbARshG0EbURthG3EbgRuhG8Eb0RvhHAEcESABICEgQSBhIIEgkSChILEgwSDhIQEhESEhIUEhUSIhIjEiQSJhJlEmcSaRJrEm0SbhJvEnAScRJzEnUSdhJ3EnkSehK5ErsSvRK/EsESwhLDEsQSxRLHEskSyhLLEs0SzhLPEw4TEBMSExQTFhMXExgTGRMaExwTHhMfEyATIhMjE2ITZBNmE2gTahNrE2wTbRNuE3ATchNzE3QTdhN3E7YTuBO6E7wTvhO/E8ATwRPCE8QTxhPHE8gTyhPLE/AUFBQ7FF8UYRRjFGUUZxRpFGsUbBRuFHsUihSMFI4UkBSSFJQUlhSYFKcUqRSrFK0UrxSxFLMUtRS3FNcVAhUcFTUVTxVvFZIV0RXTFdUV1xXZFdoV2xXcFd0V3xXhFeIV4xXlFeYWJRYnFikWKxYtFi4WLxYwFjEWMxY1FjYWNxY5FjoWeRZ7Fn0WfxaBFoIWgxaEFoUWhxaJFooWixaNFo4WzRbPFtEW0xbVFtYW1xbYFtkW2xbdFt4W3xbhFuIW5RckFyYXKBcqFywXLRcuFy8XMBcyFzQXNRc2FzgXORd4F3oXfBd+F4AXgReCF4MXhBeGF4gXiReKF4wXjRfMF84X0BfSF9QX1RfWF9cX2BfaF9wX3RfeF+AX4RfqF/gYBRgTGCAYMxhKGFwYpxjKGOoZChkMGQ4ZEBkSGRQZFRkWGRgZGRkbGRwZHhkgGSEZIhkkGSUZKhk3GTwZPhlAGUUZRxlJGUsZcBmUGbsZ3xnhGeMZ5RnnGekZ6xnsGe4Z+xoMGg4aEBoSGhQaFhoYGhoaHBotGi8aMRozGjUaNxo5GjsaPRo/Gn4agBqCGoQahhqHGogaiRqKGowajhqPGpAakhqTGtIa1BrWGtga2hrbGtwa3RreGuAa4hrjGuQa5hrnGyYbKBsqGywbLhsvGzAbMRsyGzQbNhs3GzgbOhs7G0gbSRtKG0wbixuNG48bkRuTG5QblRuWG5cbmRubG5wbnRufG6Ab3xvhG+Mb5RvnG+gb6RvqG+sb7RvvG/Ab8RvzG/QcMxw1HDccORw7HDwcPRw+HD8cQRxDHEQcRRxHHEgchxyJHIscjRyPHJAckRySHJMclRyXHJgcmRybHJwc2xzdHN8c4RzjHOQc5RzmHOcc6RzrHOwc7RzvHPAdFR05HWAdhB2GHYgdih2MHY4dkB2RHZMdoB2vHbEdsx21HbcduR27Hb0dzB3OHdAd0h3UHdYd2B3aHdweGx4dHh8eIR4jHiQeJR4mHiceKR4rHiweLR4vHjAebx5xHnMedR53HngeeR56HnsefR5/HoAegR6DHoQewx7FHsceyR7LHswezR7OHs8e0R7THtQe1R7XHtgfFx8ZHxsfHR8fHyAfIR8iHyMfJR8nHygfKR8rHywfax9tH28fcR9zH3QfdR92H3cfeR97H3wffR9/H4Afvx/BH8MfxR/HH8gfyR/KH8sfzR/PH9Af0R/TH9QgEyAVIBcgGSAbIBwgHSAeIB8gISAjICQgJSAnICggcyCWILYg1iDYINog3CDeIOAg4SDiIOQg5SDnIOgg6iDsIO0g7iDwIPEg+iEHIQwhDiEQIRUhFyEZIRshQCFkIYshryGxIbMhtSG3IbkhuyG8Ib4hyyHcId4h4CHiIeQh5iHoIeoh7CH9If8iASIDIgUiByIJIgsiDSIPIk4iUCJSIlQiViJXIlgiWSJaIlwiXiJfImAiYiJjIqIipCKmIqgiqiKrIqwirSKuIrAisiKzIrQitiK3IvYi+CL6Ivwi/iL/IwAjASMCIwQjBiMHIwgjCiMLIxgjGSMaIxwjWyNdI18jYSNjI2QjZSNmI2cjaSNrI2wjbSNvI3AjryOxI7MjtSO3I7gjuSO6I7sjvSO/I8AjwSPDI8QkAyQFJAckCSQLJAwkDSQOJA8kESQTJBQkFSQXJBgkVyRZJFskXSRfJGAkYSRiJGMkZSRnJGgkaSRrJGwkqyStJK8ksSSzJLQktSS2JLckuSS7JLwkvSS/JMAk5SUJJTAlVCVWJVglWiVcJV4lYCVhJWMlcCV/JYElgyWFJYcliSWLJY0lnCWeJaAloiWkJaYlqCWqJawl6yXtJe8l8SXzJfQl9SX2Jfc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 + + + + + body + + + \ No newline at end of file diff --git a/Diary/Network/CacheManager.swift b/Diary/Network/CacheManager.swift new file mode 100644 index 000000000..f3fd95570 --- /dev/null +++ b/Diary/Network/CacheManager.swift @@ -0,0 +1,14 @@ +// +// CacheManager.swift +// Diary +// +// Created by hoon, karen on 2023/09/15. +// + +import UIKit + +class CacheManager { + static let shared = NSCache() + + private init() {} +} diff --git a/Diary/View/DiaryCell.swift b/Diary/View/DiaryCell.swift index ebeee84c1..93e2abaf7 100644 --- a/Diary/View/DiaryCell.swift +++ b/Diary/View/DiaryCell.swift @@ -18,14 +18,25 @@ final class DiaryCell: UITableViewCell { private let dateLabel = { let label = UILabel() label.font = .preferredFont(forTextStyle: .body) + label.setContentHuggingPriority(.defaultHigh, for: .vertical) label.setContentHuggingPriority(.defaultHigh, for: .horizontal) return label }() + private let weatherIconImageView = { + let imageView = UIImageView() + imageView.setContentHuggingPriority(.defaultHigh, for: .horizontal) + imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + imageView.translatesAutoresizingMaskIntoConstraints = false + + return imageView + }() + private let previewLabel = { let label = UILabel() label.font = .preferredFont(forTextStyle: .caption1) + label.setContentHuggingPriority(.defaultLow, for: .horizontal) label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) return label @@ -57,10 +68,41 @@ final class DiaryCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } - func configureCell(title: String?, date: String, preview: String?) { + func configureCell(title: String?, date: String, preview: String?, icon: String?) { titleLabel.text = title dateLabel.text = date previewLabel.text = preview + + guard let icon else { + return + } + + guard let cache = CacheManager.shared.object(forKey: NSString(string: icon)) else { + fetchIconImage(id: icon) + return + } + + weatherIconImageView.image = cache + } + + private func fetchIconImage(id: String) { + let icon = WeatherAPI.weatherIcon(id: id) + + NetworkManager.shared.fetchData(API: icon) { [weak self] result in + switch result { + case .success(let data): + guard let image = UIImage(data: data) else { + return + } + + DispatchQueue.main.async { + CacheManager.shared.setObject(image, forKey: NSString(string: id)) + self?.weatherIconImageView.image = image + } + case .failure(let error): + print(error.description) + } + } } private func configure() { @@ -85,6 +127,7 @@ final class DiaryCell: UITableViewCell { private func configureDescriptionStackView() { descriptionStackView.addArrangedSubview(dateLabel) + descriptionStackView.addArrangedSubview(weatherIconImageView) descriptionStackView.addArrangedSubview(previewLabel) } @@ -100,5 +143,10 @@ final class DiaryCell: UITableViewCell { contentStackView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), contentStackView.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor) ]) + + NSLayoutConstraint.activate([ + weatherIconImageView.heightAnchor.constraint(equalTo: dateLabel.heightAnchor), + weatherIconImageView.widthAnchor.constraint(equalTo: weatherIconImageView.heightAnchor) + ]) } } From 40638b01650b47047e97693eb48a24fbd7720a07 Mon Sep 17 00:00:00 2001 From: Hoon94 Date: Fri, 15 Sep 2023 03:09:10 +0900 Subject: [PATCH 06/15] =?UTF-8?q?=F0=9F=92=A5refactor:=20Cell=20=EC=9E=AC?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EB=B0=8F=20=EB=82=A0=EC=94=A8=20icon=20au?= =?UTF-8?q?tolayout=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Diary/View/DiaryCell.swift | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Diary/View/DiaryCell.swift b/Diary/View/DiaryCell.swift index 93e2abaf7..0488d4182 100644 --- a/Diary/View/DiaryCell.swift +++ b/Diary/View/DiaryCell.swift @@ -18,7 +18,7 @@ final class DiaryCell: UITableViewCell { private let dateLabel = { let label = UILabel() label.font = .preferredFont(forTextStyle: .body) - label.setContentHuggingPriority(.defaultHigh, for: .vertical) + label.setContentHuggingPriority(.required, for: .vertical) label.setContentHuggingPriority(.defaultHigh, for: .horizontal) return label @@ -26,9 +26,7 @@ final class DiaryCell: UITableViewCell { private let weatherIconImageView = { let imageView = UIImageView() - imageView.setContentHuggingPriority(.defaultHigh, for: .horizontal) - imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) - imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit return imageView }() @@ -68,6 +66,10 @@ final class DiaryCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } + override func prepareForReuse() { + weatherIconImageView.image = nil + } + func configureCell(title: String?, date: String, preview: String?, icon: String?) { titleLabel.text = title dateLabel.text = date @@ -141,10 +143,7 @@ final class DiaryCell: UITableViewCell { contentStackView.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor), contentStackView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), contentStackView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), - contentStackView.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor) - ]) - - NSLayoutConstraint.activate([ + contentStackView.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor), weatherIconImageView.heightAnchor.constraint(equalTo: dateLabel.heightAnchor), weatherIconImageView.widthAnchor.constraint(equalTo: weatherIconImageView.heightAnchor) ]) From 1dd316cee5778a4cfc52b5484a67fcb22753cee6 Mon Sep 17 00:00:00 2001 From: karen Date: Fri, 15 Sep 2023 17:45:14 +0900 Subject: [PATCH 07/15] =?UTF-8?q?=E2=9C=B4=EF=B8=8F=20feat:=20NetworkAPI?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DiaryDetailViewController.swift | 19 ++- Diary/Network/NetworkManager.swift | 130 ++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/Diary/Controller/DiaryDetailViewController.swift b/Diary/Controller/DiaryDetailViewController.swift index 36cdc6527..f9672d2ed 100644 --- a/Diary/Controller/DiaryDetailViewController.swift +++ b/Diary/Controller/DiaryDetailViewController.swift @@ -29,7 +29,8 @@ final class DiaryDetailViewController: UIViewController, Shareable { override func viewDidLoad() { super.viewDidLoad() configure() - fetchWeather() +// fetchWeather() + fetchWeather2() } override func viewWillDisappear(_ animated: Bool) { @@ -211,4 +212,20 @@ private extension DiaryDetailViewController { } } } + + func fetchWeather2() { + WeatherAPI2.Users().request { [weak self] result in + switch result { + case .success(let data): + guard let currentWeather = data.weather.first else { + return + } + + self?.diary.main = currentWeather.main + self?.diary.icon = currentWeather.icon + case .failure(let error): + print(error) + } + } + } } diff --git a/Diary/Network/NetworkManager.swift b/Diary/Network/NetworkManager.swift index 517c1b16c..1b5614cf9 100644 --- a/Diary/Network/NetworkManager.swift +++ b/Diary/Network/NetworkManager.swift @@ -7,6 +7,136 @@ import Foundation +enum NetworkAPI {} + +extension NetworkAPI { + struct URLInfo { + let scheme: String + let host: String + let port: Int? + let path: String + let query: [String: String]? + + init(scheme: String = "https", host: String, port: Int? = nil, path: String, query: [String: String]? = nil) { + self.scheme = scheme + self.host = host + self.port = port + self.path = path + self.query = query + } + } +} + +extension NetworkAPI.URLInfo { + var url: URL? { + var components = URLComponents() + components.scheme = scheme + components.host = host + components.port = port + components.path = path + components.queryItems = query?.compactMap { URLQueryItem(name: $0.key, value: $0.value) } + + return components.url + } +} + +extension NetworkAPI.URLInfo { + static func weatherAPI(path: String) -> Self { + Self.init(host: "api.openweathermap.org", path: path, query: ["lat": "44.34", "lon": "10.99"]) + } +} + +extension NetworkAPI { + enum Method: String { + case get = "GET" + case post = "POST" + } +} + +extension NetworkAPI { + struct RequestInfo { + var method: Method + var headers: [String: String]? + var parameters: T? + + init(method: Method, headers: [String: String]? = nil, parameters: T? = nil) { + self.method = method + self.headers = headers + self.parameters = parameters + } + } +} + +extension NetworkAPI.RequestInfo { + func requests(url: URL) -> URLRequest { + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + request.httpBody = parameters.flatMap { try? JSONEncoder().encode($0) } + headers.map { + request.allHTTPHeaderFields?.merge($0) { lhs, _ in lhs } + } + + return request + } +} + +protocol NetworkAPIDefinition { + typealias URLInfo = NetworkAPI.URLInfo + typealias RequestInfo = NetworkAPI.RequestInfo + + associatedtype Parameter: Encodable + associatedtype Response: Decodable + + var urlInfo: URLInfo { get } + var requestInfo: RequestInfo { get } +} + +extension NetworkAPIDefinition { + func request(completion: @escaping ((Result) -> Void)) { + guard let url = urlInfo.url else { + return + } + + let request = requestInfo.requests(url: url) + let config = URLSessionConfiguration.default + let session = URLSession(configuration: config) + + let dataTask = session.dataTask(with: request) { data, response, error in + guard let data = data else { + return + } + + do { + let response = try JSONDecoder().decode(Response.self, from: data) + completion(.success(response)) + } catch { + completion(.failure(error)) + } + } + + dataTask.resume() + } +} + +struct EmptyParameter: Codable {} + +struct EmptyResponse: Codable {} + +enum WeatherAPI2 {} + +extension WeatherAPI2 { + struct Users: NetworkAPIDefinition { + let urlInfo: URLInfo + let requestInfo: RequestInfo = .init(method: .get) + + init() { + self.urlInfo = .weatherAPI(path: "/data/2.5/weather") + } + + typealias Response = Location + } +} + final class NetworkManager { static let shared = NetworkManager() From 5c597813d063c97fde0e6212643b2ca0c0012ddd Mon Sep 17 00:00:00 2001 From: karen Date: Fri, 15 Sep 2023 23:17:35 +0900 Subject: [PATCH 08/15] =?UTF-8?q?=F0=9F=92=A5refactor:=20NetworkAPI=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Diary.xcodeproj/project.pbxproj | 24 ++- .../DiaryDetailViewController.swift | 39 ++-- Diary/Network/Model/NetworkAPI.swift | 48 +++++ Diary/Network/Model/URLComponents.swift | 59 ++++++ Diary/Network/Model/WeatherAPI.swift | 38 +--- Diary/Network/NetworkAPIDefinition.swift | 48 +++++ Diary/Network/NetworkManager.swift | 176 ------------------ Diary/View/DiaryCell.swift | 9 +- 8 files changed, 195 insertions(+), 246 deletions(-) create mode 100644 Diary/Network/Model/NetworkAPI.swift create mode 100644 Diary/Network/Model/URLComponents.swift create mode 100644 Diary/Network/NetworkAPIDefinition.swift delete mode 100644 Diary/Network/NetworkManager.swift diff --git a/Diary.xcodeproj/project.pbxproj b/Diary.xcodeproj/project.pbxproj index 09ed58416..2c2ed1b88 100644 --- a/Diary.xcodeproj/project.pbxproj +++ b/Diary.xcodeproj/project.pbxproj @@ -15,11 +15,11 @@ 784F30CB2AB2FE87009A895B /* DecodingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784F30CA2AB2FE87009A895B /* DecodingManager.swift */; }; 784F30CE2AB30072009A895B /* DecodingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784F30CD2AB30072009A895B /* DecodingError.swift */; }; 78D13D952AB338B000A0D28E /* WeatherInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 78D13D942AB338B000A0D28E /* WeatherInfo.plist */; }; - 78D13D972AB33B6A00A0D28E /* WeatherAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78D13D962AB33B6A00A0D28E /* WeatherAPI.swift */; }; + 78D13D972AB33B6A00A0D28E /* URLComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78D13D962AB33B6A00A0D28E /* URLComponents.swift */; }; 92FB9B8365F3B707B891705F /* Pods_Diary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EEBF20D2433AEB2D78258668 /* Pods_Diary.framework */; }; 9C1529112AAE140900F3203E /* Shareable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C1529102AAE140800F3203E /* Shareable.swift */; }; 9C184BBF2AA21892003863E7 /* CellIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C184BBE2AA21892003863E7 /* CellIdentifier.swift */; }; - 9C243B302AB307E1009DBF80 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C243B2F2AB307E1009DBF80 /* NetworkManager.swift */; }; + 9C243B302AB307E1009DBF80 /* NetworkAPIDefinition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C243B2F2AB307E1009DBF80 /* NetworkAPIDefinition.swift */; }; 9C243B322AB308BC009DBF80 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C243B312AB308BC009DBF80 /* NetworkError.swift */; }; 9C4621EC2AB06890004ED11A /* PersistentContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4621EB2AB06890004ED11A /* PersistentContainer.swift */; }; 9C4621EE2AB0739F004ED11A /* Array+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4621ED2AB0739F004ED11A /* Array+.swift */; }; @@ -28,6 +28,8 @@ 9C5C42F22AB36D2D004FF07A /* MappingFile.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 9C5C42F12AB36D2D004FF07A /* MappingFile.xcmappingmodel */; }; 9C5C42F42AB36F6B004FF07A /* CacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5C42F32AB36F6B004FF07A /* CacheManager.swift */; }; 9C75C4CA2A9F448400C5D1CB /* DiaryDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C75C4C92A9F448400C5D1CB /* DiaryDetailViewController.swift */; }; + 9CFDA0852AB499D300B478E8 /* NetworkAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CFDA0842AB499D300B478E8 /* NetworkAPI.swift */; }; + 9CFDA0872AB49A1800B478E8 /* WeatherAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CFDA0862AB49A1800B478E8 /* WeatherAPI.swift */; }; C739AE25284DF28600741E8F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE24284DF28600741E8F /* AppDelegate.swift */; }; C739AE27284DF28600741E8F /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE26284DF28600741E8F /* SceneDelegate.swift */; }; C739AE29284DF28600741E8F /* DiaryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE28284DF28600741E8F /* DiaryViewController.swift */; }; @@ -46,10 +48,10 @@ 784F30CA2AB2FE87009A895B /* DecodingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingManager.swift; sourceTree = ""; }; 784F30CD2AB30072009A895B /* DecodingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingError.swift; sourceTree = ""; }; 78D13D942AB338B000A0D28E /* WeatherInfo.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = WeatherInfo.plist; sourceTree = ""; }; - 78D13D962AB33B6A00A0D28E /* WeatherAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherAPI.swift; sourceTree = ""; }; + 78D13D962AB33B6A00A0D28E /* URLComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLComponents.swift; sourceTree = ""; }; 9C1529102AAE140800F3203E /* Shareable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shareable.swift; sourceTree = ""; }; 9C184BBE2AA21892003863E7 /* CellIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellIdentifier.swift; sourceTree = ""; }; - 9C243B2F2AB307E1009DBF80 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; + 9C243B2F2AB307E1009DBF80 /* NetworkAPIDefinition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAPIDefinition.swift; sourceTree = ""; }; 9C243B312AB308BC009DBF80 /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; 9C243B332AB34C0F009DBF80 /* Diary v2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Diary v2.xcdatamodel"; sourceTree = ""; }; 9C4621EB2AB06890004ED11A /* PersistentContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentContainer.swift; sourceTree = ""; }; @@ -59,6 +61,8 @@ 9C5C42F12AB36D2D004FF07A /* MappingFile.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = MappingFile.xcmappingmodel; sourceTree = ""; }; 9C5C42F32AB36F6B004FF07A /* CacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManager.swift; sourceTree = ""; }; 9C75C4C92A9F448400C5D1CB /* DiaryDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryDetailViewController.swift; sourceTree = ""; }; + 9CFDA0842AB499D300B478E8 /* NetworkAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAPI.swift; sourceTree = ""; }; + 9CFDA0862AB49A1800B478E8 /* WeatherAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherAPI.swift; sourceTree = ""; }; A06CFFD4E1642CCF7B5F0BAC /* Pods-Diary.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Diary.debug.xcconfig"; path = "Target Support Files/Pods-Diary/Pods-Diary.debug.xcconfig"; sourceTree = ""; }; C739AE21284DF28600741E8F /* Diary.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Diary.app; sourceTree = BUILT_PRODUCTS_DIR; }; C739AE24284DF28600741E8F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -148,7 +152,7 @@ 784F30C72AB2F7F2009A895B /* Model */, 784F30CC2AB3004E009A895B /* Error */, 784F30CA2AB2FE87009A895B /* DecodingManager.swift */, - 9C243B2F2AB307E1009DBF80 /* NetworkManager.swift */, + 9C243B2F2AB307E1009DBF80 /* NetworkAPIDefinition.swift */, 9C5C42F32AB36F6B004FF07A /* CacheManager.swift */, ); path = Network; @@ -158,7 +162,9 @@ isa = PBXGroup; children = ( 784F30C82AB2F88F009A895B /* Location.swift */, - 78D13D962AB33B6A00A0D28E /* WeatherAPI.swift */, + 78D13D962AB33B6A00A0D28E /* URLComponents.swift */, + 9CFDA0842AB499D300B478E8 /* NetworkAPI.swift */, + 9CFDA0862AB49A1800B478E8 /* WeatherAPI.swift */, ); path = Model; sourceTree = ""; @@ -362,7 +368,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9C243B302AB307E1009DBF80 /* NetworkManager.swift in Sources */, + 9C243B302AB307E1009DBF80 /* NetworkAPIDefinition.swift in Sources */, C739AE29284DF28600741E8F /* DiaryViewController.swift in Sources */, 9C1529112AAE140900F3203E /* Shareable.swift in Sources */, 78274DA12A9E3E8E00AD4F50 /* DiaryModel.swift in Sources */, @@ -380,8 +386,10 @@ 78274D9A2A9E26DF00AD4F50 /* DiaryCell.swift in Sources */, C739AE2F284DF28600741E8F /* Diary.xcdatamodeld in Sources */, 9C5C42F22AB36D2D004FF07A /* MappingFile.xcmappingmodel in Sources */, + 9CFDA0852AB499D300B478E8 /* NetworkAPI.swift in Sources */, 9C5A1DD92AAB707A00162C98 /* CoreDataManager.swift in Sources */, - 78D13D972AB33B6A00A0D28E /* WeatherAPI.swift in Sources */, + 78D13D972AB33B6A00A0D28E /* URLComponents.swift in Sources */, + 9CFDA0872AB49A1800B478E8 /* WeatherAPI.swift in Sources */, 9C4621EE2AB0739F004ED11A /* Array+.swift in Sources */, 9C243B322AB308BC009DBF80 /* NetworkError.swift in Sources */, ); diff --git a/Diary/Controller/DiaryDetailViewController.swift b/Diary/Controller/DiaryDetailViewController.swift index f9672d2ed..750588457 100644 --- a/Diary/Controller/DiaryDetailViewController.swift +++ b/Diary/Controller/DiaryDetailViewController.swift @@ -29,8 +29,7 @@ final class DiaryDetailViewController: UIViewController, Shareable { override func viewDidLoad() { super.viewDidLoad() configure() -// fetchWeather() - fetchWeather2() + fetchWeather() } override func viewWillDisappear(_ animated: Bool) { @@ -190,41 +189,25 @@ private extension DiaryDetailViewController { return } - let weather = WeatherAPI.weatherData(latitude: latitude, longitude: longitude) - - NetworkManager.shared.fetchData(API: weather) { [weak self] result in + WeatherAPI.Users( + host: HostName.localWeather.address, + path: Path.localWeather.description, + query: Query.localWeather(latitude: latitude, longitude: longitude).parameters + ).request { [weak self] result in switch result { case .success(let data): - do { - let decodedData: Location = try DecodingManager.decodeData(from: data) - - guard let currentWeather = decodedData.weather.first else { - return - } - - self?.diary.main = currentWeather.main - self?.diary.icon = currentWeather.icon - } catch { - print(error) + guard let decodedData: Location = try? DecodingManager.decodeData(from: data) else { + return } - case .failure(let error): - print(error.description) - } - } - } - - func fetchWeather2() { - WeatherAPI2.Users().request { [weak self] result in - switch result { - case .success(let data): - guard let currentWeather = data.weather.first else { + + guard let currentWeather = decodedData.weather.first else { return } self?.diary.main = currentWeather.main self?.diary.icon = currentWeather.icon case .failure(let error): - print(error) + print(error.description) } } } diff --git a/Diary/Network/Model/NetworkAPI.swift b/Diary/Network/Model/NetworkAPI.swift new file mode 100644 index 000000000..71c723258 --- /dev/null +++ b/Diary/Network/Model/NetworkAPI.swift @@ -0,0 +1,48 @@ +// +// NetworkAPI.swift +// Diary +// +// Created by hoon, karen on 2023/09/15. +// + +import Foundation + +enum NetworkAPI {} + +extension NetworkAPI { + struct URLInfo { + let scheme: String + let host: String + let port: Int? + let path: String + let query: [String: String]? + + init(scheme: String = "https", host: String, port: Int? = nil, path: String, query: [String: String]? = nil) { + self.scheme = scheme + self.host = host + self.port = port + self.path = path + self.query = query + } + } +} + +extension NetworkAPI.URLInfo { + var url: URL? { + var components = URLComponents() + + components.scheme = scheme + components.host = host + components.port = port + components.path = path + components.queryItems = query?.compactMap { URLQueryItem(name: $0.key, value: $0.value) } + + return components.url + } +} + +extension NetworkAPI.URLInfo { + static func weatherAPI(host: String, path: String, query: [String: String]?) -> Self { + Self.init(host: host, path: path, query: query) + } +} diff --git a/Diary/Network/Model/URLComponents.swift b/Diary/Network/Model/URLComponents.swift new file mode 100644 index 000000000..e73506597 --- /dev/null +++ b/Diary/Network/Model/URLComponents.swift @@ -0,0 +1,59 @@ +// +// URLComponents.swift +// Diary +// +// Created by hoon, karen on 2023/09/14. +// + +import Foundation + +enum HostName { + case localWeather + case weatherIcon + + var address: String { + switch self { + case .localWeather: + return "api.openweathermap.org" + case .weatherIcon: + return "openweathermap.org" + } + } +} + +enum Path { + case localWeather + case weatherIcon(id: String) + + var description: String { + switch self { + case .localWeather: + return "/data/2.5/weather" + case .weatherIcon(let id): + return "/img/wn/\(id).png" + } + } +} + +enum Query { + case localWeather(latitude: Double, longitude: Double) + + var parameters: [String: String] { + switch self { + case .localWeather(let latitude, let longitude): + return ["lat": "\(latitude)", "lon": "\(longitude)", "appid": APIKey.weather] + } + } +} + +enum APIKey { + static var weather: String { + guard let file = Bundle.main.path(forResource: "WeatherInfo", ofType: "plist"), + let resource = NSDictionary(contentsOfFile: file), + let key = resource["API_KEY"] as? String else { + fatalError("⛔️ API KEY를 가져오는데 실패하였습니다.") + } + + return key + } +} diff --git a/Diary/Network/Model/WeatherAPI.swift b/Diary/Network/Model/WeatherAPI.swift index 2c2a0f007..e7a67f13d 100644 --- a/Diary/Network/Model/WeatherAPI.swift +++ b/Diary/Network/Model/WeatherAPI.swift @@ -2,39 +2,17 @@ // WeatherAPI.swift // Diary // -// Created by hoon, karen on 2023/09/14. +// Created by hoon, karen on 2023/09/15. // -import Foundation +enum WeatherAPI {} -protocol URLalbe { - var url: String? { get } -} - -enum WeatherAPI: URLalbe { - case weatherData(latitude: Double, longitude: Double) - case weatherIcon(id: String) - - var url: String? { - switch self { - case .weatherData(latitude: let latitude, longitude: let longitude): - guard let APIKey = WeatherAPI.APIKey else { - return nil - } - - return "https://api.openweathermap.org/data/2.5/weather?lat=\(latitude)&lon=\(longitude)&appid=\(APIKey)" - case .weatherIcon(id: let id): - return "https://openweathermap.org/img/wn/\(id).png" - } - } - - static var APIKey: String? { - guard let file = Bundle.main.path(forResource: "WeatherInfo", ofType: "plist"), - let resource = NSDictionary(contentsOfFile: file), - let key = resource["API_KEY"] as? String else { - fatalError("⛔️ API KEY를 가져오는데 실패하였습니다.") - } +extension WeatherAPI { + struct Users: NetworkAPIDefinition { + let urlInfo: URLInfo - return key + init(host: String, path: String, query: [String: String]? = nil) { + self.urlInfo = .weatherAPI(host: host, path: path, query: query) + } } } diff --git a/Diary/Network/NetworkAPIDefinition.swift b/Diary/Network/NetworkAPIDefinition.swift new file mode 100644 index 000000000..617840f0c --- /dev/null +++ b/Diary/Network/NetworkAPIDefinition.swift @@ -0,0 +1,48 @@ +// +// NetworkAPIDefinition.swift +// Diary +// +// Created by hoon, karen on 2023/09/14. +// + +import Foundation + +protocol NetworkAPIDefinition { + typealias URLInfo = NetworkAPI.URLInfo + + var urlInfo: URLInfo { get } +} + +extension NetworkAPIDefinition { + func request(completion: @escaping ((Result) -> Void)) { + guard let url = urlInfo.url else { + return + } + + let request = URLRequest(url: url) + let config = URLSessionConfiguration.default + let session = URLSession(configuration: config) + let dataTask = session.dataTask(with: request) { data, response, error in + guard error == nil else { + completion(.failure(.failureRequest)) + return + } + + guard let response = response, + let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + completion(.failure(.failureResponse)) + return + } + + guard let data = data else { + completion(.failure(.invalidDataType)) + return + } + + completion(.success(data)) + } + + dataTask.resume() + } +} diff --git a/Diary/Network/NetworkManager.swift b/Diary/Network/NetworkManager.swift deleted file mode 100644 index 1b5614cf9..000000000 --- a/Diary/Network/NetworkManager.swift +++ /dev/null @@ -1,176 +0,0 @@ -// -// NetworkManager.swift -// Diary -// -// Created by hoon, karen on 2023/09/14. -// - -import Foundation - -enum NetworkAPI {} - -extension NetworkAPI { - struct URLInfo { - let scheme: String - let host: String - let port: Int? - let path: String - let query: [String: String]? - - init(scheme: String = "https", host: String, port: Int? = nil, path: String, query: [String: String]? = nil) { - self.scheme = scheme - self.host = host - self.port = port - self.path = path - self.query = query - } - } -} - -extension NetworkAPI.URLInfo { - var url: URL? { - var components = URLComponents() - components.scheme = scheme - components.host = host - components.port = port - components.path = path - components.queryItems = query?.compactMap { URLQueryItem(name: $0.key, value: $0.value) } - - return components.url - } -} - -extension NetworkAPI.URLInfo { - static func weatherAPI(path: String) -> Self { - Self.init(host: "api.openweathermap.org", path: path, query: ["lat": "44.34", "lon": "10.99"]) - } -} - -extension NetworkAPI { - enum Method: String { - case get = "GET" - case post = "POST" - } -} - -extension NetworkAPI { - struct RequestInfo { - var method: Method - var headers: [String: String]? - var parameters: T? - - init(method: Method, headers: [String: String]? = nil, parameters: T? = nil) { - self.method = method - self.headers = headers - self.parameters = parameters - } - } -} - -extension NetworkAPI.RequestInfo { - func requests(url: URL) -> URLRequest { - var request = URLRequest(url: url) - request.httpMethod = method.rawValue - request.httpBody = parameters.flatMap { try? JSONEncoder().encode($0) } - headers.map { - request.allHTTPHeaderFields?.merge($0) { lhs, _ in lhs } - } - - return request - } -} - -protocol NetworkAPIDefinition { - typealias URLInfo = NetworkAPI.URLInfo - typealias RequestInfo = NetworkAPI.RequestInfo - - associatedtype Parameter: Encodable - associatedtype Response: Decodable - - var urlInfo: URLInfo { get } - var requestInfo: RequestInfo { get } -} - -extension NetworkAPIDefinition { - func request(completion: @escaping ((Result) -> Void)) { - guard let url = urlInfo.url else { - return - } - - let request = requestInfo.requests(url: url) - let config = URLSessionConfiguration.default - let session = URLSession(configuration: config) - - let dataTask = session.dataTask(with: request) { data, response, error in - guard let data = data else { - return - } - - do { - let response = try JSONDecoder().decode(Response.self, from: data) - completion(.success(response)) - } catch { - completion(.failure(error)) - } - } - - dataTask.resume() - } -} - -struct EmptyParameter: Codable {} - -struct EmptyResponse: Codable {} - -enum WeatherAPI2 {} - -extension WeatherAPI2 { - struct Users: NetworkAPIDefinition { - let urlInfo: URLInfo - let requestInfo: RequestInfo = .init(method: .get) - - init() { - self.urlInfo = .weatherAPI(path: "/data/2.5/weather") - } - - typealias Response = Location - } -} - -final class NetworkManager { - static let shared = NetworkManager() - - private init() {} - - func fetchData(API: T, completion: @escaping (Result) -> Void) { - guard let APIUrl = API.url, - let url = URL(string: APIUrl) else { - completion(.failure(.invalidURL)) - return - } - - let request = URLRequest(url: url) - let task = URLSession.shared.dataTask(with: request) { data, response, error in - guard error == nil else { - completion(.failure(.failureRequest)) - return - } - - guard let response = response, - let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - completion(.failure(.failureResponse)) - return - } - - guard let data = data else { - completion(.failure(.invalidDataType)) - return - } - - completion(.success(data)) - } - - task.resume() - } -} diff --git a/Diary/View/DiaryCell.swift b/Diary/View/DiaryCell.swift index 0488d4182..8d17519fb 100644 --- a/Diary/View/DiaryCell.swift +++ b/Diary/View/DiaryCell.swift @@ -88,15 +88,16 @@ final class DiaryCell: UITableViewCell { } private func fetchIconImage(id: String) { - let icon = WeatherAPI.weatherIcon(id: id) - - NetworkManager.shared.fetchData(API: icon) { [weak self] result in + WeatherAPI.Users( + host: HostName.weatherIcon.address, + path: Path.weatherIcon(id: id).description + ).request { [weak self] result in switch result { case .success(let data): guard let image = UIImage(data: data) else { return } - + DispatchQueue.main.async { CacheManager.shared.setObject(image, forKey: NSString(string: id)) self?.weatherIconImageView.image = image From 35ac31f570f1d574426724e8bfe97cc6f42757cc Mon Sep 17 00:00:00 2001 From: Hoon94 Date: Fri, 15 Sep 2023 23:39:16 +0900 Subject: [PATCH 09/15] =?UTF-8?q?=F0=9F=92=A5refactor:=20CacheStore?= =?UTF-8?q?=EB=A1=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Diary.xcodeproj/project.pbxproj | 8 ++++---- Diary/Network/{CacheManager.swift => CacheStore.swift} | 4 ++-- Diary/View/DiaryCell.swift | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) rename Diary/Network/{CacheManager.swift => CacheStore.swift} (77%) diff --git a/Diary.xcodeproj/project.pbxproj b/Diary.xcodeproj/project.pbxproj index 2c2ed1b88..d6bd7f41e 100644 --- a/Diary.xcodeproj/project.pbxproj +++ b/Diary.xcodeproj/project.pbxproj @@ -26,7 +26,7 @@ 9C5A1DD62AAA3F5B00162C98 /* UITextView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5A1DD52AAA3F5B00162C98 /* UITextView+.swift */; }; 9C5A1DD92AAB707A00162C98 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5A1DD82AAB707A00162C98 /* CoreDataManager.swift */; }; 9C5C42F22AB36D2D004FF07A /* MappingFile.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 9C5C42F12AB36D2D004FF07A /* MappingFile.xcmappingmodel */; }; - 9C5C42F42AB36F6B004FF07A /* CacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5C42F32AB36F6B004FF07A /* CacheManager.swift */; }; + 9C5C42F42AB36F6B004FF07A /* CacheStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5C42F32AB36F6B004FF07A /* CacheStore.swift */; }; 9C75C4CA2A9F448400C5D1CB /* DiaryDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C75C4C92A9F448400C5D1CB /* DiaryDetailViewController.swift */; }; 9CFDA0852AB499D300B478E8 /* NetworkAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CFDA0842AB499D300B478E8 /* NetworkAPI.swift */; }; 9CFDA0872AB49A1800B478E8 /* WeatherAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CFDA0862AB49A1800B478E8 /* WeatherAPI.swift */; }; @@ -59,7 +59,7 @@ 9C5A1DD52AAA3F5B00162C98 /* UITextView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+.swift"; sourceTree = ""; }; 9C5A1DD82AAB707A00162C98 /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; 9C5C42F12AB36D2D004FF07A /* MappingFile.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = MappingFile.xcmappingmodel; sourceTree = ""; }; - 9C5C42F32AB36F6B004FF07A /* CacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManager.swift; sourceTree = ""; }; + 9C5C42F32AB36F6B004FF07A /* CacheStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheStore.swift; sourceTree = ""; }; 9C75C4C92A9F448400C5D1CB /* DiaryDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryDetailViewController.swift; sourceTree = ""; }; 9CFDA0842AB499D300B478E8 /* NetworkAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAPI.swift; sourceTree = ""; }; 9CFDA0862AB49A1800B478E8 /* WeatherAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherAPI.swift; sourceTree = ""; }; @@ -153,7 +153,7 @@ 784F30CC2AB3004E009A895B /* Error */, 784F30CA2AB2FE87009A895B /* DecodingManager.swift */, 9C243B2F2AB307E1009DBF80 /* NetworkAPIDefinition.swift */, - 9C5C42F32AB36F6B004FF07A /* CacheManager.swift */, + 9C5C42F32AB36F6B004FF07A /* CacheStore.swift */, ); path = Network; sourceTree = ""; @@ -374,7 +374,7 @@ 78274DA12A9E3E8E00AD4F50 /* DiaryModel.swift in Sources */, C739AE25284DF28600741E8F /* AppDelegate.swift in Sources */, 78274DA42A9F3F0A00AD4F50 /* DateFormatter+.swift in Sources */, - 9C5C42F42AB36F6B004FF07A /* CacheManager.swift in Sources */, + 9C5C42F42AB36F6B004FF07A /* CacheStore.swift in Sources */, C739AE27284DF28600741E8F /* SceneDelegate.swift in Sources */, 9C4621EC2AB06890004ED11A /* PersistentContainer.swift in Sources */, 784F30C92AB2F88F009A895B /* Location.swift in Sources */, diff --git a/Diary/Network/CacheManager.swift b/Diary/Network/CacheStore.swift similarity index 77% rename from Diary/Network/CacheManager.swift rename to Diary/Network/CacheStore.swift index f3fd95570..e848abbe8 100644 --- a/Diary/Network/CacheManager.swift +++ b/Diary/Network/CacheStore.swift @@ -1,5 +1,5 @@ // -// CacheManager.swift +// CacheStore.swift // Diary // // Created by hoon, karen on 2023/09/15. @@ -7,7 +7,7 @@ import UIKit -class CacheManager { +final class CacheStore { static let shared = NSCache() private init() {} diff --git a/Diary/View/DiaryCell.swift b/Diary/View/DiaryCell.swift index 8d17519fb..eb05b5d17 100644 --- a/Diary/View/DiaryCell.swift +++ b/Diary/View/DiaryCell.swift @@ -79,7 +79,7 @@ final class DiaryCell: UITableViewCell { return } - guard let cache = CacheManager.shared.object(forKey: NSString(string: icon)) else { + guard let cache = CacheStore.shared.object(forKey: NSString(string: icon)) else { fetchIconImage(id: icon) return } @@ -99,7 +99,7 @@ final class DiaryCell: UITableViewCell { } DispatchQueue.main.async { - CacheManager.shared.setObject(image, forKey: NSString(string: id)) + CacheStore.shared.setObject(image, forKey: NSString(string: id)) self?.weatherIconImageView.image = image } case .failure(let error): From 7d36fcfaf8f4473f4a4178da1fe82669371cf839 Mon Sep 17 00:00:00 2001 From: karen Date: Fri, 15 Sep 2023 23:52:48 +0900 Subject: [PATCH 10/15] =?UTF-8?q?=F0=9F=92=A5refactor:=20prepareForReuse?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Diary/View/DiaryCell.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Diary/View/DiaryCell.swift b/Diary/View/DiaryCell.swift index eb05b5d17..a9997ff25 100644 --- a/Diary/View/DiaryCell.swift +++ b/Diary/View/DiaryCell.swift @@ -67,6 +67,7 @@ final class DiaryCell: UITableViewCell { } override func prepareForReuse() { + super.prepareForReuse() weatherIconImageView.image = nil } From 336190a92d82013258243d0a43196aebb37d53ab Mon Sep 17 00:00:00 2001 From: Hoon94 Date: Sat, 16 Sep 2023 01:06:55 +0900 Subject: [PATCH 11/15] =?UTF-8?q?=E2=9C=B4=EF=B8=8F=20feat:=20showToast=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Diary.xcodeproj/project.pbxproj | 4 +++ .../DiaryDetailViewController.swift | 33 +++++++++++++++++++ Diary/Network/Model/URLComponents.swift | 4 ++- Diary/Protocol/Toastable.swift | 10 ++++++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 Diary/Protocol/Toastable.swift diff --git a/Diary.xcodeproj/project.pbxproj b/Diary.xcodeproj/project.pbxproj index d6bd7f41e..7c50a4682 100644 --- a/Diary.xcodeproj/project.pbxproj +++ b/Diary.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 78274D9A2A9E26DF00AD4F50 /* DiaryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78274D992A9E26DF00AD4F50 /* DiaryCell.swift */; }; 78274DA12A9E3E8E00AD4F50 /* DiaryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78274DA02A9E3E8E00AD4F50 /* DiaryModel.swift */; }; 78274DA42A9F3F0A00AD4F50 /* DateFormatter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78274DA32A9F3F0A00AD4F50 /* DateFormatter+.swift */; }; + 782FB0992AB4B6C500BC8C12 /* Toastable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782FB0982AB4B6C500BC8C12 /* Toastable.swift */; }; 784F30C92AB2F88F009A895B /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784F30C82AB2F88F009A895B /* Location.swift */; }; 784F30CB2AB2FE87009A895B /* DecodingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784F30CA2AB2FE87009A895B /* DecodingManager.swift */; }; 784F30CE2AB30072009A895B /* DecodingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784F30CD2AB30072009A895B /* DecodingError.swift */; }; @@ -44,6 +45,7 @@ 78274D992A9E26DF00AD4F50 /* DiaryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryCell.swift; sourceTree = ""; }; 78274DA02A9E3E8E00AD4F50 /* DiaryModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryModel.swift; sourceTree = ""; }; 78274DA32A9F3F0A00AD4F50 /* DateFormatter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+.swift"; sourceTree = ""; }; + 782FB0982AB4B6C500BC8C12 /* Toastable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toastable.swift; sourceTree = ""; }; 784F30C82AB2F88F009A895B /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; 784F30CA2AB2FE87009A895B /* DecodingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingManager.swift; sourceTree = ""; }; 784F30CD2AB30072009A895B /* DecodingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingError.swift; sourceTree = ""; }; @@ -182,6 +184,7 @@ isa = PBXGroup; children = ( 9C1529102AAE140800F3203E /* Shareable.swift */, + 782FB0982AB4B6C500BC8C12 /* Toastable.swift */, ); path = Protocol; sourceTree = ""; @@ -376,6 +379,7 @@ 78274DA42A9F3F0A00AD4F50 /* DateFormatter+.swift in Sources */, 9C5C42F42AB36F6B004FF07A /* CacheStore.swift in Sources */, C739AE27284DF28600741E8F /* SceneDelegate.swift in Sources */, + 782FB0992AB4B6C500BC8C12 /* Toastable.swift in Sources */, 9C4621EC2AB06890004ED11A /* PersistentContainer.swift in Sources */, 784F30C92AB2F88F009A895B /* Location.swift in Sources */, 784F30CB2AB2FE87009A895B /* DecodingManager.swift in Sources */, diff --git a/Diary/Controller/DiaryDetailViewController.swift b/Diary/Controller/DiaryDetailViewController.swift index 750588457..261656e91 100644 --- a/Diary/Controller/DiaryDetailViewController.swift +++ b/Diary/Controller/DiaryDetailViewController.swift @@ -29,6 +29,7 @@ final class DiaryDetailViewController: UIViewController, Shareable { override func viewDidLoad() { super.viewDidLoad() configure() + configureToast() fetchWeather() } @@ -183,6 +184,38 @@ private extension DiaryDetailViewController { } } +extension DiaryDetailViewController: Toastable { + func configureToast() { + APIKey.delegate = self + } + + func showToast(message: String) { + let toastLabel = UILabel(frame: CGRect( + x: self.view.frame.size.width / 2 - 100, + y: self.view.frame.size.height / 2 - 35, + width: 200, + height: 70 + )) + + toastLabel.backgroundColor = UIColor.systemGray + toastLabel.textColor = UIColor.white + toastLabel.font = UIFont.preferredFont(forTextStyle: .body) + toastLabel.textAlignment = .center + toastLabel.text = message + toastLabel.alpha = 1.0 + toastLabel.layer.cornerRadius = 10 + toastLabel.clipsToBounds = true + toastLabel.numberOfLines = 0 + + view.addSubview(toastLabel) + UIView.animate(withDuration: 5.0, delay: 0.1, options: .curveEaseOut, animations: { + toastLabel.alpha = 0.0 + }, completion: { _ in + toastLabel.removeFromSuperview() + }) + } +} + private extension DiaryDetailViewController { func fetchWeather() { guard let latitude, let longitude else { diff --git a/Diary/Network/Model/URLComponents.swift b/Diary/Network/Model/URLComponents.swift index e73506597..308237ef9 100644 --- a/Diary/Network/Model/URLComponents.swift +++ b/Diary/Network/Model/URLComponents.swift @@ -47,11 +47,13 @@ enum Query { } enum APIKey { + static var delegate: Toastable? static var weather: String { guard let file = Bundle.main.path(forResource: "WeatherInfo", ofType: "plist"), let resource = NSDictionary(contentsOfFile: file), let key = resource["API_KEY"] as? String else { - fatalError("⛔️ API KEY를 가져오는데 실패하였습니다.") + delegate?.showToast(message: "⛔️ API KEY를 가져오는데 실패하였습니다.") + return "⛔️ API KEY를 가져오는데 실패하였습니다." } return key diff --git a/Diary/Protocol/Toastable.swift b/Diary/Protocol/Toastable.swift new file mode 100644 index 000000000..b21b9f088 --- /dev/null +++ b/Diary/Protocol/Toastable.swift @@ -0,0 +1,10 @@ +// +// Toastable.swift +// Diary +// +// Created by hoon, karen on 2023/09/16. +// + +protocol Toastable { + func showToast(message: String) +} From 6ee190ec539742e22c2e9bfd219d1f82edac7667 Mon Sep 17 00:00:00 2001 From: Hoon94 Date: Sat, 16 Sep 2023 01:37:02 +0900 Subject: [PATCH 12/15] =?UTF-8?q?=F0=9F=92=A5refactor:=20if=20let=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95=ED=95=98=EC=97=AC=20=EA=B0=80?= =?UTF-8?q?=EB=8F=85=EC=84=B1=20=ED=96=A5=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Diary/View/DiaryCell.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Diary/View/DiaryCell.swift b/Diary/View/DiaryCell.swift index a9997ff25..855faca41 100644 --- a/Diary/View/DiaryCell.swift +++ b/Diary/View/DiaryCell.swift @@ -79,13 +79,12 @@ final class DiaryCell: UITableViewCell { guard let icon else { return } - - guard let cache = CacheStore.shared.object(forKey: NSString(string: icon)) else { + + if let cache = CacheStore.shared.object(forKey: NSString(string: icon)) { + weatherIconImageView.image = cache + } else { fetchIconImage(id: icon) - return } - - weatherIconImageView.image = cache } private func fetchIconImage(id: String) { From 3874d6bb47f2dc69b75008d861c07e55d9df3274 Mon Sep 17 00:00:00 2001 From: karen Date: Sat, 16 Sep 2023 02:28:55 +0900 Subject: [PATCH 13/15] =?UTF-8?q?=F0=9F=96=A8=EF=B8=8F=20chore:=20?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=EB=B3=B4=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Diary.xcodeproj/project.pbxproj | 16 ++----------- Diary/Resource/Info.plist | 2 -- Diary/View/Base.lproj/Main.storyboard | 33 --------------------------- 3 files changed, 2 insertions(+), 49 deletions(-) delete mode 100644 Diary/View/Base.lproj/Main.storyboard diff --git a/Diary.xcodeproj/project.pbxproj b/Diary.xcodeproj/project.pbxproj index 7c50a4682..9bcd67174 100644 --- a/Diary.xcodeproj/project.pbxproj +++ b/Diary.xcodeproj/project.pbxproj @@ -34,7 +34,6 @@ C739AE25284DF28600741E8F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE24284DF28600741E8F /* AppDelegate.swift */; }; C739AE27284DF28600741E8F /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE26284DF28600741E8F /* SceneDelegate.swift */; }; C739AE29284DF28600741E8F /* DiaryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C739AE28284DF28600741E8F /* DiaryViewController.swift */; }; - C739AE2C284DF28600741E8F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C739AE2A284DF28600741E8F /* Main.storyboard */; }; C739AE2F284DF28600741E8F /* Diary.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C739AE2D284DF28600741E8F /* Diary.xcdatamodeld */; }; C739AE31284DF28600741E8F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C739AE30284DF28600741E8F /* Assets.xcassets */; }; C739AE34284DF28600741E8F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C739AE32284DF28600741E8F /* LaunchScreen.storyboard */; }; @@ -70,7 +69,6 @@ C739AE24284DF28600741E8F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C739AE26284DF28600741E8F /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; C739AE28284DF28600741E8F /* DiaryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryViewController.swift; sourceTree = ""; }; - C739AE2B284DF28600741E8F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; C739AE2E284DF28600741E8F /* Diary.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Diary.xcdatamodel; sourceTree = ""; }; C739AE30284DF28600741E8F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C739AE33284DF28600741E8F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -103,7 +101,6 @@ 78274D9C2A9E31A200AD4F50 /* View */ = { isa = PBXGroup; children = ( - C739AE2A284DF28600741E8F /* Main.storyboard */, C739AE32284DF28600741E8F /* LaunchScreen.storyboard */, 78274D992A9E26DF00AD4F50 /* DiaryCell.swift */, ); @@ -316,7 +313,6 @@ C739AE34284DF28600741E8F /* LaunchScreen.storyboard in Resources */, C739AE31284DF28600741E8F /* Assets.xcassets in Resources */, 78D13D952AB338B000A0D28E /* WeatherInfo.plist in Resources */, - C739AE2C284DF28600741E8F /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -402,14 +398,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - C739AE2A284DF28600741E8F /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - C739AE2B284DF28600741E8F /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; C739AE32284DF28600741E8F /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -547,9 +535,9 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Diary/Resource/Info.plist; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "위치 기반 날씨 정보를 위해 GPS정보가 필요합니다."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -576,9 +564,9 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Diary/Resource/Info.plist; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "위치 기반 날씨 정보를 위해 GPS정보가 필요합니다."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; diff --git a/Diary/Resource/Info.plist b/Diary/Resource/Info.plist index a4de3ecfa..16906fbe4 100644 --- a/Diary/Resource/Info.plist +++ b/Diary/Resource/Info.plist @@ -17,8 +17,6 @@ Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main diff --git a/Diary/View/Base.lproj/Main.storyboard b/Diary/View/Base.lproj/Main.storyboard deleted file mode 100644 index bf565abe3..000000000 --- a/Diary/View/Base.lproj/Main.storyboard +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 93bcb24b41cb26bcda5a785dd69660945c912034 Mon Sep 17 00:00:00 2001 From: karen <124643896+karenyang835@users.noreply.github.com> Date: Sat, 16 Sep 2023 19:11:15 +0900 Subject: [PATCH 14/15] =?UTF-8?q?=20=E2=9C=8D=EF=B8=8F=20docs:=20README.md?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stpe03 README 수정 --- README.md | 265 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 237 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 77e673b90..b84d5c888 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ 3. [시각적 프로젝트 구조](#3.) 4. [트러블 슈팅](#4.) 5. [참고 링크](#5.) +6. [about TEAM](#6.) --- @@ -17,9 +18,9 @@ ## 1. 💬 프로젝트 소개 -> `CoreData`를 활용하여 만든 일기장 앱으로 수정이 간편하고, 날짜별로 일기장 검색이 가능합니다. +> `CoreData`를 활용하여 만든 일기장 앱으로 수정이 간편하고, 날짜별로 날씨 정보를 함께 저장합니다. -[![Xcode](https://img.shields.io/badge/Xcode-14.3.1-blue?style=flat&logo=Xcode&logoColor=)]() [![swift](https://img.shields.io/badge/swift-5.6-red?style=flat&logo=Swift&logoColor=)]() [![IOS](https://img.shields.io/badge/iOS-15.0+-orange?style=flat&logo=Apple&logoColor=white)]() +[![Xcode](https://img.shields.io/badge/Xcode-v14.3.1-blue?style=flat&logo=Xcode)]() [![swift](https://img.shields.io/badge/swift-v5.6-red?style=flat&logo=Swift)]() [![IOS](https://img.shields.io/badge/iOS-v15.0+-orange?style=flat&logo=Apple&logoColor=white)]() [![SwiftLint](https://img.shields.io/badge/SwiftLint-v0.52.4-green?style=flat&logo=stylelint&logoColor=white)]() ---
@@ -27,14 +28,17 @@ ## 2.📱실행 화면 -| Diary - 화면 동작 |Diary - 키보드 동작 / 새 일기장 | +| Diary - 새 일기 작성 |Diary - 일기 수정 | | :-: |:-: | -||| +||| -| Diary - 가로 화면 (Dynamic type) | -| :-: | -|| +| Diary - 공유, 삭제(메인) |Diary - 더보기(디테일) | +| :-: |:-: | +||| +| Diary - Alert Error |Diary - Toast Error | +| :-: |:-: | +||| --- @@ -43,23 +47,43 @@ ## 3. 📊 시각적 프로젝트 구조 -
### 📂 폴더 구조 -```swift +```bash ┌── Diary +│ ├── Network +│ │ ├── Location +│ │ ├── URLComponents +│ │ ├── NetworkAPI +│ │ └── WeatherAPI +│ ├── Error +│ │ ├── DecodingManager +│ │ ├── NetworkAPIDefinition +│ │ └── CacheStore +│ ├── CoreData +│ │ ├── CoreDataManager +│ │ ├── PersistentContainer +│ │ ├── Diary +│ │ │ ├── Diary v2 +│ │ │ └── Diary +│ │ └── MappingFile │ ├── Model -│ │ └── DiaryModel +│ │ ├── DiaryModel +│ │ └── CellIdentifier │ ├── View -│ │ ├── main │ │ ├── LaunchScreen │ │ └── DiaryCell │ ├── Controller │ │ ├── DiaryViewController │ │ └── DiaryDetailViewController +│ ├── Protocol +│ │ ├── Shareable +│ │ └── Toastable │ ├── Extension -│ │ └── DateFormatter+ +│ │ ├── DateFormatter+ +│ │ ├── UITextView+ +│ │ └── Array+ │ ├── Application │ │ ├── AppDelegate │ │ └── SceneDelegate @@ -70,18 +94,22 @@ └── README.md ``` ---- +### 🎨 Class Diagram
+![image](https://github.com/karenyang835/ios-diary/assets/124643896/c2c14350-a719-49da-a2ad-5bdd811a896f) +--- + +
+ ## 4. 🚨 트러블 슈팅 ### 1️⃣ 지역별 현지화 - #### ⛔️ 문제점 - 다이어리의 작성 일자에 대한 날짜 표현 형식을 사용자에 맞게 표현해 주기 위한 방법이 필요했습니다. JSON 파일에 저장된 날짜는 시스템 시간으로 1970년을 기준으로 한 `Double` 타입의 값이었기 때문에 사용자에게 표현하는 형식을 선택해야 했습니다. @@ -107,30 +135,211 @@ 지역에 맞게 날짜의 형식을 변경해 주는 diaryFormatter를 사용하여 JSON 데이터를 디코딩 한 값인 date(Double 타입)을 매개변수로 전달하였습니다. ---- +### 2️⃣`CoreData` 사용 +#### ⛔️ 문제점 +- `CoreData`를 활용하는데 어려움이 많았습니다. 그중에서 새 일기장을 만들고 아무것도 입력하지 않은 상태로 다시 나오거나 입력을 하다가 다 지우고 나오면 생성되지 않아야 된다고 생각을 하였는데 생성이 되는 문제점이 발생했습니다. + + +#### ✅ 해결 방법 +- `textView`의 내용이 비어있으면 `delete`를 해주어 해결했습니다. -
+ ```swift + func saveDiary() { + guard !contentTextView.text.isEmpty else { + CoreDataManager.shared.deleteDiary(item: diary) + return + } + + let contents = contentTextView.text.split(separator: "\n") + let title = String(contents[0]) + let body = contents.dropFirst().joined(separator: "\n") + + if contents.isEmpty { + saveContents(title: "", body: "") + } else { + saveContents(title: title, body: body) + } + } + ``` + +### 3️⃣ `Diary` 생성 위치 +#### ⛔️ 문제점 +- `Diary`를 `Creat`하는 위치에 따라 `tableView`에 `cell`이 추가되는 시점이 달랐습니다. 두 번째 화면인 `DiaryDetailViewController`에서 `Diary`를 생성하고 저장하는 경우 이전 화면으로 돌아와 `tableView`를 확인하면 목록에 없는 문제가 발생하였습니다. 다음 화면으로 넘어갔다가 다시 돌아오면 그제야 `cell`이 `tableView`에 추가되었습니다. `tableView`에 `cell`이 그려지는 시점이 문제였습니다. + +#### ✅ 해결 방법 +- 이를 해결하기 위해 `Diary`를 첫 화면인 `DiaryViewController`에서 생성하여 다음 화면인 `DiaryDetailViewController`로 넘겨주는 작업을 수행하였습니다. + + ```swift + let action = UIAction { _ in + let diary = CoreDataManager.shared.createDiary() + let diaryDetailViewController = DiaryDetailViewController(diary: diary, isUpdate: false) + self.navigationController?.pushViewController(diaryDetailViewController, animated: true) + } + ``` + + 생성한 `Diary`에 입력을 하는 경우 `save` 또는 `update` 동작을 분리하기 위하여 `isUpdate`를 통해 구분하였습니다. + +### 4️⃣ 오토레이아웃 +#### ⛔️ 문제점 +- `Hierarchy`로 확인을 했을 때 해당 오류가 발생됐습니다. + + ![](https://hackmd.io/_uploads/rJpIedsR2.png) + +#### ✅ 해결 방법 +- 확인을 해보니 날짜를 받아오는 `label`의 사이즈가 변동이 되면서 발생된 문제점이라 `Hugging`을 주어서 해결했습니다. + + ```swift + private let dateLabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .body) + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + + return label + }() + ``` + +### 5️⃣`CoreData` 사용 +#### ⛔️ 문제점 +- 새 일기장을 만들고 아무것도 입력하지 않은 상태로 다시 나오거나 입력을 하다가 다 지우고 나오면 생성되지 않아야 된다고 생각을 하였는데 생성이 되는 문제점이 발생했습니다. + + +#### ✅ 해결 방법 +- `textView`의 내용이 비어있으면 `delete`를 해주어 해결했습니다. + + ```swift + func saveDiary() { + guard !contentTextView.text.isEmpty else { + CoreDataManager.shared.deleteDiary(item: diary) + return + } + + let contents = contentTextView.text.split(separator: "\n") + let title = String(contents[0]) + let body = contents.dropFirst().joined(separator: "\n") + + if contents.isEmpty { + saveContents(title: "", body: "") + } else { + saveContents(title: title, body: body) + } + } + ``` + +### 6️⃣ `Diary` 생성 위치 +#### ⛔️ 문제점 +- `Diary`를 `Creat`하는 위치에 따라 `tableView`에 `cell`이 추가되는 시점이 달랐습니다. 두 번째 화면인 `DiaryDetailViewController`에서 `Diary`를 생성하고 저장하는 경우 이전 화면으로 돌아와 `tableView`를 확인하면 목록에 없는 문제가 발생하였습니다. 다음 화면으로 넘어갔다가 다시 돌아오면 그제야 `cell`이 `tableView`에 추가되었습니다. `tableView`에 `cell`이 그려지는 시점이 문제였습니다. + +#### ✅ 해결 방법 +- `Diary`를 첫 화면인 `DiaryViewController`에서 생성하여 다음 화면인 `DiaryDetailViewController`로 넘겨주는 작업을 수행하였습니다. + + ```swift + let action = UIAction { _ in + let diary = CoreDataManager.shared.createDiary() + let diaryDetailViewController = DiaryDetailViewController(diary: diary, isUpdate: false) + self.navigationController?.pushViewController(diaryDetailViewController, animated: true) + } + ``` + + 생성한 `Diary`에 입력을 하는 경우 `save` 또는 `update` 동작을 분리하기 위하여 `isUpdate`를 통해 구분하였습니다. + +### 7️⃣ 오토레이아웃 +#### ⛔️ 문제점 +- 일기장을 새로 만든 후에 날씨 아이콘을 받아와서 등록됐을때는 온전하게 오토 레이아웃이 적용됐는데 일기장을 들어갔다 나오면 날씨 아이콘이 커져버리는 문제점이 발생했습니다. + + +#### ✅ 해결 방법 +- `dateLabel`의 높이에 맞추어서 날씨 아이콘의 크기가 변경되도록 제약조건을 잡아주고 `dateLabel`의 `setContentHuggingPriority`을 `required`로 가장 높게 잡아주어 해결했습니다. + + ```swift + private let dateLabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .body) + label.setContentHuggingPriority(.required, for: .vertical) + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + + return label + }() + + private let weatherIconImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + + return imageView + }() + ``` + +### 8️⃣ `API KEY` 숨기기 +#### ⛔️ 문제점 +- `API KEY`를 코드에 직접 입력하여 사용하는 경우 `API KEY`가 외부에 드러날 수 있다는 문제점이 있었습니다. 팀원과의 협업을 위해 `git`을 통해 코드를 업로드 하는 과정에서 입력했던 `API KEY`가 함께 업로드 되었습니다. + +#### ✅ 해결 방법 +- `API KEY`를 감추기 위해 `plist` 파일을 생성하여 외부에 드러나지 않도록 숨겼습니다. `API KEY`를 위해 생성한 파일을 `git`에 저장하고 이후 `git`에서 추적하지 않도록 설정하여 실제 `KEY` 값을 사용하였습니다. + + ```bash + git update-index --skip-worktree Diary/Resource/WeatherInfo.plist + ``` + +--- + +
## 5.🔗 참고 링크 -🍎 [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/)
-🍏 [Apple Developer - UINavigationController](https://developer.apple.com/documentation/uikit/uinavigationcontroller)
-🍏 [Apple Developer - UITextView](https://developer.apple.com/documentation/uikit/uitextview)
-🍏 [Apple Developer - UIKeyboardLayoutGuide](https://developer.apple.com/documentation/uikit/uikeyboardlayoutguide/)
-🍏 [Apple Developer - Date](https://developer.apple.com/documentation/foundation/date)
-🍏 [Apple Developer - DateFormatter](https://developer.apple.com/documentation/foundation/dateformatter)
-🍏 [Apple Developer - Adding support for languages and regions](https://developer.apple.com/documentation/xcode/adding-support-for-languages-and-regions)
-🍏 [Apple Developer - Locale](https://developer.apple.com/documentation/foundation/locale)
- [BLOG : 김종권의 iOS 앱 개발 알아가기 - SwiftLint 적용 방법](https://ios-development.tistory.com/1199)
- [BLOG : Dr.kim의 나를 위한 블로그 - 화면에 딱 맞는 UITextView 만들기](https://hereismyblog.tistory.com/34)
- [BLOG : Hacking with Swift - Fixing the keyboard: NotificationCenter -](https://www.hackingwithswift.com/read/19/7/fixing-the-keyboard-notificationcenter)
+- 🍎 [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/) +- 🍏 [Apple Developer - UINavigationController](https://developer.apple.com/documentation/uikit/uinavigationcontroller) +- 🍏 [Apple Developer - UITextView](https://developer.apple.com/documentation/uikit/uitextview) +- 🍏 [Apple Developer - UIKeyboardLayoutGuide](https://developer.apple.com/documentation/uikit/uikeyboardlayoutguide/) +- 🍏 [Apple Developer - Date](https://developer.apple.com/documentation/foundation/date) +- 🍏 [Apple Developer - DateFormatter](https://developer.apple.com/documentation/foundation/dateformatter) +- 🍏 [Apple Developer - Adding support for languages and regions](https://developer.apple.com/documentation/xcode/adding-support-for-languages-and-regions) +- 🍏 [Apple Developer - Locale](https://developer.apple.com/documentation/foundation/locale) +- [BLOG : 김종권의 iOS 앱 개발 알아가기 - SwiftLint 적용 방법](https://ios-development.tistory.com/1199) +- [BLOG : Dr.kim의 나를 위한 블로그 - 화면에 딱 맞는 UITextView 만들기](https://hereismyblog.tistory.com/34) +- [BLOG : Hacking with Swift - Fixing the keyboard: NotificationCenter +](https://www.hackingwithswift.com/read/19/7/fixing-the-keyboard-notificationcenter) + +--- + +
+ + + + +## 6. 🎩 aboutTEAM +| hoon ♓️ |Karen ♉️| +| :-: | :-: | +| | | +|https://github.com/Hoon94 |https://github.com/karenyang835| + +
⏰ 타임 라인 (펼쳐보기) + +|**날 짜**|**내 용**| +|:-:|-| +| 2023.08.28. | 📝 프로젝트에서 필요로 하는 핵심기능 공부 - `CoreData`
| +| 2023.08.29. | 🖨️ `SwiftLint` 라이브러리 추가
✴️ `tableView` 구현
✴️ `navigationController` 구현
| +| 2023.08.30. | ✴️ `parseData` 메서드 구현
✴️ `custom cell` 구현
✴️ `diaryFormatter` 구현
💥 `DiaryDetailViewController` 화면이동 추가 구현
| +| 2023.08.31. | ✴️ `DiaryDetailViewController` 레이아웃 구현
💥 `keyboard`에 맞춰 제약조건 수정
| +| 2023.09.01. | 💥 `DiaryCell` 선택시 다음 화면으로 이동
✴️ `CellIdentifier` 구현
✴️ `DataAsset`을 불러오지 못하는 경우 `presentAlert`메소드 추가
💥 중복 사용하는 프로퍼티를 상수로 선언
💥 `UITableViewDelegate`와 `UITableViewDataSource extension` 분리
💥 `diaryList`로 네이밍 변경
✍️ `README`수정
| +| 2023.09.04. | 📝 프로젝트에서 필요로 하는 핵심기능 공부 - `CoreData CRUD` | +| 2023.09.05. | 📝 프로젝트에서 필요로 하는 핵심기능 공부 - `UITextViewDelegate` | +| 2023.09.06. | 📝 프로젝트에서 필요로 하는 핵심기능 공부 - `UISwipeActionsConfiguration` | +| 2023.09.07. |✴️ `CoreData Diary Entity` 생성
✴️ `Coredata loadDiary` 메소드 추가
✴️ `CoreData saveDiary` 메소드 추가
✴️ `Coredata deleteDiary` 메소드 추가
| +| 2023.09.08. |✴️ `CoreData updateDiary` 메소드 추가
✴️ `keyboard Done`버튼 추가
💥 `DiaryDetailViewController`의 화면 레이아웃을 하나의 `contentTextView`로 통합
| +| 2023.09.09. |✴️ `CoreDataManager` 추가
✴️ 백그라운드로 진입하는 경우 일기 자동저장 구현
💥 기존 `CRUD`코드 통합
| +| 2023.09.11. |✴️ `showActivityView` 메서드 추가
✴️ `Shareable` `Protocol` 생성
✴️ `Alert` 기능 추가
💥 `tableView swipe action` 공유기능 추가
| +| 2023.09.12. |✴️ `Array extension` 추가
💥`CoreDataManager`을 제너릭타입 활용으로 변경
💥 `isUpdated` 상수명 수정
💥`closure` 순환참조 방지를 위한 `weak self` 수정
| +| 2023.09.13. |💥 `configureCell` 메서드 매개변수 타입 변경
💥 `Shareable` 프로토콜 제네릭 타입으로 변경
| +| 2023.09.14. |✴️ `WeatherAPI` 생성 및 `API KEY` 숨기기
✴️ `NetworkManager` 생성 및 `fetchWeather`메서드 구현
✴️ `decodeData` 메서드 구현 및 모델 생성
✴️ `CoreLocation`기반 위치정보 가져오기 구현
| +| 2023.09.15. |💥 `prepareForReuse` 메서드 수정
💥 `CacheStore`로 네이밍 변경
💥 `NetworkAPI` 수정
✴️ `NetworkAPI` 생성
💥 `Cell` 재사용을 위한 초기화 및 날씨 icon autolayout 수정
✴️ `CoreData` 마이그레이션 및 `fetchIconImage` 메서드 추가
| +| 2023.09.16. |✴️ `showToast` 메서드 생성
💥 `if let`으로 수정하여 가독성 향상
🖨️ 스토리보드 삭제
| +
+ --- From 0482761fda6121c88f5f977fe3359de630521f93 Mon Sep 17 00:00:00 2001 From: karen <124643896+karenyang835@users.noreply.github.com> Date: Sat, 16 Sep 2023 19:29:16 +0900 Subject: [PATCH 15/15] =?UTF-8?q?=E2=9C=8D=EF=B8=8F=20docs:=20README.md=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README : 블로그 아이콘 이미지 다시 올림 --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b84d5c888..adbb2bb22 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ ### 🎨 Class Diagram
-![image](https://github.com/karenyang835/ios-diary/assets/124643896/c2c14350-a719-49da-a2ad-5bdd811a896f) +![image](https://github.com/karenyang835/ios-diary/assets/124643896/2d213526-ff5e-457e-a0d1-d1ab540e648d) --- @@ -299,10 +299,10 @@ - 🍏 [Apple Developer - DateFormatter](https://developer.apple.com/documentation/foundation/dateformatter) - 🍏 [Apple Developer - Adding support for languages and regions](https://developer.apple.com/documentation/xcode/adding-support-for-languages-and-regions) - 🍏 [Apple Developer - Locale](https://developer.apple.com/documentation/foundation/locale) -- [BLOG : 김종권의 iOS 앱 개발 알아가기 - SwiftLint 적용 방법](https://ios-development.tistory.com/1199) -- [BLOG : Dr.kim의 나를 위한 블로그 - 화면에 딱 맞는 UITextView 만들기](https://hereismyblog.tistory.com/34) -- [BLOG : Hacking with Swift - Fixing the keyboard: NotificationCenter -](https://www.hackingwithswift.com/read/19/7/fixing-the-keyboard-notificationcenter) +- [BLOG : 김종권의 iOS 앱 개발 알아가기 - SwiftLint 적용 방법](https://ios-development.tistory.com/1199) +- [BLOG : Dr.kim의 나를 위한 블로그 - 화면에 딱 맞는 UITextView 만들기](https://hereismyblog.tistory.com/34) +- [BLOG : Hacking with Swift - Fixing the keyboard: NotificationCenter +]() --- @@ -341,6 +341,7 @@ + ---