Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] #252 - 낫투두 위젯 생성 #258

Open
wants to merge 30 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b8cedc3
[Add] #252 - 이미지 및 필요한 파일 추가
yungu0010 Apr 19, 2024
43b127f
[Add] #252 - 커스텀 체크박스 생성
yungu0010 Apr 19, 2024
d261a5c
[Add] #252 - Circular ProgressBar 생성
yungu0010 Apr 19, 2024
9f2c272
[Add] #252 - 커스텀 수평선 생성
yungu0010 Apr 19, 2024
5130ecd
[Design] .systemSmall 타입 위젯 뷰 생성
yungu0010 Apr 19, 2024
713415d
[Add] #252 - 더미데이터 추가
yungu0010 Apr 19, 2024
434cbdb
[Design] #252 - .systemMedium 타입 위젯 뷰 생성
yungu0010 Apr 19, 2024
65ac534
[Feat] #252 - 위젯 설정
yungu0010 Apr 19, 2024
e16314e
[Feat] #252 - 위젯 인터랙션 추가
yungu0010 Apr 20, 2024
e636ba6
[Fix] #252 - 어노테이션 제거
yungu0010 Apr 20, 2024
fb90783
[Feat] #252 - 인증서 변경
yungu0010 Apr 20, 2024
1f513a6
[Add] #252 - 컬러 에셋 추가 및 systemColor 중복 이름 수정
yungu0010 Apr 22, 2024
1a215df
[Feat] #252 - App Group 설정
yungu0010 Apr 22, 2024
6e08fc0
[Chore] #252 - Identifiable 준수
yungu0010 Apr 22, 2024
5485a5b
[Feat] #252 - 앱과 위젯 간 데이터 공유
yungu0010 Apr 22, 2024
9ecc260
[Feat] #252 - 텍스트 속성 변경
yungu0010 Apr 22, 2024
db17a85
[Feat] #252 - 달성 여부에 따른 취소선 추가
yungu0010 Apr 22, 2024
069a707
[Chore] #252 - displayname 변경
yungu0010 May 6, 2024
3df0426
[Feat] #252 - 변경된 낫투두가 오늘인 경우에만 App Group 업데이 트
yungu0010 May 8, 2024
fd926e0
[Feat] #252 - 명언 api 요청을 위한 basefile 세팅
yungu0010 May 10, 2024
93fa7b3
[Chore] #252 - typealias 사용
yungu0010 May 10, 2024
9ea62ca
[Feat] #252 - 명언 api 통신 후 entry 업데이트
yungu0010 May 10, 2024
5f8ad7b
[Fix] #252 - Circle trim 명세서에 맞게 수정
yungu0010 May 10, 2024
46b4083
[Fix] #252 - 요일 가져오기 및 명언 자정에만 업데이트 되도록 수정
yungu0010 May 10, 2024
d79f132
[Fix] #252 - 네트워크 상수 사용
yungu0010 May 13, 2024
e790087
[Fix] #252 - AppGroup의 accessToken도 함께 업데이트
yungu0010 Jun 26, 2024
096bd15
[Chore] #252 - dateformatter 함수화
yungu0010 Jun 26, 2024
68233e3
[Feat] #252 - 위젯 서버통신 코드 작성
yungu0010 Jun 26, 2024
048e70f
[Feat] #252 - AppIntent 기능 추가
yungu0010 Jun 26, 2024
6bb2a0b
[Fix] #252 - 00시에 AppGroup의 dailyMission 업데이트 되도록 수정
yungu0010 Jun 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions iOS-NOTTODO/Settings.bundle/Root.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ApplicationGroupContainerIdentifier</key>
<string>group.nottodo.iOS-NOTTODO</string>
<key>StringsTable</key>
<string>Root</string>
<key>PreferenceSpecifiers</key>
Expand Down
34 changes: 34 additions & 0 deletions iOS-NOTTODO/Widget-NOTTODO/AppIntent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// AppIntent.swift
// Widget-NOTTODO
//
// Created by 강윤서 on 4/14/24.
//

import WidgetKit
import AppIntents

struct ConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Configuration"
}

struct ToggleButtonIntent: AppIntent {
static var title: LocalizedStringResource = .init(stringLiteral: "Mission's State")

@Parameter(title: "Mission ID")
var id: Int

@Parameter(title: "Mission status")
var status: String

init() { }
init(id: Int, status: String) {
self.id = id
self.status = status == CompletionStatus.UNCHECKED.rawValue ? CompletionStatus.CHECKED.rawValue : CompletionStatus.UNCHECKED.rawValue
}

func perform() async throws -> some IntentResult {
_ = try await WidgetService.shared.updateMission(id: id, status: status)
return .result()
}
}
18 changes: 18 additions & 0 deletions iOS-NOTTODO/Widget-NOTTODO/Global/Extensions/Formatter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Formatter.swift
// iOS-NOTTODO
//
// Created by 강윤서 on 5/12/24.
//

