diff --git a/AsyncSwift.xcodeproj/project.pbxproj b/AsyncSwift.xcodeproj/project.pbxproj index 8e225c9..0fa6ab6 100644 --- a/AsyncSwift.xcodeproj/project.pbxproj +++ b/AsyncSwift.xcodeproj/project.pbxproj @@ -10,9 +10,10 @@ B289943328CA69FF002B9F67 /* StampView+Observed.swift in Sources */ = {isa = PBXBuildFile; fileRef = B289943228CA69FF002B9F67 /* StampView+Observed.swift */; }; B2E1083128C9CD6900C3DD59 /* AppData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2E1083028C9CD6900C3DD59 /* AppData.swift */; }; C63A865F28CA70ED0064C417 /* EventDetailView+Observed.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63A865E28CA70ED0064C417 /* EventDetailView+Observed.swift */; }; - C63A866128CB252D0064C417 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63A866028CB252D0064C417 /* Mock.swift */; }; C63A866328CB3D490064C417 /* View+.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63A866228CB3D490064C417 /* View+.swift */; }; C63A866528CB3F6D0064C417 /* DateFormatter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63A866428CB3F6D0064C417 /* DateFormatter+.swift */; }; + C66C68D328D1B00A0091F960 /* EventModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66C68D228D1B00A0091F960 /* EventModel.swift */; }; + C66C68D528D1B0130091F960 /* SessionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66C68D428D1B0130091F960 /* SessionModel.swift */; }; C66DAD5028CF478700195DEB /* SessionView+Observed.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66DAD4F28CF478700195DEB /* SessionView+Observed.swift */; }; C68DE93628C7685800CA4CC8 /* AsyncSwiftApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68DE93528C7685800CA4CC8 /* AsyncSwiftApp.swift */; }; C68DE93828C7685800CA4CC8 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C68DE93728C7685800CA4CC8 /* MainTabView.swift */; }; @@ -27,7 +28,6 @@ C6E744A028CA557100B7B2BD /* Color+.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6E7449F28CA557100B7B2BD /* Color+.swift */; }; C6F7798728C9CB3A0036773B /* StampView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6F7798628C9CB3A0036773B /* StampView.swift */; }; C6F7798B28C9CBC60036773B /* EventView+Observed.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6F7798A28C9CBC60036773B /* EventView+Observed.swift */; }; - C6F7798D28C9CBD80036773B /* EventResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6F7798C28C9CBD80036773B /* EventResponse.swift */; }; C6F7798F28C9D1BF0036773B /* SessionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6F7798E28C9D1BF0036773B /* SessionView.swift */; }; C6F7799128C9E5DD0036773B /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6F7799028C9E5DD0036773B /* EventDetailView.swift */; }; E9171F0028D15426002FAF52 /* TicketingView+Observed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9171EFF28D15426002FAF52 /* TicketingView+Observed.swift */; }; @@ -38,9 +38,10 @@ B289943228CA69FF002B9F67 /* StampView+Observed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StampView+Observed.swift"; sourceTree = ""; }; B2E1083028C9CD6900C3DD59 /* AppData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppData.swift; sourceTree = ""; }; C63A865E28CA70ED0064C417 /* EventDetailView+Observed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventDetailView+Observed.swift"; sourceTree = ""; }; - C63A866028CB252D0064C417 /* Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mock.swift; sourceTree = ""; }; C63A866228CB3D490064C417 /* View+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+.swift"; sourceTree = ""; }; C63A866428CB3F6D0064C417 /* DateFormatter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+.swift"; sourceTree = ""; }; + C66C68D228D1B00A0091F960 /* EventModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventModel.swift; sourceTree = ""; }; + C66C68D428D1B0130091F960 /* SessionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionModel.swift; sourceTree = ""; }; C66DAD4F28CF478700195DEB /* SessionView+Observed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionView+Observed.swift"; sourceTree = ""; }; C68DE93228C7685800CA4CC8 /* AsyncSwift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AsyncSwift.app; sourceTree = BUILT_PRODUCTS_DIR; }; C68DE93528C7685800CA4CC8 /* AsyncSwiftApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSwiftApp.swift; sourceTree = ""; }; @@ -56,7 +57,6 @@ C6E7449F28CA557100B7B2BD /* Color+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+.swift"; sourceTree = ""; }; C6F7798628C9CB3A0036773B /* StampView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StampView.swift; sourceTree = ""; }; C6F7798A28C9CBC60036773B /* EventView+Observed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventView+Observed.swift"; sourceTree = ""; }; - C6F7798C28C9CBD80036773B /* EventResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventResponse.swift; sourceTree = ""; }; C6F7798E28C9D1BF0036773B /* SessionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionView.swift; sourceTree = ""; }; C6F7799028C9E5DD0036773B /* EventDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailView.swift; sourceTree = ""; }; E9171EFF28D15426002FAF52 /* TicketingView+Observed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TicketingView+Observed.swift"; sourceTree = ""; }; @@ -100,7 +100,7 @@ C68DE93528C7685800CA4CC8 /* AsyncSwiftApp.swift */, C68DE94B28C76F3200CA4CC8 /* AppDelegate.swift */, C6E7449E28CA556800B7B2BD /* Extensions */, - C6F7798828C9CB9B0036773B /* Identifiable */, + C6F7798828C9CB9B0036773B /* Models */, C68DE95228C77F4800CA4CC8 /* Views */, C6F7798928C9CBA60036773B /* Observed */, C68DE93928C7685900CA4CC8 /* Assets.xcassets */, @@ -142,13 +142,13 @@ path = Extensions; sourceTree = ""; }; - C6F7798828C9CB9B0036773B /* Identifiable */ = { + C6F7798828C9CB9B0036773B /* Models */ = { isa = PBXGroup; children = ( - C6F7798C28C9CBD80036773B /* EventResponse.swift */, - C63A866028CB252D0064C417 /* Mock.swift */, + C66C68D228D1B00A0091F960 /* EventModel.swift */, + C66C68D428D1B0130091F960 /* SessionModel.swift */, ); - path = Identifiable; + path = Models; sourceTree = ""; }; C6F7798928C9CBA60036773B /* Observed */ = { @@ -247,6 +247,7 @@ C6E744A028CA557100B7B2BD /* Color+.swift in Sources */, C68DE95128C77DDA00CA4CC8 /* TicketingView.swift in Sources */, C6F7798B28C9CBC60036773B /* EventView+Observed.swift in Sources */, + C66C68D528D1B0130091F960 /* SessionModel.swift in Sources */, E9171F0028D15426002FAF52 /* TicketingView+Observed.swift in Sources */, C6F7798728C9CB3A0036773B /* StampView.swift in Sources */, B289943328CA69FF002B9F67 /* StampView+Observed.swift in Sources */, @@ -255,12 +256,11 @@ C68DE93828C7685800CA4CC8 /* MainTabView.swift in Sources */, C6F7799128C9E5DD0036773B /* EventDetailView.swift in Sources */, C68DE93628C7685800CA4CC8 /* AsyncSwiftApp.swift in Sources */, - C63A866128CB252D0064C417 /* Mock.swift in Sources */, E9E2A4D828CEC5680016AEFF /* WebView.swift in Sources */, C63A866328CB3D490064C417 /* View+.swift in Sources */, C63A866528CB3F6D0064C417 /* DateFormatter+.swift in Sources */, C6F7798F28C9D1BF0036773B /* SessionView.swift in Sources */, - C6F7798D28C9CBD80036773B /* EventResponse.swift in Sources */, + C66C68D328D1B00A0091F960 /* EventModel.swift in Sources */, B2E1083128C9CD6900C3DD59 /* AppData.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -395,11 +395,15 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = AsyncSwift/Info.plist; + INFOPLIST_KEY_NSCalendarsUsageDescription = "캘린더에 일정 추가를 위해서는 권한이 필요해요."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UIStatusBarHidden = NO; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -426,11 +430,15 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = AsyncSwift/Info.plist; + INFOPLIST_KEY_NSCalendarsUsageDescription = "캘린더에 일정 추가를 위해서는 권한이 필요해요."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UIStatusBarHidden = NO; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/AsyncSwift.xcodeproj/project.xcworkspace/xcuserdata/kiminsub.xcuserdatad/UserInterfaceState.xcuserstate b/AsyncSwift.xcodeproj/project.xcworkspace/xcuserdata/kiminsub.xcuserdatad/UserInterfaceState.xcuserstate index 27fef93..8f275be 100644 Binary files a/AsyncSwift.xcodeproj/project.xcworkspace/xcuserdata/kiminsub.xcuserdatad/UserInterfaceState.xcuserstate and b/AsyncSwift.xcodeproj/project.xcworkspace/xcuserdata/kiminsub.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/AsyncSwift/AppDelegate.swift b/AsyncSwift/AppDelegate.swift index 8205ae8..422101f 100644 --- a/AsyncSwift/AppDelegate.swift +++ b/AsyncSwift/AppDelegate.swift @@ -64,12 +64,6 @@ class AppDelegate: NSObject, UIApplicationDelegate { let data1Key = "DATA1" let data2Key = "DATA2" - static var orientationLock = UIInterfaceOrientationMask.all - - func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { - return AppDelegate.orientationLock - } - // Register for remote notifications func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { FirebaseApp.configure() diff --git a/AsyncSwift/AsyncSwiftApp.swift b/AsyncSwift/AsyncSwiftApp.swift index 4effa71..8a40c6e 100644 --- a/AsyncSwift/AsyncSwiftApp.swift +++ b/AsyncSwift/AsyncSwiftApp.swift @@ -11,7 +11,7 @@ import SwiftUI struct AsyncSwiftApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate @ObservedObject var appData: AppData = AppData() - + var body: some Scene { WindowGroup { MainTabView() diff --git a/AsyncSwift/Identifiable/EventResponse.swift b/AsyncSwift/Identifiable/EventResponse.swift deleted file mode 100644 index a5eac8c..0000000 --- a/AsyncSwift/Identifiable/EventResponse.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// EventResponse.swift -// AsyncSwift -// -// Created by Kim Insub on 2022/09/08. -// - -import Foundation - -struct EventResponse: Codable, Identifiable { - - let id: Int - -} diff --git a/AsyncSwift/Identifiable/Mock.swift b/AsyncSwift/Identifiable/Mock.swift deleted file mode 100644 index 4d89cfc..0000000 --- a/AsyncSwift/Identifiable/Mock.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// Mock.swift -// AsyncSwift -// -// Created by Kim Insub on 2022/09/09. -// - -import Foundation - -struct Mock { - - static let data: EventModel = EventModel( - event: EventModel.Event( - title: "AsyncSwift Seminar 002", - detailTitle: "AsyncSwift 002", - subject: "🛠생산성 향상[생산썽 향:상]", - description: [ - EventModel.Event.Paragraph(content: "우리가 동료들과 함께 프로젝트를 더 잘 협업할 수 있는 방법은 무엇일까요?"), - EventModel.Event.Paragraph(content: "다 함께 고민하고, 다 같이 나아갈 수 있는 생산성 향상이 힌트가 될 수 있지 않을까 생각이 되었습니다."), - EventModel.Event.Paragraph(content: "같이 잘 나아가고 성장하기 위해 가을의 문턱에서 함께 이야기를 나눠보려합니다.") - ], - date: "Thu, September 22, 2022", - startDate: "2022/09/22 19:00", - endDate: "2022/09/22 23:00", - time: "7:00 PM – 10:00 PM KST", - location: "체인지업 그라운드 포항, 2층 미디어월", - address: "청암로 87, 남구, 포항시, 경상북도 790-390", - hashTags: "#리팩토링 #테스트코드 #모듈화 #디자인패턴 #Architecture", - addressURLs: EventModel.Event.AddressURLs( - naverMapURL: "https://map.naver.com/v5/entry/place/1019717788?c=14396419.6520108,4302029.7423806,15,0,0,0,dh", - kakaoMapURL: "http://kko.to/ONFeYdS33" - ) - ), - sessions: [ - EventModel.Session( - id: 0, - title: "Coupang의 MVVM", - description: [ - EventModel.Session.Paragraph(content: "- 왜 MVVM이 나왔을까요? 주요 구조는 어떤 구조가 있을까요? 고민했던 내용을 공유합니다!"), - EventModel.Session.Paragraph(content: "- MVVM 등장의 이유에 대해 알았으니 어떻게 동작하는지 알아보겠습니다!"), - ], - speaker: EventModel.Session.Speaker( - name: "권문범", - imageURL: "https://firebasestorage.googleapis.com/v0/b/asyncswiftkorea.appspot.com/o/moonbum.jpg?alt=media&token=0ab41b70-d3b0-4430-ac16-c97fdc3027bc", - role: "Coupang | Staff Software Engineer", - description: "오픈 소스 사용 전에 구현 해보는, 직접 구현 하기 전에 앱 구조를 말하는, 앱 구조를 말하기 전에 함수/객체 지향을 논하는, 함수/객체 지향을 논하기 전에 iOS 구조를 공부하는, iOS 구조를 공부하기 전에 자료구조와 알고리즘을 생각하는, 자료구조와 알고리즘을 생각하기 전에 나에게 맞는 플랫폼을 찾는 개발자") - ), - EventModel.Session( - id: 1, - title: "내일 지구가 멸망하더라도 테스트는 같게 동작해야한다.", - description: [ - EventModel.Session.Paragraph(content: "- XCode에서 테스팅을 진행하는 이유와, 간략한 예제를 통해 테스팅을 진행합니다."), - EventModel.Session.Paragraph(content: "- 테스트 더블이 필요한 상황(네트워크와 무관하게 Response처리를 검사해야하는 경우)을 가정할 때 테스트 방식을 소개합니다."), - ], - speaker: EventModel.Session.Speaker( - name: "김찬우", - imageURL: "https://firebasestorage.googleapis.com/v0/b/asyncswiftkorea.appspot.com/o/woosung.jpg?alt=media&token=14991e8d-5d09-4666-a8df-c40d7cda5b88", - role: "Coda, iOS 교육 설계", - description: "iOS 개발과 교육 그 사이 어딘가에서 머무르고 있습니다.\n서비스를 통해 세상의 문제를 해결하는 것을 즐거워합니다.") - ), - EventModel.Session( - id: 2, - title: "내일 지구가 멸망하더라도 테스트는 같게 동작해야한다", - description: [ - EventModel.Session.Paragraph(content: "- XCode에서 테스팅을 진행하는 이유와, 간략한 예제를 통해 테스팅을 진행합니다."), - EventModel.Session.Paragraph(content: "- 테스트 더블이 필요한 상황(네트워크와 무관하게 Response처리를 검사해야하는 경우)을 가정할 때 테스트 방식을 소개합니다."), - EventModel.Session.Paragraph(content: "- 길어지는 경우에 이렇게 들어갑니다. XCode에서 테스팅을 진행하는 이유와, 간략한 예제를 통해 테스팅을 진행합니다."), - EventModel.Session.Paragraph(content: "- 밑의 여백을 추가해요.테스트 더블이 필요한 상황(네트워크와 무관하게 Response처리를 검사해야하는 경우)을 가정할 때 테스트 방식을 소개합니다.") - ], - speaker: EventModel.Session.Speaker( - name: "김찬우", - imageURL: "", - role: "Coda, iOS 교육 설계", - description: "iOS 개발과 교육 그 사이 어딘가에서 머무르고 있습니다.\n서비스를 통해 세상의 문제를 해결하는 것을 즐거워합니다.") - ), - EventModel.Session( - id: 3, - title: "내일 지구가 멸망하더라도 테스트는 같게 동작해야한다", - description: [ - EventModel.Session.Paragraph(content: "- XCode에서 테스팅을 진행하는 이유와, 간략한 예제를 통해 테스팅을 진행합니다."), - EventModel.Session.Paragraph(content: "- 테스트 더블이 필요한 상황(네트워크와 무관하게 Response처리를 검사해야하는 경우)을 가정할 때 테스트 방식을 소개합니다."), - EventModel.Session.Paragraph(content: "- 길어지는 경우에 이렇게 들어갑니다. XCode에서 테스팅을 진행하는 이유와, 간략한 예제를 통해 테스팅을 진행합니다."), - EventModel.Session.Paragraph(content: "- 밑의 여백을 추가해요.테스트 더블이 필요한 상황(네트워크와 무관하게 Response처리를 검사해야하는 경우)을 가정할 때 테스트 방식을 소개합니다.") - ], - speaker: EventModel.Session.Speaker( - name: "김찬우", - imageURL: "", - role: "Coda, iOS 교육 설계", - description: "iOS 개발과 교육 그 사이 어딘가에서 머무르고 있습니다.\n서비스를 통해 세상의 문제를 해결하는 것을 즐거워합니다.") - ), - EventModel.Session( - id: 4, - title: "내일 지구가 멸망하더라도 테스트는 같게 동작해야한다", - description: [ - EventModel.Session.Paragraph(content: "- XCode에서 테스팅을 진행하는 이유와, 간략한 예제를 통해 테스팅을 진행합니다."), - EventModel.Session.Paragraph(content: "- 테스트 더블이 필요한 상황(네트워크와 무관하게 Response처리를 검사해야하는 경우)을 가정할 때 테스트 방식을 소개합니다."), - EventModel.Session.Paragraph(content: "- 길어지는 경우에 이렇게 들어갑니다. XCode에서 테스팅을 진행하는 이유와, 간략한 예제를 통해 테스팅을 진행합니다."), - EventModel.Session.Paragraph(content: "- 밑의 여백을 추가해요.테스트 더블이 필요한 상황(네트워크와 무관하게 Response처리를 검사해야하는 경우)을 가정할 때 테스트 방식을 소개합니다.") - ], - speaker: EventModel.Session.Speaker( - name: "김찬우", - imageURL: "", - role: "Coda, iOS 교육 설계", - description: "iOS 개발과 교육 그 사이 어딘가에서 머무르고 있습니다.\n서비스를 통해 세상의 문제를 해결하는 것을 즐거워합니다.") - ), - EventModel.Session( - id: 5, - title: "내일 지구가 멸망하더라도 테스트는 같게 동작해야한다", - description: [ - EventModel.Session.Paragraph(content: "- XCode에서 테스팅을 진행하는 이유와, 간략한 예제를 통해 테스팅을 진행합니다."), - EventModel.Session.Paragraph(content: "- 테스트 더블이 필요한 상황(네트워크와 무관하게 Response처리를 검사해야하는 경우)을 가정할 때 테스트 방식을 소개합니다."), - EventModel.Session.Paragraph(content: "- 길어지는 경우에 이렇게 들어갑니다. XCode에서 테스팅을 진행하는 이유와, 간략한 예제를 통해 테스팅을 진행합니다."), - EventModel.Session.Paragraph(content: "- 밑의 여백을 추가해요.테스트 더블이 필요한 상황(네트워크와 무관하게 Response처리를 검사해야하는 경우)을 가정할 때 테스트 방식을 소개합니다.") - ], - speaker: EventModel.Session.Speaker( - name: "김찬우", - imageURL: "", - role: "Coda, iOS 교육 설계", - description: "iOS 개발과 교육 그 사이 어딘가에서 머무르고 있습니다.\n서비스를 통해 세상의 문제를 해결하는 것을 즐거워합니다.") - ) - ] - ) -} diff --git a/AsyncSwift/Info.plist b/AsyncSwift/Info.plist index 85946ad..04b25f1 100644 --- a/AsyncSwift/Info.plist +++ b/AsyncSwift/Info.plist @@ -2,8 +2,6 @@ - NSCalendarsUsageDescription - 캘린더에 일정 추가를 위해서는 권한이 필요해요. CFBundleURLTypes diff --git a/AsyncSwift/Models/EventModel.swift b/AsyncSwift/Models/EventModel.swift new file mode 100644 index 0000000..c7da583 --- /dev/null +++ b/AsyncSwift/Models/EventModel.swift @@ -0,0 +1,50 @@ +// +// EventModel.swift +// AsyncSwift +// +// Created by Kim Insub on 2022/09/14. +// + +import Foundation + +struct Event: Codable { + + init() { + self.title = "AsyncSwift" + self.detailTitle = "AsyncSwift" + self.subject = "" + self.type = "세미나" + self.description = [] + self.date = "" + self.startDate = "" + self.endDate = "" + self.time = "" + self.location = "" + self.address = "" + self.hashTags = "" + self.addressURLs = AddressURLs(naverMapURL: "", kakaoMapURL: "") + self.sessions = [] + } + + var title, detailTitle, subject, type: String + var description: [Paragraph] + var date, startDate, endDate, time: String + var location, address, hashTags: String + var addressURLs: AddressURLs + var sessions: [Session] + + enum CodingKeys: String, CodingKey { + case title, detailTitle, subject, type + case description = "description" + case date, startDate, endDate, time, location, address, hashTags, addressURLs, sessions + } + + struct Paragraph: Codable, Hashable { + var content: String + } + + struct AddressURLs: Codable { + var naverMapURL: String + var kakaoMapURL: String + } +} diff --git a/AsyncSwift/Models/SessionModel.swift b/AsyncSwift/Models/SessionModel.swift new file mode 100644 index 0000000..f532102 --- /dev/null +++ b/AsyncSwift/Models/SessionModel.swift @@ -0,0 +1,37 @@ +// +// SessionModel.swift +// AsyncSwift +// +// Created by Kim Insub on 2022/09/14. +// + +import Foundation + +struct Session: Codable, Identifiable { + + var id: Int + var title: String + var description: [Paragraph] + var speaker: Speaker + + enum CodingKeys: String, CodingKey { + case id, title + case description = "description" + case speaker + } + + struct Paragraph: Codable, Hashable { + var content: String + } + + struct Speaker: Codable { + var name: String + var imageURL: String + var role, description: String + + enum CodingKeys: String, CodingKey { + case name, imageURL, role + case description = "description" + } + } +} diff --git a/AsyncSwift/Observed/EventDetailView+Observed.swift b/AsyncSwift/Observed/EventDetailView+Observed.swift index 5ed472e..d916f60 100644 --- a/AsyncSwift/Observed/EventDetailView+Observed.swift +++ b/AsyncSwift/Observed/EventDetailView+Observed.swift @@ -5,19 +5,22 @@ // Created by Kim Insub on 2022/09/09. // -import Foundation import EventKit +import SwiftUI extension EventDetailView { final class Observed: ObservableObject { + init(event: Event) { + self.event = event + } + + @Published var event: Event @Published var isShowingSheet = false @Published var isShowingAddEventConfirmationAlert = false @Published var isShowingAddEventSuccessAlert = false @Published var isShowingAddEventFailureAlert = false - let data = Mock.data - func additionConfirmed() { addEventOnCalendar { isSuccess in DispatchQueue.main.async { [weak self] in @@ -43,10 +46,10 @@ extension EventDetailView { } let event = EKEvent(eventStore: eventStore) let formatter = DateFormatter.calendarFormatter - event.title = self.data.event.title - event.location = self.data.event.location - event.startDate = formatter.date(from: self.data.event.startDate) - event.endDate = formatter.date(from: self.data.event.endDate) + event.title = self.event.title + event.location = self.event.location + event.startDate = formatter.date(from: self.event.startDate) + event.endDate = formatter.date(from: self.event.endDate) event.calendar = eventStore.defaultCalendarForNewEvents do { try eventStore.save(event, span: .thisEvent) diff --git a/AsyncSwift/Observed/EventView+Observed.swift b/AsyncSwift/Observed/EventView+Observed.swift index b22e5af..b595a4d 100644 --- a/AsyncSwift/Observed/EventView+Observed.swift +++ b/AsyncSwift/Observed/EventView+Observed.swift @@ -5,10 +5,66 @@ // Created by Kim Insub on 2022/09/08. // -import Foundation +import SwiftUI extension EventView { final class Observed: ObservableObject { - + + @Published var event = Event() + @Published var eventStatus: EventStatus = .upcoming + + init() { + fetchJson() + } + + func fetchJson() { + guard let url = URL(string: "https://async-swift.github.io/jsonstorage/asyncswift.json") else { return } + let request = URLRequest(url: url) + let dataTask = URLSession.shared.dataTask(with: request) { data, response, _ in + guard + let response = response as? HTTPURLResponse, + response.statusCode == 200, + let data = data + else { return } + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + do { + let decodedData = try JSONDecoder().decode(Event.self, from: data) + withAnimation { + self.event = decodedData + } + self.calculateEventStatus() + } catch let error { + print("❌ \(error.localizedDescription)") + } + } + } + dataTask.resume() + } + + func calculateEventStatus() { + let formatter = DateFormatter.calendarFormatter + guard + let start = formatter.date(from: event.startDate), + let end = formatter.date(from: event.endDate) + else { return } + let currentDate = Date() + + if currentDate < start { + self.eventStatus = .upcoming + } else if start <= currentDate && currentDate < end { + self.eventStatus = .onProgress + } else if currentDate > end { + self.eventStatus = .done + } + } + } +} + +extension EventView { + enum EventStatus: String { + case upcoming = "예정된 행사" + case onProgress = "진행중인 행사" + case done = "지나간 행사" } } diff --git a/AsyncSwift/Observed/SessionView+Observed.swift b/AsyncSwift/Observed/SessionView+Observed.swift index a31b7a4..efc63e7 100644 --- a/AsyncSwift/Observed/SessionView+Observed.swift +++ b/AsyncSwift/Observed/SessionView+Observed.swift @@ -9,6 +9,12 @@ import SwiftUI extension SessionView { final class Observed: ObservableObject { + + init(session: Session) { + self.session = session + } + + @Published var session: Session let speakerImageSize: CGFloat = 80 } } diff --git a/AsyncSwift/Views/EventDetailView.swift b/AsyncSwift/Views/EventDetailView.swift index 12bdc8b..c90e7ee 100644 --- a/AsyncSwift/Views/EventDetailView.swift +++ b/AsyncSwift/Views/EventDetailView.swift @@ -9,11 +9,10 @@ import SwiftUI struct EventDetailView: View { - private let event: EventModel.Event - @StateObject var observed = Observed() + @ObservedObject var observed: Observed - init(event: EventModel.Event) { - self.event = event + init(event: Event) { + observed = Observed(event: event) } var body: some View { @@ -27,12 +26,12 @@ struct EventDetailView: View { Spacer() } } - .navigationTitle(event.detailTitle) + .navigationTitle(observed.event.detailTitle) .confirmationDialog("", isPresented: $observed.isShowingSheet, titleVisibility: .hidden) { - if let naverMapURL = URL(string: event.addressURLs.naverMapURL) { + if let naverMapURL = URL(string: observed.event.addressURLs.naverMapURL) { Link("네이버 지도로 길 찾기", destination: naverMapURL) } - if let kakaoMapURL = URL(string: event.addressURLs.kakaoMapURL) { + if let kakaoMapURL = URL(string: observed.event.addressURLs.kakaoMapURL) { Link("카카오맵으로 길 찾기", destination: kakaoMapURL) } } @@ -43,14 +42,14 @@ private extension EventDetailView { var description: some View { VStack(alignment: .leading) { - Text(event.subject) + Text(observed.event.subject) .fontWeight(.bold) .font(.title3) - ForEach(event.description, id:\.self) { paragraph in + ForEach(observed.event.description, id:\.self) { paragraph in Text(paragraph.content) .font(.body) } - Text(event.hashTags) + Text(observed.event.hashTags) .padding(.top, 8) .foregroundColor(.gray) .font(.body) @@ -65,7 +64,7 @@ private extension EventDetailView { Text("\(Image(systemName: "calendar")) Date and time") .font(.title3) .fontWeight(.semibold) - Text("\(event.date)\n\(event.time)") + Text("\(observed.event.date)\n\(observed.event.time)") .font(.body) Button("캘린더에 추가") { observed.isShowingAddEventConfirmationAlert = true @@ -86,14 +85,14 @@ private extension EventDetailView { }, message: { Text("등록에 오류가 발생했습니다.\n다시 시도하십시오.") }) - + } VStack(alignment: .leading, spacing: 8) { Text("\(Image(systemName: "location.fill")) Location") .font(.title3) .fontWeight(.semibold) VStack(alignment: .leading) { - Text(event.location) - Text(event.address) + Text(observed.event.location) + Text(observed.event.address) } Button { observed.isShowingSheet = true @@ -104,6 +103,5 @@ private extension EventDetailView { } .padding(.horizontal, 24) .padding(.vertical, 30) - } } } diff --git a/AsyncSwift/Views/EventView.swift b/AsyncSwift/Views/EventView.swift index fb72162..bbaa32f 100644 --- a/AsyncSwift/Views/EventView.swift +++ b/AsyncSwift/Views/EventView.swift @@ -5,75 +5,22 @@ // Created by Kim Insub on 2022/09/06. // -/* TODO: 내용 - 1. light mode only - 2. lock landscape - 3. 예정된 행사 테그 구현하기 - */ - import SwiftUI -struct EventModel { - - let event: Event - let sessions: [Session] - - struct Event { - - var title: String - var detailTitle: String - var subject: String - var description: [Paragraph] - var date: String - var startDate: String - var endDate: String - var time: String - var location: String - var address: String - var hashTags: String - var addressURLs: AddressURLs +struct EventView: View { - struct Paragraph: Hashable { - var content: String - } + @ObservedObject var observed: Observed - struct AddressURLs { - var naverMapURL: String - var kakaoMapURL: String - } + init() { + observed = Observed() } - struct Session: Identifiable { - - let id: Int - var title: String - var description: [Paragraph] - var speaker: Speaker - - struct Speaker { - var name: String - var imageURL: String - var role: String - var description: String - } - - struct Paragraph: Hashable { - var content: String - } - } -} - - -struct EventView: View { - - private let data = Mock.data - var body: some View { NavigationView { ScrollView { Header LazyVStack { - ForEach(data.sessions) { session in + ForEach(observed.event.sessions) { session in makeSessionCell(for: session) } } @@ -87,11 +34,11 @@ private extension EventView { var Header: some View { VStack(alignment: .leading, spacing: 8) { - Text(data.event.subject) + Text(observed.event.subject) .font(.title) .fontWeight(.bold) HStack { - Text(data.event.title) + Text(observed.event.title) .font(.caption2) .fontWeight(.bold) .foregroundColor(.white) @@ -99,12 +46,22 @@ private extension EventView { .padding(.horizontal, 8) .background(Color.seminarOrange) .cornerRadius(20) + Text(observed.eventStatus.rawValue) + .font(.caption2) + .fontWeight(.bold) + .foregroundColor(Color.accentColor) + .padding(.vertical, 4) + .padding(.horizontal, 8) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(Color.accentColor, lineWidth: 1) + ) Spacer() } NavigationLink { - EventDetailView(event: data.event) + EventDetailView(event: observed.event) } label: { - Text("세미나 살펴보기 \(Image(systemName: "arrow.right"))") + Text("\(observed.event.type) 살펴보기 \(Image(systemName: "arrow.right"))") .font(.footnote) .fontWeight(.bold) } @@ -114,7 +71,7 @@ private extension EventView { } @ViewBuilder - func makeSessionCell(for session: EventModel.Session) -> some View { + func makeSessionCell(for session: Session) -> some View { NavigationLink { SessionView(session: session) } label: { diff --git a/AsyncSwift/Views/SessionView.swift b/AsyncSwift/Views/SessionView.swift index c504d2a..456fdc0 100644 --- a/AsyncSwift/Views/SessionView.swift +++ b/AsyncSwift/Views/SessionView.swift @@ -9,11 +9,10 @@ import SwiftUI struct SessionView: View { - private let session: EventModel.Session - private let observed = Observed() + @ObservedObject var observed: Observed - init(session: EventModel.Session) { - self.session = session + init(session: Session) { + observed = Observed(session: session) } var body: some View { @@ -36,50 +35,60 @@ struct SessionView: View { private extension SessionView { var sessionDetail: some View { - VStack(alignment: .leading, spacing: 0) { - Text(session.title) - .font(.title3) - .fontWeight(.semibold) - .padding(.vertical, 24) - VStack(alignment: .leading, spacing: 8) { - ForEach(session.description, id: \.self) { paragraph in - Text(paragraph.content) + HStack(spacing: 0) { + VStack(alignment: .leading, spacing: 0) { + Text(observed.session.title) + .font(.title3) + .fontWeight(.semibold) + .padding(.vertical, 24) + VStack(alignment: .leading, spacing: 8) { + ForEach(observed.session.description, id: \.self) { paragraph in + Text(paragraph.content) + } } + .padding(.bottom, 80) } - .padding(.bottom, 80) + .padding(.horizontal, 24) + Spacer() } - .padding(.horizontal, 24) } var speakerDetail: some View { - VStack(alignment: .leading, spacing: 4) { - AsyncImage(url: URL(string: session.speaker.imageURL)) { image in - image - .resizable() - .aspectRatio(contentMode: .fit) - .clipShape(Circle()) - .frame(width: observed.speakerImageSize, height: observed.speakerImageSize) - } placeholder: { - Image(systemName: "person.circle") - .resizable() - .aspectRatio(contentMode: .fit) - .clipShape(Circle()) - .frame(width: observed.speakerImageSize, height: observed.speakerImageSize) - .opacity(0.4) - } - .padding(.vertical, 24) - VStack(alignment: .leading, spacing: 2) { - Text("\(session.speaker.name) 님") - .font(.headline) - Text(session.speaker.role) - .font(.caption2) + + HStack(spacing: 0) { + VStack(alignment: .leading, spacing: 4) { + AsyncImage(url: URL(string: observed.session.speaker.imageURL), transaction: Transaction(animation: .default)) { phase in + if let image = phase.image { + image + .resizable() + } else if phase.error != nil { + Image(systemName: "person.crop.circle.fill") + .resizable() + .opacity(0.04) + } else { + Image(systemName: "person.crop.circle.fill") + .resizable() + .opacity(0.04) + } + } + .aspectRatio(contentMode: .fit) + .frame(width: observed.speakerImageSize, height: observed.speakerImageSize) + .clipShape(Circle()) + .padding(.vertical, 24) + VStack(alignment: .leading, spacing: 2) { + Text("\(observed.session.speaker.name) 님") + .font(.headline) + Text(observed.session.speaker.role) + .font(.caption2) + } + Text(observed.session.speaker.description) + .font(.footnote) } - Text(session.speaker.description) - .font(.footnote) + .padding(.horizontal, 32) + .padding(.bottom, 60) + + Spacer() } - .padding(.bottom, 60) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding(.horizontal, 32) .background(Color.speakerBackground) } }