import Foundation

struct Formatter {
static func dateFormatterString(format: String?, date: Date) -> String {
let formatter = Foundation.DateFormatter()
formatter.dateFormat = format ?? "yyyy-MM-dd"
formatter.locale = Locale(identifier: "ko_KR")
let convertStr = formatter.string(from: date)
return convertStr
}
}
17 changes: 17 additions & 0 deletions iOS-NOTTODO/Widget-NOTTODO/Network/Base/NetworkError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// NetworkError.swift
// iOS-NOTTODO
//
// Created by 강윤서 on 5/11/24.
//

import Foundation

enum NetworkError: Error {
case invalidResponse
case networkError
case dataParsingError
case invalidRequestParameters
case encodingFailed
case internalError(message: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// QuoteResponseDTO.swift
// iOS-NOTTODO
//
// Created by 강윤서 on 5/10/24.
//

import Foundation

struct QuoteResponseDTO: Codable {
let id: Int
let description: String
let author: String
}
205 changes: 205 additions & 0 deletions iOS-NOTTODO/Widget-NOTTODO/Network/Service/WidgetService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//
// WidgetService.swift
// iOS-NOTTODO
//
// Created by 강윤서 on 5/10/24.
//

import Foundation
import SwiftUI

typealias QuoteData = GeneralResponse<QuoteResponseDTO>
typealias DailyMissionData = GeneralArrayResponse<DailyMissionResponseDTO>
typealias UpdateMissionStatus = GeneralResponse<DailyMissionResponseDTO>

struct WidgetService {
@AppStorage(DefaultKeys.accessToken, store: UserDefaults.shared) var accessToken: String = ""

static let shared = WidgetService()
private init() {}

let session = URLSession(configuration: URLSessionConfiguration.default, delegate: URLSessionLoggingDelegate(), delegateQueue: nil)

func fetchWiseSaying() async throws -> QuoteResponseDTO {
do {
return try await getWiseSaying(
from: generateURL(constant: URLConstant.quote))
} catch {
switch error {
case NetworkError.networkError:
print("네트워크 에러가 발생")
case NetworkError.invalidResponse:
print("유효하지 않은 응답")
case NetworkError.dataParsingError:
print("데이터 파싱 실패")
default:
print("알 수 없는 에러 발생")
}
throw error
}
}

func fetchDailyMissoin(date: String) async throws -> [DailyMissionResponseDTO] {
do {
return try await getDailyMission(
from: generateURL(constant: URLConstant.recommend + URLConstant.dailyMission, parameter: date))
} catch {
switch error {
case NetworkError.networkError:
print("네트워크 에러가 발생")
case NetworkError.invalidResponse:
print("유효하지 않은 응답")
case NetworkError.dataParsingError:
print("데이터 파싱 실패")
default:
print("알 수 없는 에러 발생")
}
throw error
}
}

func updateMission(id: Int, status: String) async throws {
do {
try await patchUpdateMission(
from: generateURL(constant: URLConstant.recommend, parameter: "\(id)" + "/check"),
id: id,
requestData: status)

let dailyMission = try await fetchDailyMissoin(date: Formatter.dateFormatterString(format: nil, date: Date()))
UserDefaults.shared?.setSharedCustomArray(dailyMission, forKey: "dailyMission")
} catch {
switch error {
case NetworkError.networkError:
print("네트워크 에러가 발생")
case NetworkError.invalidResponse:
print("유효하지 않은 응답")
case NetworkError.dataParsingError:
print("데이터 파싱 실패")
case NetworkError.encodingFailed:
print("데이터 인코딩 실패")
case NetworkError.invalidRequestParameters:
print("유효하지 않은 파라미터")
default:
print("알 수 없는 에러 발생")
}
throw error
}
}

private func getWiseSaying(from url: URL) async throws -> QuoteResponseDTO {
let (data, response) = try await session.data(from: url)

guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.networkError
}

guard (200..<300).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}

do {
let result = try JSONDecoder().decode(QuoteData.self, from: data)
guard let responseData = result.data else {
throw NetworkError.dataParsingError
}
return responseData
} catch {
throw NetworkError.dataParsingError
}
}

private func getDailyMission(from url: URL) async throws -> [DailyMissionResponseDTO] {
var request = URLRequest(url: url)
request.setValue("application/json",
forHTTPHeaderField: "Content-Type")
request.setValue(accessToken,
forHTTPHeaderField: "Authorization")

let (data, response) = try await session.data(for: request)

guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.networkError
}

print("Response Status Code: \(httpResponse.statusCode)")
print("Response Headers: \(httpResponse.allHeaderFields)")

guard (200..<300).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}

do {
let result = try JSONDecoder().decode(DailyMissionData.self, from: data)
guard let responseData = result.data else {
throw NetworkError.dataParsingError
}
return responseData
} catch {
throw NetworkError.dataParsingError
}
}

private func patchUpdateMission(from url: URL, id: Int, requestData: String) async throws {
var request = URLRequest(url: url)
request.httpMethod = "PATCH"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(accessToken, forHTTPHeaderField: "Authorization")

do {
let body = ["completionStatus": requestData]
request.httpBody = try JSONSerialization.data(withJSONObject: body)

if let bodyData = request.httpBody, let bodyString = String(data: bodyData, encoding: .utf8) {
print("Request Body: \(bodyString)")
} else {
print("Request Body is empty or cannot be converted to String.")
}
} catch {
print("Encoding Failed: \(error)")
throw NetworkError.encodingFailed
}

do {
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
print("Invalid response received")
throw NetworkError.networkError
}

print("Response Status Code: \(httpResponse.statusCode)")
print("Response Headers: \(httpResponse.allHeaderFields)")

if (200..<300).contains(httpResponse.statusCode) {
if let responseString = String(data: data, encoding: .utf8) {
print("Response Data: \(responseString)")
} else {
print("Response data is not a valid string.")
}
do {
let result = try JSONDecoder().decode(UpdateMissionStatus.self, from: data)
guard let responseData = result.data else {
print("Data Parsing Error: \(String(data: data, encoding: .utf8) ?? "No data")")
throw NetworkError.dataParsingError
}
print("Response Data: \(responseData)")
} catch {
print("Data Parsing Error: \(error)")
throw NetworkError.dataParsingError
}
} else {
print("Invalid response status code: \(httpResponse.statusCode)")
throw NetworkError.invalidResponse
}
} catch {
print("Network Request Failed: \(error)")
throw NetworkError.networkError
}
}

private func generateURL(constant: String, parameter: String = "") -> URL {
let baseURL = Bundle.main.baseURL
let url = baseURL + constant + "/\(parameter)"
print(url)
return URL(string: url)!
}
}
61 changes: 61 additions & 0 deletions iOS-NOTTODO/Widget-NOTTODO/Provider/MissionProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// MissionProvider.swift
// iOS-NOTTODO
//
// Created by 강윤서 on 5/11/24.
//

import SwiftUI
import WidgetKit

struct Provider: AppIntentTimelineProvider {
@AppStorage("dailyMission", store: UserDefaults.shared) var sharedData: Data = Data()
@AppStorage("quote", store: UserDefaults.shared) var quote: String = ""

func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(todayMission: [], quote: quote)
}

func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
do {
try await getQuote()
} catch {
return SimpleEntry(todayMission: [], quote: "")
}
return SimpleEntry(todayMission: [], quote: quote)
}

func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
do {
let now = Date()
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: now) ?? now
let midnightTomorrow = Calendar.current.startOfDay(for: tomorrow)
let midnightToday = Calendar.current.startOfDay(for: now)

if now == midnightToday {
try await getQuote()
try await getDailyMission(date: Formatter.dateFormatterString(format: nil, date: now))
}

guard let decodedData = try? JSONDecoder().decode([DailyMissionResponseDTO].self, from: sharedData) else {
return Timeline(entries: [], policy: .never)
}

let entry = SimpleEntry(todayMission: decodedData, quote: quote)
return Timeline(entries: [entry], policy: .after(midnightTomorrow))
} catch {
return Timeline(entries: [], policy: .never)
}

}

private func getQuote() async throws {
let quoteResponse = try await WidgetService.shared.fetchWiseSaying()
quote = quoteResponse.description + " - " + quoteResponse.author
}

private func getDailyMission(date: String) async throws {
let dailyMission = try await WidgetService.shared.fetchDailyMissoin(date: date)
UserDefaults.shared?.setSharedCustomArray(dailyMission, forKey: "dailyMission")
}
}
18 changes: 18 additions & 0 deletions iOS-NOTTODO/Widget-NOTTODO/Provider/TimeEntity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// TimeEntity.swift
// iOS-NOTTODO
//
// Created by 강윤서 on 5/11/24.
//

import WidgetKit

struct SimpleEntry: TimelineEntry {
var date: Date = .now

var dayOfWeek: String {
return Formatter.dateFormatterString(format: "E", date: date)
}
var todayMission: [DailyMissionResponseDTO]
let quote: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading