├── AsyncSwift ├── Assets.xcassets │ ├── Contents.json │ ├── Colors │ │ ├── Contents.json │ │ ├── 0A66C2.colorset │ │ │ └── Contents.json │ │ ├── 1920FF.colorset │ │ │ └── Contents.json │ │ ├── 6C6C70.colorset │ │ │ └── Contents.json │ │ ├── AFAFAF.colorset │ │ │ └── Contents.json │ │ ├── D9D9D9.colorset │ │ │ └── Contents.json │ │ ├── E5E5EA.colorset │ │ │ └── Contents.json │ │ ├── EAEAEA.colorset │ │ │ └── Contents.json │ │ ├── profileGray.colorset │ │ │ └── Contents.json │ │ ├── buttonBackground.colorset │ │ │ └── Contents.json │ │ ├── myProfileBackground.colorset │ │ │ └── Contents.json │ │ ├── seminarOrange.colorset │ │ │ └── Contents.json │ │ ├── skeletonBackground.colorset │ │ │ └── Contents.json │ │ ├── speakerBackground.colorset │ │ │ └── Contents.json │ │ ├── dividerForeground.colorset │ │ │ └── Contents.json │ │ ├── placeholderForeground.colorset │ │ │ └── Contents.json │ │ ├── emptyTicketViewBackground.colorset │ │ │ └── Contents.json │ │ ├── emptyTicketViewForeground.colorset │ │ │ └── Contents.json │ │ └── unavailableButtonBackground.colorset │ │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Logo.png │ │ └── Contents.json │ ├── QRplaceholder.imageset │ │ ├── 􀎹.png │ │ └── Contents.json │ ├── Seminar002StampBack.imageset │ │ ├── Seminar002StampBack.pdf │ │ └── Contents.json │ ├── Seminar002StampFront.imageset │ │ ├── Contents.json │ │ └── Seminar002StampFront.pdf │ ├── AccentColor.colorset │ │ └── Contents.json │ └── HashTagForeground.colorset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Extensions │ ├── Text+.swift │ ├── TextField+.swift │ ├── TextEditor+.swift │ ├── DateFormatter+.swift │ ├── String+.swift │ ├── SafariView.swift │ ├── WebView.swift │ ├── View+.swift │ └── Color+.swift ├── Models │ ├── Stamp.swift │ ├── SessionModel.swift │ ├── Tab.swift │ ├── User.swift │ ├── EventModel.swift │ └── Ticketing.swift ├── AsyncSwiftApp.swift ├── AsyncSwift.entitlements ├── Managers │ ├── GitHubStorageURL.swift │ ├── KeyChainManager.swift │ └── FirebaseManager.swift ├── Views │ ├── MainTabView.swift │ ├── StampView.swift │ ├── SessionView.swift │ ├── EventDetailView.swift │ ├── Profile │ │ ├── ProfileFriendDetailView.swift │ │ ├── ProfileFriendsListView.swift │ │ ├── ProfileEditView.swift │ │ ├── ProfileRegisterView.swift │ │ └── ProfileView.swift │ ├── TicketingView.swift │ └── EventView.swift ├── Observed │ ├── MainTabView+Observed.swift │ ├── TicketingView+Observed.swift │ ├── ProfileView │ │ ├── ProfileFriendDetailViewObserved.swift │ │ ├── ProfileFriendsListViewObserved.swift │ │ ├── ProfileEditViewObserved.swift │ │ ├── ProfileRegisterViewObserved.swift │ │ └── ProfileViewObserved.swift │ ├── EventView+Observed.swift │ ├── EventDetailView+Observed.swift │ └── StampView+Observed.swift ├── Info.plist ├── GoogleService-Info.plist └── AppDelegate.swift ├── AsyncSwiftWidget ├── Assets.xcassets │ ├── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── WidgetBackground.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── AsyncSwiftWidgetBundle.swift ├── Info.plist ├── AsyncSwiftWidgetEntryView.swift └── AsyncSwiftWidget.swift ├── AsyncSwift.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── kiminsub.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcuserdata │ └── kiminsub.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── pull_request_template.md ├── .github └── ISSUE_TEMPLATE │ └── issus-template.md ├── AsyncSwiftWidgetExtension.entitlements ├── README.md └── .gitignore /AsyncSwift/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AsyncSwiftWidget/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AsyncSwift/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/AppIcon.appiconset/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Async-Swift/AsyncSwift/HEAD/AsyncSwift/Assets.xcassets/AppIcon.appiconset/Logo.png -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/QRplaceholder.imageset/􀎹.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Async-Swift/AsyncSwift/HEAD/AsyncSwift/Assets.xcassets/QRplaceholder.imageset/􀎹.png -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Seminar002StampBack.imageset/Seminar002StampBack.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Async-Swift/AsyncSwift/HEAD/AsyncSwift/Assets.xcassets/Seminar002StampBack.imageset/Seminar002StampBack.pdf -------------------------------------------------------------------------------- /AsyncSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | 4 | # Next TODO 5 | 6 | 7 | # Reference 8 | 9 | 10 | Close 11 | -------------------------------------------------------------------------------- /AsyncSwiftWidget/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AsyncSwiftWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AsyncSwift.xcodeproj/project.xcworkspace/xcuserdata/kiminsub.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Async-Swift/AsyncSwift/HEAD/AsyncSwift.xcodeproj/project.xcworkspace/xcuserdata/kiminsub.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/QRplaceholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "􀎹.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /AsyncSwiftWidget/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Logo.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /AsyncSwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AsyncSwift/Extensions/Text+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Text+.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/10/17. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Text { 11 | var profileInputTitle: some View { 12 | self.font(.headline) 13 | .frame(minWidth: 58, minHeight: 18, alignment: .leading) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AsyncSwift/Extensions/TextField+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextField+.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/10/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension TextField { 11 | var profileTextField: some View { 12 | self 13 | .font(.subheadline) 14 | .frame(minHeight: 20) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /AsyncSwift/Models/Stamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stamp.swift 3 | // AsyncSwift 4 | // 5 | // Created by Inho Choi on 2022/09/15. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Stamp: Decodable { 11 | let title: String 12 | } 13 | 14 | struct Card { 15 | 16 | var originalPosition: CGFloat 17 | var image: Image 18 | var event: String 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issus-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issus Template 3 | about: "\b이슈 생성을 위한 템플릿입니다." 4 | title: "[작업 태그] 이슈 제목" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 배경 11 | - 12 | 13 | 14 | ## 내용 15 | - 16 | 17 | 18 | ## 작업 범위 19 | - [ ] 20 | -------------------------------------------------------------------------------- /AsyncSwift/Extensions/TextEditor+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextEditor+.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/10/28. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension TextEditor { 11 | var profileTextEditor: some View { 12 | self 13 | .font(.subheadline) 14 | .frame(height: 53) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /AsyncSwiftWidget/AsyncSwiftWidgetBundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncSwiftWidgetBundle.swift 3 | // AsyncSwiftWidget 4 | // 5 | // Created by 김인섭 on 2023/10/01. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | 11 | @main 12 | struct AsyncSwiftWidgetBundle: WidgetBundle { 13 | var body: some Widget { 14 | AsyncSwiftWidget() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Seminar002StampBack.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Seminar002StampBack.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Seminar002StampFront.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Seminar002StampFront.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AsyncSwiftWidgetExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.com.kim.AsyncSwift 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /AsyncSwiftWidget/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.widgetkit-extension 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /AsyncSwift/AsyncSwiftApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncSwiftApp.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/09/06. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct AsyncSwiftApp: App { 12 | @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate 13 | 14 | var body: some Scene { 15 | WindowGroup { 16 | MainTabView() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /AsyncSwift/Extensions/DateFormatter+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatter+.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/09/09. 6 | // 7 | 8 | import Foundation 9 | 10 | extension DateFormatter { 11 | static var calendarFormatter: DateFormatter { 12 | let formatter = DateFormatter() 13 | formatter.dateFormat = "yyyy/MM/dd HH:mm" 14 | return formatter 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x38", 9 | "green" : "0x51", 10 | "red" : "0xEE" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/0A66C2.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xC2", 9 | "green" : "0x66", 10 | "red" : "0x0A" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/1920FF.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0x20", 10 | "red" : "0x19" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/6C6C70.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x70", 9 | "green" : "0x6C", 10 | "red" : "0x6C" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/AFAFAF.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xAF", 9 | "green" : "0xAF", 10 | "red" : "0xAF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/D9D9D9.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xD9", 9 | "green" : "0xD9", 10 | "red" : "0xD9" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/E5E5EA.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEA", 9 | "green" : "0xE5", 10 | "red" : "0xE5" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/EAEAEA.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEA", 9 | "green" : "0xEA", 10 | "red" : "0xEA" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/profileGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x93", 9 | "green" : "0x8E", 10 | "red" : "0x8E" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/buttonBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF3", 9 | "green" : "0xF3", 10 | "red" : "0xF3" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/myProfileBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFB", 9 | "green" : "0xFB", 10 | "red" : "0xFB" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/seminarOrange.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.149", 9 | "green" : "0.267", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/skeletonBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xD6", 9 | "green" : "0xD1", 10 | "red" : "0xD1" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/speakerBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF5", 9 | "green" : "0xF4", 10 | "red" : "0xF4" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/dividerForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.929", 9 | "green" : "0.929", 10 | "red" : "0.929" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/placeholderForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x76", 9 | "green" : "0x76", 10 | "red" : "0x76" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/emptyTicketViewBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFB", 9 | "green" : "0xFB", 10 | "red" : "0xFB" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/emptyTicketViewForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xD8", 9 | "green" : "0xD8", 10 | "red" : "0xD8" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Colors/unavailableButtonBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xD6", 9 | "green" : "0xD6", 10 | "red" : "0xD2" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AsyncSwift/AsyncSwift.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.associated-domains 8 | 9 | applinks:asyncswift.info 10 | 11 | com.apple.security.application-groups 12 | 13 | group.com.kim.AsyncSwift 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /AsyncSwift/Extensions/String+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Any+.swift 3 | // AsyncSwift 4 | // 5 | // Created by Inho Choi on 2022/10/07. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | func convertToStringArray() -> [String]? { 12 | guard let stringData = self.data(using: .utf8) else { 13 | return nil 14 | } 15 | 16 | var result = [String]() 17 | do { 18 | result = try JSONDecoder().decode([String].self, from: stringData) 19 | } catch { 20 | return nil 21 | } 22 | return result 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /AsyncSwift/Extensions/SafariView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariView.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/11/04. 6 | // 7 | 8 | import SwiftUI 9 | import SafariServices 10 | 11 | struct SafariView: UIViewControllerRepresentable { 12 | let url: URL 13 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> SFSafariViewController { 14 | return SFSafariViewController(url: url) 15 | } 16 | 17 | func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext) { } 18 | } 19 | -------------------------------------------------------------------------------- /AsyncSwiftWidget/AsyncSwiftWidgetEntryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncSwiftWidgetEntryView.swift 3 | // AsyncSwift 4 | // 5 | // Created by 김인섭 on 2023/10/01. 6 | // 7 | 8 | import SwiftUI 9 | import WidgetKit 10 | import SVGKit 11 | 12 | struct AsyncSwiftWidgetEntryView : View { 13 | var entry: Provider.Entry 14 | 15 | var body: some View { 16 | if let imageData = entry.imageData, let image = SVGKImage(data: imageData) { 17 | Image(uiImage: image.uiImage) 18 | .resizable() 19 | .scaledToFill() 20 | .offset(y: 10) 21 | } else { 22 | Text("다음 행사때 만나요. 🤗") 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /AsyncSwift/Extensions/WebView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebView.swift 3 | // AsyncSwift 4 | // 5 | // Created by Eunyeong Kim on 2022/09/09. 6 | // 7 | 8 | import SwiftUI 9 | import WebKit 10 | 11 | struct WebView: UIViewRepresentable { 12 | 13 | let url: String 14 | 15 | func makeUIView(context: Context) -> WKWebView { 16 | let webView = WKWebView() 17 | 18 | return webView 19 | } 20 | 21 | func updateUIView(_ uiView: WKWebView, context: Context) { 22 | uiView.scrollView.isScrollEnabled = true 23 | 24 | 25 | if let url = URL(string: url) { 26 | uiView.load(URLRequest(url: url)) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /AsyncSwift/Managers/GitHubStorageURL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitHubStorageURL.swift 3 | // AsyncSwift 4 | // 5 | // Created by 김인섭 on 10/8/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum GitHubStorageURL { 11 | 12 | static let baseUrl = "https://async-swift.github.io/jsonstorage" 13 | static let customDomain = "https://asyncswift.info/" 14 | 15 | static let widgetLargeImage = customDomain + "/Images/widget-large.svg" 16 | static let eventData = GitHubStorageURL.baseUrl + "/asyncswift.json" 17 | static var stampImage: (String) -> String {{ event in 18 | GitHubStorageURL.baseUrl + "/Images/Stamp/" + event + "/stamp.png" 19 | }} 20 | static let ticketingData = GitHubStorageURL.baseUrl + "/ticketing.json" 21 | } 22 | -------------------------------------------------------------------------------- /AsyncSwift/Views/MainTabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/09/06. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MainTabView: View { 11 | @StateObject var observed = MainTabViewObserved() 12 | 13 | init() { 14 | UITabBar.appearance().backgroundColor = UIColor.white 15 | } 16 | 17 | var body: some View { 18 | TabView(selection: $observed.currentTab) { 19 | ForEach(Tab.allCases, id: \.self) { tab in 20 | tab.view.tabItem { 21 | Image(systemName: tab.systemImageName) 22 | Text(tab.title) 23 | } 24 | .environmentObject(observed) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /AsyncSwift/Observed/MainTabView+Observed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppData.swift 3 | // AsyncSwift 4 | // 5 | // Created by Inho Choi on 2022/09/08. 6 | // 7 | 8 | import SwiftUI 9 | 10 | final class MainTabViewObserved: ObservableObject { 11 | /// Universal Link로 앱진입시 StampView 전환을 위한 변수 12 | @Published var currentTab: Tab = .event 13 | private let keyChainManager = KeyChainManager() 14 | 15 | init() { 16 | fixKeyChain() 17 | } 18 | 19 | 20 | // MARK: 버전 1의 실수를 바로 잡습니다. @Toby 21 | /// "seminar002"가 key로 들어가 있던 기존 코드를 삭제하는 함수입니다. 22 | /// - KeyChain에 저장되는 방식을 개선하고자 함수가 구현되었습니다. 23 | func fixKeyChain() { 24 | let isKeyDelete = keyChainManager.deleteItem(key: "seminar002") 25 | if isKeyDelete { 26 | keyChainManager.addItem(key: keyChainManager.stampKey, pwd: ["Seminar002"].description) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /AsyncSwift/Models/SessionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionModel.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/09/14. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Session: Codable, Identifiable { 11 | 12 | var id: Int 13 | var title: String 14 | var description: [Paragraph] 15 | var speaker: Speaker 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case id, title 19 | case description = "description" 20 | case speaker 21 | } 22 | 23 | struct Paragraph: Codable, Hashable { 24 | var content: String 25 | } 26 | 27 | struct Speaker: Codable { 28 | var name: String 29 | var imageURL: String 30 | var role, description: String 31 | 32 | enum CodingKeys: String, CodingKey { 33 | case name, imageURL, role 34 | case description = "description" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/HashTagForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x93", 9 | "green" : "0x93", 10 | "red" : "0x93" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /AsyncSwift/Models/Tab.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tab.swift 3 | // AsyncSwift 4 | // 5 | // Created by Inho Choi on 2022/10/08. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum Tab: String, CaseIterable { 11 | case event = "Event" 12 | case ticketing = "Ticketing" 13 | case stamp = "Stamp" 14 | case profile = "Profile" 15 | 16 | var title: String { 17 | rawValue 18 | } 19 | 20 | var systemImageName: String { 21 | switch self { 22 | case .event: return "calendar" 23 | case .ticketing: return "banknote" 24 | case .stamp: return "checkmark.square" 25 | case .profile: return "person.circle.fill" 26 | } 27 | } 28 | 29 | @ViewBuilder 30 | var view: some View { 31 | switch self { 32 | case .event: EventView() 33 | case .ticketing: TicketingView() 34 | case .stamp: StampView() 35 | case .profile: ProfileView() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /AsyncSwift/Extensions/View+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/09/09. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | @ViewBuilder 12 | var customDivider: some View { 13 | Rectangle() 14 | .fill(Color.dividerForeground) 15 | .frame(height: 3) 16 | .edgesIgnoringSafeArea(.horizontal) 17 | } 18 | 19 | func placeholder( 20 | when shouldShow: Bool, 21 | text: String, 22 | isTextField: Bool 23 | ) -> some View { 24 | ZStack(alignment: .leading) { 25 | Text(text) 26 | .font(.subheadline) 27 | .foregroundColor(.placeholderForeground) 28 | .frame(height: 20) 29 | .opacity(shouldShow ? 1 : 0) 30 | .offset(x: isTextField ? 0.0 : 3.0, y: isTextField ? 0.0 : -8.0) 31 | self 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /AsyncSwift.xcodeproj/xcuserdata/kiminsub.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | AsyncSwift.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | Promises (Playground) 1.xcscheme 13 | 14 | isShown 15 | 16 | orderHint 17 | 2 18 | 19 | Promises (Playground) 2.xcscheme 20 | 21 | isShown 22 | 23 | orderHint 24 | 3 25 | 26 | Promises (Playground).xcscheme 27 | 28 | isShown 29 | 30 | orderHint 31 | 1 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /AsyncSwift/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | CFBundleURLTypes 11 | 12 | 13 | CFBundleTypeRole 14 | Editor 15 | CFBundleURLSchemes 16 | 17 | com.googleusercontent.apps.388113408677-32e2k6kqi1ags2n6bmompvpuu196ci0k 18 | 19 | 20 | 21 | CFBundleTypeRole 22 | Editor 23 | CFBundleURLName 24 | Deep Link 25 | CFBundleURLSchemes 26 | 27 | asyncswift 28 | 29 | 30 | 31 | FirebaseAppDelegateProxyEnabled 32 | 33 | UIBackgroundModes 34 | 35 | remote-notification 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /AsyncSwift/Extensions/Color+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/09/09. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Color { 11 | static let seminarOrange = Color("seminarOrange") 12 | static let dividerForeground = Color("dividerForeground") 13 | static let emptyTicketViewBackground = Color("emptyTicketViewBackground") 14 | static let emptyTicketViewForeground = Color("emptyTicketViewForeground") 15 | static let speakerBackground = Color("speakerBackground") 16 | static let skeletonBackground = Color("skeletonBackground") 17 | static let unavailableButtonBackground = Color("unavailableButtonBackground") 18 | static let placeholderForeground = Color("placeholderForeground") 19 | static let profileGray = Color("profileGray") 20 | static let buttonBackground = Color("buttonBackground") 21 | static let profileFontGrayForeground = Color("6C6C70") 22 | static let linkedInBlueBackground = Color("0A66C2") 23 | static let inActiveButtonBackground = Color("E5E5EA") 24 | static let skeletonQR = Color("AFAFAF") 25 | static let skeletonName = Color("D9D9D9") 26 | static let skeletonDescription = Color("EAEAEA") 27 | static let asyncBlue = Color("1920FF") 28 | } 29 | -------------------------------------------------------------------------------- /AsyncSwift/Models/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/11/03. 6 | // 7 | 8 | import Foundation 9 | 10 | struct User: Identifiable { 11 | init() { 12 | self.id = "" 13 | self.name = "" 14 | self.nickname = "" 15 | self.role = "" 16 | self.description = "" 17 | self.linkedInURL = "" 18 | self.profileURL = "" 19 | self.friends = [] 20 | } 21 | 22 | init( 23 | id: String, 24 | name: String, 25 | nickname: String, 26 | role: String, 27 | description: String, 28 | linkedInURL: String, 29 | profileURL: String, 30 | friends: [String] 31 | ) { 32 | self.id = id 33 | self.name = name 34 | self.nickname = nickname 35 | self.role = role 36 | self.description = description 37 | self.linkedInURL = linkedInURL 38 | self.profileURL = profileURL 39 | self.friends = friends 40 | } 41 | 42 | var id: String 43 | var name: String 44 | var nickname: String 45 | var role: String 46 | var description: String 47 | var linkedInURL: String 48 | var profileURL: String 49 | var friends: [String] 50 | } 51 | -------------------------------------------------------------------------------- /AsyncSwift/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 388113408677-32e2k6kqi1ags2n6bmompvpuu196ci0k.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.388113408677-32e2k6kqi1ags2n6bmompvpuu196ci0k 9 | API_KEY 10 | AIzaSyCghy3j9aCxys2wv4ohWeLnBm6oqVrGD6A 11 | GCM_SENDER_ID 12 | 388113408677 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | com.kim.AsyncSwift 17 | PROJECT_ID 18 | asyncswiftkorea 19 | STORAGE_BUCKET 20 | asyncswiftkorea.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:388113408677:ios:03e5ba8ae5a2f613ce234c 33 | DATABASE_URL 34 | https://asyncswiftkorea-default-rtdb.asia-southeast1.firebasedatabase.app 35 | 36 | 37 | -------------------------------------------------------------------------------- /AsyncSwift/Models/EventModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventModel.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/09/14. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Event: Codable { 11 | 12 | init() { 13 | self.title = "AsyncSwift" 14 | self.detailTitle = "AsyncSwift" 15 | self.subject = "" 16 | self.type = "세미나" 17 | self.description = [] 18 | self.date = "" 19 | self.startDate = "" 20 | self.endDate = "" 21 | self.time = "" 22 | self.location = "" 23 | self.address = "" 24 | self.hashTags = "" 25 | self.addressURLs = AddressURLs(naverMapURL: "", kakaoMapURL: "") 26 | self.sessions = [] 27 | } 28 | 29 | var title, detailTitle, subject, type: String 30 | var description: [Paragraph] 31 | var date, startDate, endDate, time: String 32 | var location, address, hashTags: String 33 | var addressURLs: AddressURLs 34 | var sessions: [Session] 35 | 36 | enum CodingKeys: String, CodingKey { 37 | case title, detailTitle, subject, type 38 | case description = "description" 39 | case date, startDate, endDate, time, location, address, hashTags, addressURLs, sessions 40 | } 41 | 42 | struct Paragraph: Codable, Hashable { 43 | var content: String 44 | } 45 | 46 | struct AddressURLs: Codable { 47 | var naverMapURL: String 48 | var kakaoMapURL: String 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /AsyncSwift/Models/Ticketing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ticketing.swift 3 | // AsyncSwift 4 | // 5 | // Created by Eunyeong Kim on 2022/09/15. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Ticketing: Decodable { 11 | let currentTicket: CurrentTicket? 12 | let upcomingEvent: UpcomingEvent? 13 | 14 | struct CurrentTicket: Decodable { 15 | let ticketingImageURL: String 16 | let ticketingURL: String 17 | let date: String 18 | } 19 | 20 | struct UpcomingEvent: Decodable { 21 | let headerTitle: String 22 | let title: String 23 | let subscription: String 24 | private let gradientStartColor: RGBColor 25 | private let gradientEndColor: RGBColor 26 | 27 | var backgroundGradientStartColor: Color { 28 | makeColor(from: gradientStartColor) 29 | } 30 | 31 | var backgroundGradientEndColor: Color { 32 | makeColor(from: gradientEndColor) 33 | } 34 | 35 | private func makeColor(from rgbColor: RGBColor) -> Color { 36 | Color( 37 | red: rgbColor.red / 255.0, 38 | green: rgbColor.green / 255.0, 39 | blue: rgbColor.blue / 255.0, 40 | opacity: rgbColor.opacity / 100.0 41 | ) 42 | } 43 | 44 | struct RGBColor: Decodable { 45 | let red: Double 46 | let green: Double 47 | let blue: Double 48 | let opacity: Double 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /AsyncSwift/Observed/TicketingView+Observed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TicketingView+Observed.swift 3 | // AsyncSwift 4 | // 5 | // Created by Eunyeong Kim on 2022/09/09. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | extension TicketingView { 12 | final class Observed: ObservableObject { 13 | @Published var ticketing: Ticketing? 14 | @Published var isActivatedWebViewNavigationLink = false 15 | var cancellable = Set() 16 | 17 | var hasAvailableTicket: Bool { 18 | let currentDate = Date() 19 | return currentDate <= DateFormatter.calendarFormatter.date(from: ticketing?.currentTicket?.date ?? "") ?? Date() 20 | } 21 | 22 | var isTicketingLinkDisabled: Bool { 23 | ticketing?.currentTicket?.ticketingURL == nil && !hasAvailableTicket 24 | } 25 | 26 | func getTicketingData() { 27 | let url = URL(string: GitHubStorageURL.ticketingData)! 28 | URLSession.shared.dataTaskPublisher(for: url) 29 | .map(\.data) 30 | .decode(type: Ticketing.self, decoder: JSONDecoder()) 31 | .receive(on: RunLoop.main) 32 | .sink { _ in 33 | 34 | } receiveValue: { [weak self] event in 35 | self?.ticketing = event 36 | } 37 | .store(in: &cancellable) 38 | } 39 | 40 | func didTappedTicketingButton() { 41 | isActivatedWebViewNavigationLink = true 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AsyncSwift 2 | ## AsyncSwift란? 3 | ![스크린샷 2022-12-19 오후 9 43 34](https://user-images.githubusercontent.com/55151796/209364676-179c792e-b8f0-4213-aaf0-e9961b5d29ac.png) 4 | 5 | --- 6 | ## AsyncSwift 앱 소개 7 | ### 1. 행사 안내 8 | ``` 9 | 컨퍼런스가 앞으로 계획되어있다면 앞으로 열릴 컨퍼런스에 대한 발표 소개 및 연사자를 소개해줍니다. 10 | 컨퍼런스 계획이 아직 잡혀있지 않다면 가장 최신의 정보를 제공하게 됩니다. 11 | ``` 12 |

13 | 14 |

15 | 16 | ### 2. 티켓팅 안내 17 | ``` 18 | 컨퍼런스가 계획되어져 있다면 컨퍼런스 포스터와 함께 컨퍼런스 티켓(유/무료) 구매페이지로 안내되어집니다. 19 | 컨퍼런스 계획이 잡혀 있지 않다면 아래와 같이 비어있는 화면이 보여지게 됩니다. 20 | ``` 21 |

22 | 23 |

24 | 25 | ### 3. 디지털 Stamp 26 | ``` 27 | 컨퍼런스에서 얻을 수 있는 QR코드(DeepLink)를 찍으면 디지털 Stamp를 얻을 수 있습니다. 28 | 디지털 Stamp는 아래와 같이 애니메이션이 작동합니다. 29 | ``` 30 |

31 | 32 | 33 | 34 | 35 | ### 4. Profile공유 36 | ``` 37 | AsyncSwift에서 주최한 행사의 After Party에서 즐기실 수 있는 프로필 교환 및 작성을 위한 기능입니다. 38 | 서로의 QR코드 스캔을 통해서 프로필을 교환할 수 있습니다. 39 | ``` 40 |

41 | 42 |

43 | 44 | --- 45 | ## 사용 기술 46 | ```Swift 47 | DeepLink 48 | Firebase 49 | KeyChain 50 | ``` 51 | -------------------------------------------------------------------------------- /AsyncSwift/Observed/ProfileView/ProfileFriendDetailViewObserved.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileFriendDetailViewObserved.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/11/04. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum PreviousView { 11 | case ProfileView 12 | case ListView 13 | } 14 | 15 | @MainActor 16 | final class ProfileFriendDetailViewObserved: ObservableObject { 17 | @Binding var inActive: Bool 18 | @Published var isShowingLinkedInSheet = false 19 | @Published var isShowingProfileSheet = false 20 | @Published var isShowingConfirmAlert = false 21 | let previous: PreviousView 22 | let friend: User 23 | var user: User 24 | var profileURL: URL? { 25 | URL(string: friend.profileURL) 26 | } 27 | var linkedInURL: URL? { 28 | URL(string: friend.linkedInURL) 29 | } 30 | var hasProfileURL: Bool { 31 | get { 32 | !friend.profileURL.isEmpty 33 | } 34 | } 35 | var hasLinkedInURL: Bool { 36 | get { 37 | !friend.linkedInURL.isEmpty 38 | } 39 | } 40 | 41 | init(inActive: Binding, user: User, friend: User, previous: PreviousView) { 42 | self._inActive = inActive 43 | self.friend = friend 44 | self.user = user 45 | self.previous = previous 46 | } 47 | 48 | func didTapDoneButton() { 49 | inActive = false 50 | } 51 | 52 | func didTapDeleteButton() { 53 | isShowingConfirmAlert = true 54 | } 55 | 56 | func didConfirmDelete() { 57 | Task { 58 | await removeFriendFromList() 59 | inActive = false 60 | } 61 | } 62 | } 63 | 64 | private extension ProfileFriendDetailViewObserved { 65 | func removeFriendFromList() async { 66 | let removedList = user.friends.filter { $0 != friend.id } 67 | user.friends = removedList 68 | FirebaseManager.shared.editUser(user: user) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /AsyncSwiftWidget/AsyncSwiftWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncSwiftWidget.swift 3 | // AsyncSwiftWidget 4 | // 5 | // Created by 김인섭 on 2023/10/01. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | 11 | struct Provider: TimelineProvider { 12 | func placeholder(in context: Context) -> SimpleEntry { 13 | SimpleEntry(date: Date(), imageData: getRemoteImage()) 14 | } 15 | 16 | func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { 17 | let entry = SimpleEntry(date: Date(), imageData: getRemoteImage()) 18 | completion(entry) 19 | } 20 | 21 | func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { 22 | 23 | let entry = SimpleEntry(date: Date(), imageData: getRemoteImage()) 24 | let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) 25 | let timeline = Timeline(entries: [entry], policy: .after(nextUpdate!)) 26 | completion(timeline) 27 | } 28 | 29 | func getRemoteImage() -> Data? { 30 | try? Data(contentsOf: URL(string: GitHubStorageURL.widgetLargeImage)!) 31 | } 32 | } 33 | 34 | struct SimpleEntry: TimelineEntry { 35 | var date: Date 36 | let imageData: Data? 37 | } 38 | 39 | struct AsyncSwiftWidget: Widget { 40 | let kind: String = "AsyncSwiftWidget" 41 | 42 | var body: some WidgetConfiguration { 43 | StaticConfiguration(kind: kind, provider: Provider()) { entry in 44 | AsyncSwiftWidgetEntryView(entry: entry) 45 | } 46 | .configurationDisplayName("AsyncSwift") 47 | .description("행사 정보를 확인하세요.") 48 | .supportedFamilies( 49 | [.systemLarge] 50 | ) 51 | } 52 | } 53 | 54 | struct AsyncSwiftWidget_Previews: PreviewProvider { 55 | static var previews: some View { 56 | AsyncSwiftWidgetEntryView( 57 | entry: SimpleEntry( 58 | date: Date(), 59 | imageData: try? Data(contentsOf: URL(string: GitHubStorageURL.widgetLargeImage)!) 60 | ) 61 | ) 62 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /AsyncSwift/Observed/EventView+Observed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventView+Observed.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/09/08. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | final class EventViewObserved: ObservableObject { 12 | 13 | @Published var event = Event() 14 | @Published var eventStatus: EventStatus = .upcoming 15 | @Published var isLoading = true 16 | let onLoadingCells = Array(repeating: [0], count: 6) 17 | var cancellable = Set() 18 | 19 | func getEventData() { 20 | let url = URL(string: GitHubStorageURL.eventData)! 21 | URLSession.shared.dataTaskPublisher(for: url) 22 | .map(\.data) 23 | .decode(type: Event.self, decoder: JSONDecoder()) 24 | .receive(on: RunLoop.main) 25 | .sink { _ in 26 | 27 | } receiveValue: { [weak self] event in 28 | self?.event = event 29 | self?.calculateEventStatus() 30 | self?.isLoading = false 31 | } 32 | .store(in: &cancellable) 33 | } 34 | 35 | func calculateEventStatus() { 36 | let formatter = DateFormatter.calendarFormatter 37 | guard 38 | let start = formatter.date(from: event.startDate), 39 | let end = formatter.date(from: event.endDate) 40 | else { return } 41 | let currentDate = Date() 42 | 43 | if currentDate < start { 44 | self.eventStatus = .upcoming 45 | } else if start <= currentDate && currentDate < end { 46 | self.eventStatus = .onProgress 47 | } else if currentDate > end { 48 | self.eventStatus = .done 49 | } 50 | } 51 | } 52 | 53 | 54 | extension EventViewObserved { 55 | enum EventStatus: String { 56 | case upcoming = "예정된 행사" 57 | case onProgress = "진행중인 행사" 58 | case done = "지나간 행사" 59 | 60 | var statusColor: Color { 61 | switch self { 62 | case .upcoming: return Color.accentColor 63 | case .onProgress: return Color.asyncBlue 64 | case .done: return Color.black 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /AsyncSwift/Observed/EventDetailView+Observed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventDetailView+Observed.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/09/09. 6 | // 7 | 8 | import EventKit 9 | import SwiftUI 10 | 11 | extension EventDetailView { 12 | 13 | final class Observed: ObservableObject { 14 | 15 | init(event: Event) { 16 | self.event = event 17 | } 18 | 19 | let event: Event 20 | @Published var isShowingSheet = false 21 | @Published var isShowingAddEventConfirmationAlert = false 22 | @Published var isShowingAddEventSuccessAlert = false 23 | @Published var isShowingAddEventFailureAlert = false 24 | 25 | func additionConfirmed() { 26 | addEventOnCalendar { [weak self] isSuccess in 27 | DispatchQueue.main.async { 28 | switch isSuccess { 29 | case true: 30 | self?.isShowingAddEventSuccessAlert = true 31 | case false: 32 | self?.isShowingAddEventFailureAlert = true 33 | } 34 | } 35 | } 36 | } 37 | 38 | func addEventOnCalendar(completion: @escaping ((Bool) -> Void) ) { 39 | let eventStore = EKEventStore() 40 | 41 | eventStore.requestAccess(to: .event) { [weak self] (granted, error) in 42 | guard let self, error == nil else { return } 43 | let event = EKEvent(eventStore: eventStore) 44 | let formatter = DateFormatter.calendarFormatter 45 | event.title = self.event.title 46 | event.location = self.event.location 47 | event.startDate = formatter.date(from: self.event.startDate) 48 | event.endDate = formatter.date(from: self.event.endDate) 49 | event.calendar = eventStore.defaultCalendarForNewEvents 50 | do { 51 | try eventStore.save(event, span: .thisEvent) 52 | completion(true) 53 | } catch let error as NSError { 54 | print("failed to save event with error : \(error)") 55 | completion(false) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /AsyncSwift/Views/StampView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StampView.swift 3 | // AsyncSwift 4 | // 5 | // Created by Inho Choi on 2022/10/29. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StampView: View { 11 | @StateObject var observed = Observed() 12 | @EnvironmentObject var envObserved: MainTabViewObserved 13 | let columns = [ 14 | GridItem(.flexible(), spacing: 10), 15 | GridItem(.flexible()) 16 | ] 17 | 18 | var body: some View { 19 | 20 | GeometryReader { proxy in 21 | NavigationView { 22 | ScrollView(showsIndicators: false) { 23 | ScrollViewReader { reader in 24 | LazyVGrid( 25 | columns: columns, 26 | spacing: 10 27 | ) { 28 | ForEach(observed.cards, id: \.event) { card in 29 | cardView(card: card, size: proxy.size) 30 | } 31 | } 32 | .padding(.horizontal, 14) 33 | } 34 | } 35 | .navigationTitle(Tab.stamp.title) 36 | .overlay { 37 | if observed.isLoading { 38 | loadingIndicator 39 | } else if !observed.isLoading, observed.cards.isEmpty { 40 | emptyCardView 41 | .padding(36) 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | private extension StampView { 50 | 51 | @ViewBuilder var loadingIndicator: some View { 52 | ProgressView() 53 | .scaleEffect(1.5) 54 | .padding(30) 55 | .background(.ultraThinMaterial) 56 | .cornerRadius(10) 57 | } 58 | 59 | @ViewBuilder var emptyCardView: some View { 60 | ZStack { 61 | RoundedRectangle(cornerRadius: 30) 62 | .strokeBorder(Color(red: 0.78, green: 0.78, blue: 0.8), style: StrokeStyle(lineWidth: 2, dash: [10])) 63 | Text("아직 참여한 행사가 없습니다.") 64 | .foregroundColor(.gray) 65 | } 66 | } 67 | 68 | @ViewBuilder func cardView(card: Card, size: CGSize) -> some View { 69 | card.image 70 | .resizable() 71 | .aspectRatio(contentMode: .fill) 72 | .shadow(color: Color.black.opacity(0.1), radius: 10, y: 4) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /AsyncSwift/Managers/KeyChainManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyChain.swift 3 | // AsyncSwift 4 | // 5 | // Created by Inho Choi on 2022/09/15. 6 | // 7 | 8 | import UIKit 9 | 10 | final class KeyChainManager { 11 | let stampKey = "AsyncSwiftStamp" 12 | 13 | @discardableResult func addItem(key: Any, pwd: Any) -> Bool { 14 | let addQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword, 15 | kSecAttrAccount: key, 16 | kSecValueData: (pwd as AnyObject).data(using: String.Encoding.utf8.rawValue) as Any] 17 | let status = SecItemAdd(addQuery as CFDictionary, nil) 18 | 19 | switch status { 20 | case errSecSuccess: 21 | return true 22 | case errSecDuplicateItem: 23 | return updateItem(value: pwd, key: key) 24 | default: 25 | print("addItem Error : \(status.description))") 26 | return false 27 | } 28 | } 29 | 30 | func getItem(key: Any) -> Any? { 31 | let getQuery: [CFString: Any] = [ 32 | kSecClass: kSecClassGenericPassword, 33 | kSecAttrAccount: key, 34 | kSecReturnAttributes: true, 35 | kSecReturnData: true 36 | ] 37 | var item: CFTypeRef? 38 | let result = SecItemCopyMatching(getQuery as CFDictionary, &item) 39 | 40 | if result == errSecSuccess, 41 | let existingItem = item as? [String: Any], 42 | let data = existingItem[kSecValueData as String] as? Data, 43 | let password = String(data: data, encoding: .utf8) { 44 | return password 45 | } 46 | print("getItem Error : \(result.description)") 47 | return nil 48 | } 49 | 50 | func updateItem(value: Any, key: Any) -> Bool { 51 | let prevQuery: [CFString: Any] = [ 52 | kSecClass: kSecClassGenericPassword, 53 | kSecAttrAccount: key 54 | ] 55 | let updateQuery: [CFString: Any] = [kSecValueData: (value as AnyObject).data(using: String.Encoding.utf8.rawValue) as Any] 56 | 57 | let result: Bool = { 58 | let status = SecItemUpdate(prevQuery as CFDictionary, updateQuery as CFDictionary) 59 | return status == errSecSuccess 60 | }() 61 | 62 | return result 63 | } 64 | 65 | func deleteItem(key: String) -> Bool { 66 | let deleteQuery: [CFString: Any] = [ 67 | kSecClass: kSecClassGenericPassword, 68 | kSecAttrAccount: key 69 | ] 70 | let status = SecItemDelete(deleteQuery as CFDictionary) 71 | if status == errSecSuccess { 72 | return true 73 | } else { 74 | print("deleteItem Error : \(status.description)") 75 | return false 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/swift,firebase,xcode 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,firebase,xcode 3 | 4 | ### Custom ### 5 | **/.DS_Store 6 | 7 | 8 | ### Firebase ### 9 | .idea 10 | **/node_modules/* 11 | **/.firebaserc 12 | 13 | ### Firebase Patch ### 14 | .runtimeconfig.json 15 | .firebase/ 16 | 17 | ### Swift ### 18 | # Xcode 19 | # 20 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 21 | 22 | ## User settings 23 | xcuserdata/ 24 | 25 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 26 | *.xcscmblueprint 27 | *.xccheckout 28 | 29 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 30 | build/ 31 | DerivedData/ 32 | *.moved-aside 33 | *.pbxuser 34 | !default.pbxuser 35 | *.mode1v3 36 | !default.mode1v3 37 | *.mode2v3 38 | !default.mode2v3 39 | *.perspectivev3 40 | !default.perspectivev3 41 | 42 | ## Obj-C/Swift specific 43 | *.hmap 44 | 45 | ## App packaging 46 | *.ipa 47 | *.dSYM.zip 48 | *.dSYM 49 | 50 | ## Playgrounds 51 | timeline.xctimeline 52 | playground.xcworkspace 53 | 54 | # Swift Package Manager 55 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 56 | # Packages/ 57 | # Package.pins 58 | # Package.resolved 59 | # *.xcodeproj 60 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 61 | # hence it is not needed unless you have added a package configuration file to your project 62 | # .swiftpm 63 | 64 | .build/ 65 | 66 | # CocoaPods 67 | # We recommend against adding the Pods directory to your .gitignore. However 68 | # you should judge for yourself, the pros and cons are mentioned at: 69 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 70 | # Pods/ 71 | # Add this line if you want to avoid checking in source code from the Xcode workspace 72 | # *.xcworkspace 73 | 74 | # Carthage 75 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 76 | # Carthage/Checkouts 77 | 78 | Carthage/Build/ 79 | 80 | # Accio dependency management 81 | Dependencies/ 82 | .accio/ 83 | 84 | # fastlane 85 | # It is recommended to not store the screenshots in the git repo. 86 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 87 | # For more information about the recommended setup visit: 88 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 89 | 90 | fastlane/report.xml 91 | fastlane/Preview.html 92 | fastlane/screenshots/**/*.png 93 | fastlane/test_output 94 | 95 | # Code Injection 96 | # After new code Injection tools there's a generated folder /iOSInjectionProject 97 | # https://github.com/johnno1962/injectionforxcode 98 | 99 | iOSInjectionProject/ 100 | 101 | ### Xcode ### 102 | 103 | ## Xcode 8 and earlier 104 | 105 | ### Xcode Patch ### 106 | *.xcodeproj/* 107 | !*.xcodeproj/project.pbxproj 108 | !*.xcodeproj/xcshareddata/ 109 | !*.xcworkspace/contents.xcworkspacedata 110 | /*.gcno 111 | **/xcshareddata/WorkspaceSettings.xcsettings 112 | 113 | # End of https://www.toptal.com/developers/gitignore/api/swift,firebase,xcode 114 | -------------------------------------------------------------------------------- /AsyncSwift/Managers/FirebaseManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirebaseManager.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/11/03. 6 | // 7 | 8 | import Firebase 9 | import Foundation 10 | 11 | final class FirebaseManager: ObservableObject { 12 | static let shared = FirebaseManager() 13 | let db = Firestore.firestore() 14 | private init() { } 15 | } 16 | 17 | extension FirebaseManager { 18 | func createUser(user: User) { 19 | let docRef = db.collection("users").document(user.id) 20 | let docData: [String: Any] = [ 21 | "id": user.id, 22 | "name": user.name, 23 | "nickname": user.nickname, 24 | "role": user.role, 25 | "description": user.description, 26 | "linkedInURL": user.linkedInURL, 27 | "profileURL": user.profileURL, 28 | "friends": [] 29 | ] 30 | docRef.setData(docData) { error in 31 | if let error = error { 32 | // TODO: error 일 경우 Alert Message 보내기 33 | print("Error writing document: \(error)") 34 | } else { 35 | print("Document successfully written") 36 | } 37 | } 38 | } 39 | 40 | func getUserBy(id: String, completion: @escaping (User) -> Void) { 41 | let docRef = db.collection("users").document(id) 42 | docRef.getDocument { (document, error) in 43 | guard error == nil, 44 | let document = document, 45 | document.exists, 46 | let data = document.data(), 47 | let id = data["id"] as? String, // TODO : Optional 에서 변경하기 48 | let name = data["name"] as? String, 49 | let nickname = data["nickname"] as? String, 50 | let role = data["role"] as? String, 51 | let description = data["description"] as? String, 52 | let linkedInURL = data["linkedInURL"] as? String, 53 | let profileURL = data["profileURL"] as? String, 54 | let friends = data["friends"] as? [String] 55 | else { return } 56 | 57 | let user = User( 58 | id: id, 59 | name: name, 60 | nickname: nickname, 61 | role: role, 62 | description: description, 63 | linkedInURL: linkedInURL, 64 | profileURL: profileURL, 65 | friends: friends 66 | ) 67 | completion(user) 68 | } 69 | } 70 | 71 | func editUser(user: User) { 72 | let docRef = db.collection("users").document(user.id) 73 | let docData: [String: Any] = [ 74 | "id": user.id, 75 | "name": user.name, 76 | "nickname": user.nickname, 77 | "role": user.role, 78 | "description": user.description, 79 | "linkedInURL": user.linkedInURL, 80 | "profileURL": user.profileURL, 81 | "friends": user.friends 82 | ] 83 | 84 | docRef.setData(docData) { error in 85 | if let error = error { 86 | // TODO: error 일 경우 Alert Message 보내기 87 | print("Error writing document: \(error)") 88 | } else { 89 | print("Document successfully editted") 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /AsyncSwift/Observed/ProfileView/ProfileFriendsListViewObserved.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileFriendsListViewObserved.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/11/03. 6 | // 7 | 8 | import CodeScanner 9 | import Combine 10 | import SwiftUI 11 | 12 | @MainActor 13 | final class ProfileFriendsListViewObserved: ObservableObject { 14 | @Binding var inActive: Bool 15 | @Published var isShowingUserDetail = false { 16 | didSet { 17 | if isShowingUserDetail == false { 18 | DispatchQueue.main.async { [weak self] in 19 | guard let self = self else { return } 20 | self.inActive = false 21 | } 22 | } 23 | } 24 | } 25 | @Published var isLoading = true 26 | @Published var isShowingScanner = false 27 | @Published var isShowingScanErrorAlert = false 28 | @Published var friendsList: [User] = [] 29 | @Published var scannedFriend: User = .init() 30 | 31 | var user: User 32 | 33 | init(inActive: Binding, user: User) { 34 | self._inActive = inActive 35 | self.user = user 36 | } 37 | 38 | func onAppear() { 39 | Task { 40 | await getFriendsByID() 41 | isLoading = false 42 | } 43 | } 44 | 45 | func didTapXButton() { 46 | isShowingScanner = false 47 | } 48 | 49 | func handleScan(result: Result) { 50 | switch result { 51 | case .success(let success): 52 | let uuidString = success.string 53 | handleScanSuccess(id: uuidString) 54 | case .failure(_): 55 | isShowingScanner = false 56 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in 57 | guard let self = self else { return } 58 | self.isShowingScanErrorAlert = true 59 | } 60 | } 61 | } 62 | } 63 | 64 | private extension ProfileFriendsListViewObserved { 65 | func handleScanSuccess(id: String) { 66 | guard UUID(uuidString: id) != nil 67 | else { 68 | isShowingScanner = false 69 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in 70 | guard let self = self else { return } 71 | self.isShowingScanErrorAlert = true 72 | } 73 | return 74 | } 75 | Task { 76 | user.friends.append(id) 77 | FirebaseManager.shared.editUser(user: self.user) 78 | await getFriendByID(id: id) 79 | isShowingScanner = false 80 | isShowingUserDetail = true 81 | } 82 | } 83 | 84 | func getFriendByID(id: String) async { 85 | FirebaseManager.shared.getUserBy(id: id) { [weak self] user in 86 | guard let self = self else { return } 87 | self.scannedFriend = user 88 | } 89 | } 90 | 91 | func getFriendsByID() async { 92 | friendsList = [] 93 | for friendID in self.user.friends { 94 | FirebaseManager.shared.getUserBy(id: friendID) { [weak self] user in 95 | guard let self = self else { return } 96 | self.friendsList.append(user) 97 | } 98 | } 99 | } 100 | 101 | func isNewFriend(id: String) -> Bool { 102 | return !user.friends.contains(id) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /AsyncSwift/Views/SessionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionView.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/09/08. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSwiftUI 10 | 11 | struct SessionView: View { 12 | 13 | let session: Session 14 | let speakerImageSize: CGFloat = 80 15 | 16 | var body: some View { 17 | ScrollView { 18 | Group { 19 | customDivider 20 | .padding(.top, 10) 21 | .padding(.bottom, 4) 22 | sessionDetail 23 | } 24 | .background(.white) 25 | speakerDetail 26 | } 27 | .navigationTitle("Session") 28 | } 29 | } 30 | 31 | private extension SessionView { 32 | 33 | var sessionDetail: some View { 34 | VStack(alignment: .leading, spacing: 0) { 35 | HStack { 36 | Text(session.title) 37 | .font(.title3) 38 | .fontWeight(.semibold) 39 | .padding(.vertical, 24) 40 | Spacer(minLength: 0) 41 | } 42 | VStack(alignment: .leading, spacing: 8) { 43 | ForEach(session.description, id: \.self) { paragraph in 44 | Text(paragraph.content) 45 | } 46 | } 47 | .padding(.bottom, 80) 48 | } 49 | .frame(width: UIScreen.main.bounds.width - 32) 50 | } 51 | 52 | var speakerDetail: some View { 53 | 54 | HStack(spacing: 0) { 55 | VStack(alignment: .leading, spacing: 4) { 56 | 57 | WebImage(url: URL(string: session.speaker.imageURL)) 58 | .resizable() 59 | .placeholder { 60 | Image(systemName: "person.crop.circle.fill") 61 | .resizable() 62 | .frame(width: speakerImageSize, height: speakerImageSize) 63 | .opacity(0.04) 64 | } 65 | .transition(.fade) 66 | .frame(width: speakerImageSize, height: speakerImageSize) 67 | .scaledToFit() 68 | .clipShape(Circle()) 69 | .padding(.vertical, 24) 70 | 71 | VStack(alignment: .leading, spacing: 2) { 72 | Text("\(session.speaker.name) 님") 73 | .font(.headline) 74 | Text(session.speaker.role) 75 | .font(.caption2) 76 | } 77 | Text(session.speaker.description) 78 | .font(.footnote) 79 | } 80 | .padding(.horizontal, 32) 81 | .padding(.bottom, 60) 82 | 83 | Spacer() 84 | } 85 | .background(Color.speakerBackground) 86 | } 87 | } 88 | 89 | struct SessionView_Previews: PreviewProvider { 90 | static var previews: some View { 91 | SessionView( 92 | session: .init( 93 | id: 0, 94 | title: "[Event] 사이드 프로젝트가 메인 JOB이 되기까지의 이야기", 95 | description: [ 96 | .init(content: "사이드 프로젝트가 메인 JOB이 되기까지 이야기") 97 | ], 98 | speaker: .init( 99 | name: "박성은", 100 | imageURL: "https://github.com/Async-Swift/jsonstorage/blob/main/Images/Speaker/syncswift2023/hyeonjung.png?raw=true", 101 | role: "북적 스튜디오 | iOS Developer", 102 | description: "iOS 개발자 입니다." 103 | ) 104 | ) 105 | ) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /AsyncSwift/Observed/ProfileView/ProfileEditViewObserved.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileEditViewObserved.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | @MainActor 11 | final class ProfileEditViewObserved: ObservableObject { 12 | 13 | @Published var isShowingSuccessAlert = false 14 | @Published var isShowingFailureAlert = false 15 | @Published var isShowingInputFailureAlert = false 16 | 17 | @Published var user: User 18 | @Published var description = "" { 19 | didSet { 20 | if description.count >= 80 { 21 | description = oldValue 22 | } 23 | } 24 | } 25 | @Published var linkedInURL = "" { 26 | didSet { 27 | print(self.verifyURL(urlString: profileURL)) 28 | self.isLinkedinURLValidated = self.verifyURL(urlString: linkedInURL) 29 | } 30 | } 31 | @Published var profileURL = "" { 32 | didSet { 33 | print(self.verifyURL(urlString: profileURL)) 34 | self.isProfileURLValidated = self.verifyURL(urlString: profileURL) 35 | } 36 | } 37 | var isLinkedinURLValidated = true 38 | var isProfileURLValidated = true 39 | 40 | init(user: User) { 41 | self.description = user.description 42 | self.linkedInURL = user.linkedInURL 43 | self.profileURL = user.profileURL 44 | self.user = user 45 | } 46 | 47 | func didTapRegisterButton() { 48 | register() 49 | } 50 | 51 | func isButtonAvailable() -> Bool { 52 | !user.name.isEmpty && !user.role.isEmpty 53 | } 54 | } 55 | 56 | private extension ProfileEditViewObserved { 57 | func register() { 58 | guard isButtonAvailable() else { return } 59 | if !linkedInURL.isEmpty && !profileURL.isEmpty { 60 | if isLinkedinURLValidated && isProfileURLValidated { 61 | handleSuccess() 62 | } else { 63 | showFailureAlert() 64 | } 65 | } else if !linkedInURL.isEmpty { 66 | if isLinkedinURLValidated { 67 | handleSuccess() 68 | } else { 69 | showFailureAlert() 70 | } 71 | } else if !profileURL.isEmpty { 72 | if isProfileURLValidated { 73 | handleSuccess() 74 | } else { 75 | showFailureAlert() 76 | } 77 | } else { 78 | handleSuccess() 79 | } 80 | } 81 | 82 | func handleSuccess() { 83 | Task { 84 | await editUser() 85 | showSuccessAlert() 86 | } 87 | } 88 | 89 | func showSuccessAlert() { 90 | DispatchQueue.main.async { [weak self] in 91 | guard let self = self else { return } 92 | self.isShowingSuccessAlert = true 93 | } 94 | } 95 | 96 | func showFailureAlert() { 97 | DispatchQueue.main.async { [weak self] in 98 | guard let self = self else { return } 99 | self.isShowingInputFailureAlert = true 100 | } 101 | } 102 | 103 | func editUser() async { 104 | let user = User( 105 | id: user.id, 106 | name: user.name, 107 | nickname: user.nickname, 108 | role: user.role, 109 | description: description, 110 | linkedInURL: linkedInURL, 111 | profileURL: profileURL, 112 | friends: user.friends 113 | ) 114 | FirebaseManager.shared.editUser(user: user) 115 | } 116 | 117 | func verifyURL (urlString: String?) -> Bool { 118 | guard let urlString = urlString, 119 | let url = NSURL(string: urlString) 120 | else { return false } 121 | return UIApplication.shared.canOpenURL(url as URL) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /AsyncSwift/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/09/06. 6 | // 7 | 8 | import SwiftUI 9 | import Firebase 10 | import UserNotifications 11 | 12 | @available(iOS 10, *) 13 | extension AppDelegate: UNUserNotificationCenterDelegate { 14 | 15 | // WHILE APP IS ON FOREGROUND 16 | func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { 17 | let userInfo = notification.request.content.userInfo 18 | if let messageID = userInfo[gcmMessageIDKey]{ 19 | print("Message ID : ", messageID) 20 | } 21 | if let data1 = userInfo[data1Key]{ 22 | print("data1 : ", data1) 23 | } 24 | if let data2 = userInfo[data2Key]{ 25 | print("data2 : ", data2) 26 | } 27 | if let apsData = userInfo[aps]{ 28 | print("apsData : ", apsData) 29 | } 30 | completionHandler([[.banner, .badge, .sound]]) 31 | 32 | print("MESSAGE RECIEVED ON FOREGROUND") 33 | } 34 | 35 | // EXECUTE WHEN USER CLICKS NOTIFICATION WHILE APP IS ON BACKGROUND 36 | func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { 37 | let userInfo = response.notification.request.content.userInfo 38 | if let messageID = userInfo[gcmMessageIDKey]{ 39 | print("Message ID from userNotificationCenter didRecieve : ", messageID) 40 | } 41 | completionHandler() 42 | 43 | } 44 | func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { 45 | print("앱이 APNS에 등록되었음") 46 | Messaging.messaging().apnsToken = deviceToken 47 | } 48 | func application(_ application: UIApplication, didFailToContinueUserActivityWithType userActivityType: String, error: Error) { 49 | print("APNS가 등록에 실패 하였음") 50 | } 51 | } 52 | 53 | extension AppDelegate: MessagingDelegate { 54 | func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { 55 | let deviceToken: [String:String] = ["token": fcmToken ?? ""] 56 | print("Device Token : ", deviceToken) 57 | } 58 | } 59 | 60 | class AppDelegate: NSObject, UIApplicationDelegate { 61 | 62 | let gcmMessageIDKey = "gcm.message_id" 63 | let aps = "aps" 64 | let data1Key = "DATA1" 65 | let data2Key = "DATA2" 66 | 67 | // Register for remote notifications 68 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 69 | FirebaseApp.configure() 70 | Messaging.messaging().delegate = self 71 | 72 | if #available(iOS 10.0, *){ 73 | UNUserNotificationCenter.current().delegate = self 74 | let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] 75 | UNUserNotificationCenter.current().requestAuthorization( 76 | options: authOptions, completionHandler: {_, _ in}) 77 | } else { 78 | let settings: UIUserNotificationSettings = UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil) 79 | application.registerUserNotificationSettings(settings) 80 | } 81 | application.registerForRemoteNotifications() 82 | return true 83 | } 84 | 85 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 86 | if let messageID = userInfo[gcmMessageIDKey]{ 87 | print("Message ID : \(messageID)") 88 | } 89 | print("userInfo : ", userInfo) 90 | completionHandler(UIBackgroundFetchResult.newData) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /AsyncSwift/Observed/ProfileView/ProfileRegisterViewObserved.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileRegisterViewObserved.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/10/28. 6 | // 7 | 8 | import Combine 9 | import SwiftUI 10 | 11 | @MainActor 12 | final class ProfileRegisterViewObserved: ObservableObject { 13 | @Binding var hasRegisteredProfile: Bool 14 | @Binding var userID: String? 15 | 16 | @Published var isShowingSuccessAlert = false 17 | @Published var isShowingFailureAlert = false 18 | @Published var isShowingInputFailureAlert = false 19 | 20 | @Published var name = "" 21 | @Published var nickname = "" 22 | @Published var role = "" 23 | @Published var description = "" { 24 | didSet { 25 | if description.count >= 80 { 26 | description = oldValue 27 | } 28 | } 29 | } 30 | @Published var linkedInURL = "" { 31 | didSet { 32 | self.isLinkedinURLValidated = self.verifyURL(urlString: linkedInURL) 33 | } 34 | } 35 | @Published var profileURL = "" { 36 | didSet { 37 | self.isProfileURLValidated = self.verifyURL(urlString: profileURL) 38 | } 39 | } 40 | var isLinkedinURLValidated = false 41 | var isProfileURLValidated = false 42 | 43 | init(hasRegisteredProfile: Binding, userID: Binding) { 44 | self._hasRegisteredProfile = hasRegisteredProfile 45 | self._userID = userID 46 | } 47 | 48 | func didTapRegisterButton() { 49 | register() 50 | } 51 | 52 | func isButtonAvailable() -> Bool { 53 | if !name.isEmpty && !role.isEmpty { 54 | return true 55 | } else { 56 | return false 57 | } 58 | } 59 | } 60 | 61 | private extension ProfileRegisterViewObserved { 62 | 63 | func register() { 64 | guard isButtonAvailable() else { return } 65 | if !linkedInURL.isEmpty && !profileURL.isEmpty { 66 | if isLinkedinURLValidated && isProfileURLValidated { 67 | handleSuccess() 68 | } else { 69 | showFailureAlert() 70 | } 71 | } else if !linkedInURL.isEmpty { 72 | if isLinkedinURLValidated { 73 | handleSuccess() 74 | } else { 75 | showFailureAlert() 76 | } 77 | } else if !profileURL.isEmpty { 78 | if isProfileURLValidated { 79 | handleSuccess() 80 | } else { 81 | showFailureAlert() 82 | } 83 | } else { 84 | handleSuccess() 85 | } 86 | } 87 | 88 | func handleSuccess() { 89 | Task { 90 | await createUser() 91 | hasRegisteredProfile = true 92 | showSuccessAlert() 93 | } 94 | } 95 | 96 | func showSuccessAlert() { 97 | DispatchQueue.main.async { [weak self] in 98 | guard let self = self else { return } 99 | self.isShowingSuccessAlert = true 100 | } 101 | } 102 | 103 | func showFailureAlert() { 104 | DispatchQueue.main.async { [weak self] in 105 | guard let self = self else { return } 106 | self.isShowingInputFailureAlert = true 107 | } 108 | } 109 | 110 | 111 | func createUser() async { 112 | let userId = UUID().uuidString 113 | let user = User( 114 | id: userId, 115 | name: name, 116 | nickname: nickname, 117 | role: role, 118 | description: description, 119 | linkedInURL: linkedInURL, 120 | profileURL: profileURL, 121 | friends: [] 122 | ) 123 | self.userID = userId 124 | FirebaseManager.shared.createUser(user: user) 125 | } 126 | 127 | func verifyURL (urlString: String?) -> Bool { 128 | guard let urlString = urlString, 129 | let url = NSURL(string: urlString) 130 | else { return false } 131 | return UIApplication.shared.canOpenURL(url as URL) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /AsyncSwift/Views/EventDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventDetailView.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/09/08. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EventDetailView: View { 11 | 12 | @ObservedObject var observed: Observed 13 | 14 | init(event: Event) { 15 | observed = Observed(event: event) 16 | } 17 | 18 | var body: some View { 19 | ScrollView { 20 | VStack(alignment: .leading, spacing: 0) { 21 | customDivider 22 | .padding(.top, 10) 23 | description 24 | customDivider 25 | information 26 | Spacer() 27 | } 28 | } 29 | .navigationTitle(observed.event.detailTitle) 30 | .confirmationDialog("", isPresented: $observed.isShowingSheet, titleVisibility: .hidden) { 31 | if let naverMapURL = URL(string: observed.event.addressURLs.naverMapURL) { 32 | Link("네이버 지도로 길 찾기", destination: naverMapURL) 33 | } 34 | if let kakaoMapURL = URL(string: observed.event.addressURLs.kakaoMapURL) { 35 | Link("카카오맵으로 길 찾기", destination: kakaoMapURL) 36 | } 37 | } 38 | } 39 | } 40 | 41 | private extension EventDetailView { 42 | 43 | var description: some View { 44 | VStack(alignment: .leading, spacing: 8) { 45 | Text(observed.event.subject) 46 | .fontWeight(.bold) 47 | .font(.title3) 48 | ForEach(observed.event.description, id:\.self) { paragraph in 49 | Text(paragraph.content) 50 | .font(.body) 51 | } 52 | Text(observed.event.hashTags) 53 | .padding(.top, 8) 54 | .foregroundColor(.gray) 55 | .font(.body) 56 | } 57 | .padding(.horizontal, 24) 58 | .padding(.vertical, 30) 59 | } 60 | 61 | var information: some View { 62 | VStack(alignment: .leading, spacing: 40) { 63 | VStack(alignment: .leading, spacing: 8) { 64 | Text("\(Image(systemName: "calendar")) Date and time") 65 | .font(.title3) 66 | .fontWeight(.semibold) 67 | Text("\(observed.event.date)\n\(observed.event.time)") 68 | .font(.body) 69 | Button("캘린더에 추가") { 70 | observed.isShowingAddEventConfirmationAlert = true 71 | } 72 | .alert("'AsyncSwift'이(가) 사용자의 캘린터에 접근하려고 합니다.", isPresented: $observed.isShowingAddEventConfirmationAlert, actions: { 73 | Button("허용 안 함") { observed.isShowingAddEventConfirmationAlert = false } 74 | Button("확인") { observed.additionConfirmed() } 75 | }, message: { 76 | Text("사용자의 '캘린더'에 접근하도록 허용합니다.") 77 | }) 78 | .alert("일정 등록 완료", isPresented: $observed.isShowingAddEventSuccessAlert, actions: { 79 | Button("확인", role: .cancel) { } 80 | }, message: { 81 | Text("세미나 일정이 캘린더에 추가되었습니다.") 82 | }) 83 | .alert("일정 등록 실패", isPresented: $observed.isShowingAddEventFailureAlert, actions: { 84 | Button("다시 시도", role: .cancel) { } 85 | }, message: { 86 | Text("등록에 오류가 발생했습니다.\n다시 시도하십시오.") 87 | }) 88 | } 89 | VStack(alignment: .leading, spacing: 8) { 90 | Text("\(Image(systemName: "location.fill")) Location") 91 | .font(.title3) 92 | .fontWeight(.semibold) 93 | VStack(alignment: .leading) { 94 | Text(observed.event.location) 95 | Text(observed.event.address) 96 | } 97 | Button { 98 | observed.isShowingSheet = true 99 | } label: { 100 | Text("지도로 길찾기") 101 | } 102 | } 103 | } 104 | .padding(.horizontal, 24) 105 | .padding(.vertical, 30) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /AsyncSwift/Observed/ProfileView/ProfileViewObserved.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileView+Observed.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/10/16. 6 | // 7 | 8 | import CodeScanner 9 | import CoreImage.CIFilterBuiltins 10 | import Combine 11 | import UIKit 12 | 13 | @MainActor 14 | final class ProfileViewObserved: ObservableObject { 15 | @Published var hasRegisteredProfile = false 16 | @Published var isLoading = true 17 | @Published var isShowingFriends = false 18 | @Published var isShowingEdit = false 19 | @Published var isShowingScanner = false 20 | @Published var isShowingUserDetail = false 21 | @Published var isShowingFailureAlert = false 22 | @Published var isShowingScanErrorAlert = false 23 | @Published var user: User = .init() 24 | @Published var scannedFriend: User = .init() 25 | private let keyChainManager = KeyChainManager() 26 | 27 | var userID: String? { 28 | didSet { 29 | let _ = keyChainManager.addItem(key: "userID", pwd: userID ?? "") 30 | } 31 | } 32 | 33 | init() { 34 | guard let userid = keyChainManager.getItem(key: "userID") else { return } 35 | self.hasRegisteredProfile = true 36 | self.userID = userid as? String 37 | } 38 | 39 | func onAppear() { 40 | guard hasRegisteredProfile else { return } 41 | Task { 42 | await getUserByID() 43 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in 44 | guard let self = self else { return } 45 | self.isLoading = false 46 | } 47 | } 48 | } 49 | 50 | func didTapCloseButton() { 51 | isShowingScanner = false 52 | } 53 | 54 | func getQRCodeImage() -> UIImage { 55 | let context = CIContext() 56 | let filter = CIFilter.qrCodeGenerator() 57 | let data = Data(userID?.utf8 ?? "".utf8) 58 | filter.setValue(data, forKey: "inputMessage") 59 | guard let qrCodeImage = filter.outputImage, 60 | let qrCodeImage = context.createCGImage(qrCodeImage, from: qrCodeImage.extent) 61 | else { return UIImage(systemName: "xmark") ?? UIImage() } 62 | return UIImage(cgImage: qrCodeImage) 63 | } 64 | 65 | func handleScan(result: Result) { 66 | switch result { 67 | case .success(let success): 68 | let uuidString = success.string 69 | handleScanSuccess(id: uuidString) 70 | case .failure(_): 71 | isShowingScanner = false 72 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in 73 | guard let self = self else { return } 74 | self.isShowingScanErrorAlert = true 75 | } 76 | } 77 | } 78 | } 79 | 80 | private extension ProfileViewObserved { 81 | func handleScanSuccess(id: String) { 82 | guard (UUID(uuidString: id)) != nil 83 | else { 84 | isShowingScanner = false 85 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in 86 | guard let self = self else { return } 87 | self.isShowingScanErrorAlert = true 88 | } 89 | return 90 | } 91 | guard isNewFriend(id: id) 92 | else { 93 | isShowingScanner = false 94 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in 95 | guard let self = self else { return } 96 | self.isShowingFailureAlert = true 97 | } 98 | return 99 | } 100 | Task { 101 | user.friends.append(id) 102 | FirebaseManager.shared.editUser(user: self.user) 103 | await getFriendByID(id: id) 104 | isShowingScanner = false 105 | isShowingUserDetail = true 106 | } 107 | } 108 | 109 | func getUserByID() async { 110 | FirebaseManager.shared.getUserBy(id: self.userID ?? "") { [weak self] user in 111 | guard let self = self else { return } 112 | self.user = user 113 | } 114 | } 115 | 116 | func getFriendByID(id: String) async { 117 | FirebaseManager.shared.getUserBy(id: id) { [weak self] user in 118 | guard let self = self else { return } 119 | self.scannedFriend = user 120 | } 121 | } 122 | 123 | func isNewFriend(id: String) -> Bool { 124 | return !user.friends.contains(id) 125 | } 126 | } 127 | 128 | -------------------------------------------------------------------------------- /AsyncSwift/Views/Profile/ProfileFriendDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileFriendDetailView.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/11/02. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProfileFriendDetailView: View { 11 | @ObservedObject var observed: ProfileFriendDetailViewObserved 12 | 13 | init( 14 | previous: PreviousView, 15 | inActive: Binding, 16 | user: User, 17 | friend: User 18 | ) { 19 | observed = ProfileFriendDetailViewObserved( 20 | inActive: inActive, 21 | user: user, 22 | friend: friend, 23 | previous: previous 24 | ) 25 | } 26 | 27 | var body: some View { 28 | VStack(alignment: .leading, spacing: 0) { 29 | customDivider 30 | .padding(.top, 10) 31 | userDetail 32 | Spacer() 33 | linkButtons 34 | } 35 | .navigationTitle(observed.friend.name) 36 | .navigationBarItems(trailing: navigationBarTrailingButton) 37 | .fullScreenCover(isPresented: $observed.isShowingProfileSheet, content: { 38 | if let url = observed.profileURL { 39 | SafariView(url: url) 40 | .ignoresSafeArea() 41 | } 42 | }) 43 | .fullScreenCover(isPresented: $observed.isShowingLinkedInSheet, content: { 44 | if let url = observed.linkedInURL { 45 | SafariView(url: url) 46 | .ignoresSafeArea() 47 | } 48 | }) 49 | .alert(isPresented: $observed.isShowingConfirmAlert) { 50 | Alert( 51 | title: Text("삭제"), 52 | message: Text("유저 친구 목록에서 삭제하시겠습니까?"), 53 | primaryButton: .destructive(Text("삭제")) { 54 | observed.didConfirmDelete() 55 | }, 56 | secondaryButton: .cancel(Text("취소")) { 57 | observed.isShowingConfirmAlert = false 58 | } 59 | ) 60 | } 61 | } 62 | } 63 | 64 | private extension ProfileFriendDetailView { 65 | var userDetail: some View { 66 | VStack(alignment: .leading, spacing: 0) { 67 | Text(observed.friend.nickname) 68 | .fontWeight(.semibold) 69 | .font(.system(size: 20)) 70 | Text(observed.friend.role) 71 | .fontWeight(.semibold) 72 | .font(.system(size: 20)) 73 | .foregroundColor(.profileFontGrayForeground) 74 | .padding(.bottom, 24) 75 | Text(observed.friend.description) 76 | } 77 | .padding(.horizontal, 24) 78 | .padding(.top, 28) 79 | } 80 | 81 | var linkButtons: some View { 82 | VStack { 83 | profileButton 84 | linkedInButton 85 | .padding(.bottom, 16) 86 | } 87 | .padding(.horizontal) 88 | } 89 | 90 | var profileButton: some View { 91 | Button { 92 | if observed.hasProfileURL { 93 | observed.isShowingProfileSheet = true 94 | } 95 | } label: { 96 | Text("Profile URL") 97 | .font(.headline) 98 | .foregroundColor(.white) 99 | .frame(minWidth: 0, maxWidth: .infinity, maxHeight: 68) 100 | .background(observed.hasProfileURL ? Color.seminarOrange : Color.inActiveButtonBackground) 101 | .cornerRadius(15) 102 | } 103 | } 104 | 105 | var linkedInButton: some View { 106 | Button { 107 | if observed.hasLinkedInURL { 108 | observed.isShowingLinkedInSheet = true 109 | } 110 | } label: { 111 | Text("LinkedIn") 112 | .font(.headline) 113 | .foregroundColor(.white) 114 | .frame(minWidth: 0, maxWidth: .infinity, maxHeight: 68) 115 | .background(observed.hasLinkedInURL ? Color.linkedInBlueBackground : Color.inActiveButtonBackground) 116 | .cornerRadius(15) 117 | } 118 | } 119 | 120 | @ViewBuilder 121 | var navigationBarTrailingButton: some View { 122 | switch observed.previous { 123 | case .ProfileView: 124 | doneButton 125 | case .ListView: 126 | deleteButton 127 | } 128 | } 129 | 130 | var doneButton: some View { 131 | Button { 132 | observed.didTapDoneButton() 133 | } label: { 134 | Text("Done") 135 | } 136 | } 137 | 138 | var deleteButton: some View { 139 | Button { 140 | observed.didTapDeleteButton() 141 | } label: { 142 | Text("Delete") 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /AsyncSwift/Views/TicketingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TicketView.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/09/06. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSwiftUI 10 | 11 | struct TicketingView: View { 12 | @StateObject private var observed = Observed() 13 | 14 | var body: some View { 15 | 16 | NavigationView { 17 | ScrollView { 18 | VStack(spacing: 30) { 19 | if let upcomingEvent = observed.ticketing?.upcomingEvent { 20 | makeUpcomingEventView(from: upcomingEvent) 21 | .ticketingViewStyle() 22 | } else { 23 | skeletonView 24 | .aspectRatio(2.75, contentMode: .fill) 25 | .ticketingViewStyle() 26 | } 27 | 28 | switch observed.hasAvailableTicket { 29 | case true: 30 | ticketingView 31 | .ticketingViewStyle() 32 | case false: 33 | emptyTicketingView 34 | .ticketingViewStyle() 35 | } 36 | } 37 | .padding(.horizontal) 38 | .padding(.vertical, 30) 39 | 40 | } 41 | .navigationTitle("Ticketing") 42 | } 43 | .onAppear { observed.getTicketingData() } 44 | } 45 | } 46 | 47 | fileprivate extension TicketingView { 48 | struct TicketingViewStyle: ViewModifier { 49 | func body(content: Content) -> some View { 50 | content 51 | .cornerRadius(8.0) 52 | .shadow( 53 | color: Color(.sRGB, red: 0, green: 0, blue: 0, opacity: 0.15), 54 | radius: 20, x: 0, y: 4 55 | ) 56 | } 57 | } 58 | } 59 | 60 | fileprivate extension View { 61 | func ticketingViewStyle() -> some View { 62 | modifier(TicketingView.TicketingViewStyle()) 63 | } 64 | } 65 | 66 | private extension TicketingView { 67 | var skeletonView: some View { 68 | LinearGradient( 69 | colors: [.skeletonBackground, .white], 70 | startPoint: .topLeading, 71 | endPoint: .bottomTrailing 72 | ) 73 | .animation(.linear(duration: 3.0), value: 1.0) 74 | } 75 | 76 | @ViewBuilder var ticketingView: some View { 77 | 78 | if let url = URL(string: observed.ticketing?.currentTicket?.ticketingURL ?? "") { 79 | Link(destination: url) { 80 | WebImage(url: URL(string: observed.ticketing?.currentTicket?.ticketingImageURL ?? "")) 81 | .resizable() 82 | .placeholder { 83 | skeletonView 84 | .aspectRatio(0.85, contentMode: .fill) 85 | } 86 | .scaledToFill() 87 | .transition(.opacity.animation(.easeOut)) 88 | } 89 | .disabled(observed.isTicketingLinkDisabled) 90 | } 91 | } 92 | 93 | var emptyTicketingView: some View { 94 | Text("현재 판매중인 티켓이 없습니다.") 95 | .fontWeight(.bold) 96 | .font(.body) 97 | .foregroundColor(.emptyTicketViewForeground) 98 | .frame(maxWidth: .infinity, maxHeight: .infinity) 99 | .aspectRatio(0.85, contentMode: .fill) 100 | .background(Color.emptyTicketViewBackground) 101 | } 102 | 103 | @ViewBuilder 104 | func makeUpcomingEventView(from upcomingEvent: Ticketing.UpcomingEvent) -> some View { 105 | HStack(alignment: .top, spacing: 0.0) { 106 | VStack(alignment: .leading, spacing: 15.0) { 107 | Text(upcomingEvent.headerTitle) 108 | .fontWeight(.bold) 109 | .font(.caption2) 110 | VStack(alignment: .leading, spacing: 5.0) { 111 | Text(upcomingEvent.title) 112 | .fontWeight(.bold) 113 | .font(.title2) 114 | Text(upcomingEvent.subscription) 115 | .fontWeight(.bold) 116 | .font(.subheadline) 117 | } 118 | } 119 | Spacer() 120 | } 121 | .frame(maxWidth: .infinity) 122 | .padding( 123 | EdgeInsets(top: 15.0, leading: 12.0, bottom: 24.0, trailing: 12.0) 124 | ) 125 | .foregroundColor(.white) 126 | .background( 127 | LinearGradient( 128 | colors: [upcomingEvent.backgroundGradientStartColor, upcomingEvent.backgroundGradientEndColor], 129 | startPoint: .leading, 130 | endPoint: .trailing 131 | ) 132 | ) 133 | } 134 | } 135 | 136 | struct TicketView_Previews: PreviewProvider { 137 | static var previews: some View { 138 | TicketingView() 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /AsyncSwift/Observed/StampView+Observed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StampView+Observed.swift 3 | // AsyncSwift 4 | // 5 | // Created by Inho Choi on 2022/09/09. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | extension StampView { 12 | final class Observed: ObservableObject { 13 | @Published var cards: [Card] = [] 14 | @Published var events = [String]() 15 | @Published var currentIndex = 0 16 | @Published var isLoading = true 17 | private let keyChainManager = KeyChainManager() 18 | private let cardInterval: CGFloat = (UIScreen.main.bounds.width - 32) * 56 / 358 19 | private let cardSize: CGFloat = UIScreen.main.bounds.width - 32 20 | private var cancenllable = Set() 21 | 22 | init() { 23 | fetchStampsImages() 24 | } 25 | 26 | private func getEvents() -> [String] { 27 | let pwRaw = keyChainManager.getItem(key: keyChainManager.stampKey) as? String 28 | guard let convertedStringArray = pwRaw?.convertToStringArray() else { return [] } 29 | self.events = convertedStringArray.reversed() 30 | return events 31 | } 32 | 33 | /// Storage에 저장되어 있는 Stamp Image를 가져오는 함수이다. 34 | /// - 35 | private func fetchStampsImages() { 36 | 37 | let events = getEvents() 38 | guard !events.isEmpty else { return isLoading = false } 39 | 40 | events.enumerated().forEach { [weak self] in 41 | guard let self else { return } 42 | let event = $0.element 43 | let index = $0.offset 44 | let url = URL(string: GitHubStorageURL.stampImage(event))! 45 | 46 | URLSession.shared.dataTaskPublisher(for: url) 47 | .map(\.data) 48 | .tryMap { 49 | guard let image = UIImage(data: $0) else { 50 | throw URLError(.badURL) 51 | } 52 | return Card( 53 | originalPosition: self.cardInterval * CGFloat(index), 54 | image: Image(uiImage: image), 55 | event: event 56 | ) 57 | } 58 | .receive(on: RunLoop.main) 59 | .sink(receiveCompletion: { _ in 60 | 61 | }, receiveValue: { [weak self] card in 62 | self?.cards.append(card) 63 | if index == events.count - 1 { 64 | self?.isLoading = false 65 | } 66 | }) 67 | .store(in: &cancenllable) 68 | } 69 | } 70 | 71 | func isAvailableURL(url: URL) async -> Bool { 72 | // URL Example = https://asyncswift.info?tab=Stamp&event=Conference001 73 | // URL Example = https://asyncswift.info?tab=Event 74 | 75 | if URLComponents(url: url, resolvingAgainstBaseURL: true)?.host == nil { return false } 76 | var queries = [String: String]() 77 | for item in URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems ?? [] { 78 | queries[item.name] = item.value 79 | } 80 | 81 | do { 82 | let stamp = try await fetchCurrentStamp() 83 | 84 | if queries["tab"] == Tab.stamp.rawValue { 85 | guard let queryEvent = queries["event"] else { return false } 86 | if stamp.title == queryEvent { 87 | let pwRaw = keyChainManager.getItem(key: keyChainManager.stampKey) as? String 88 | var pw: [String] = pwRaw?.convertToStringArray() ?? [] 89 | pw.append(queryEvent) 90 | 91 | if keyChainManager.addItem(key: keyChainManager.stampKey, pwd: pw.description) { 92 | fetchStampsImages() 93 | } 94 | } 95 | } 96 | } catch { 97 | print(error.localizedDescription) 98 | return false 99 | } 100 | return true 101 | } 102 | 103 | private func fetchCurrentStamp() async throws -> Stamp { 104 | guard let url = URL(string: "https://raw.githubusercontent.com/Async-Swift/jsonstorage/main/currentEvent.json") // MARK: URL 주소 확인 테스트용으로 되어 있음 105 | else { return .init(title: "error") } 106 | 107 | let request = URLRequest(url: url) 108 | let (data, response) = try await URLSession.shared.data(for: request) 109 | 110 | guard let httpResponse = response as? HTTPURLResponse, 111 | httpResponse.statusCode == 200 else { return .init(title: "error")} 112 | 113 | let stamp = try JSONDecoder().decode(Stamp.self, from: data) 114 | 115 | return stamp 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /AsyncSwift/Views/Profile/ProfileFriendsListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileFriendsListView.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/11/02. 6 | // 7 | 8 | import CodeScanner 9 | import SwiftUI 10 | 11 | struct ProfileFriendsListView: View { 12 | @StateObject var observed: ProfileFriendsListViewObserved 13 | 14 | init(inActive: Binding, user: User) { 15 | _observed = StateObject( 16 | wrappedValue: ProfileFriendsListViewObserved( 17 | inActive: inActive, 18 | user: user 19 | )) 20 | } 21 | 22 | var body: some View { 23 | VStack(spacing: 0) { 24 | customDivider 25 | .padding(.top, 10) 26 | friendList 27 | } 28 | .navigationTitle("Friends") 29 | .fullScreenCover( 30 | isPresented: $observed.isShowingScanner, 31 | content: { scannerView } 32 | ) 33 | .fullScreenCover( 34 | isPresented: $observed.isShowingUserDetail, 35 | content: { scannedFriendDetail } 36 | ) 37 | .onAppear { 38 | observed.onAppear() 39 | } 40 | .alert("QR 등록 오류", isPresented: $observed.isShowingScanErrorAlert, actions: { 41 | Button("취소", role: .cancel) { observed.isShowingScanErrorAlert = false } 42 | }, message: { 43 | Text("등록할 수 없는 QR코드입니다.") 44 | }) 45 | } 46 | } 47 | 48 | private extension ProfileFriendsListView { 49 | @ViewBuilder 50 | var friendList: some View { 51 | switch observed.isLoading { 52 | case true: 53 | loadingList 54 | case false: 55 | switch observed.user.friends.isEmpty { 56 | case true: 57 | emptyList 58 | case false: 59 | list 60 | } 61 | } 62 | } 63 | 64 | var emptyList: some View { 65 | VStack(spacing: 0) { 66 | Spacer() 67 | Text("등록된 프로필이 없습니다.") 68 | .foregroundColor(.profileGray) 69 | .padding(.bottom, 17) 70 | Button { 71 | observed.isShowingScanner = true 72 | } label: { 73 | Text("프로필 스캔하기") 74 | .foregroundColor(.seminarOrange) 75 | .font(.headline) 76 | } 77 | Spacer() 78 | } 79 | } 80 | 81 | var loadingList: some View { 82 | ScrollView { 83 | ForEach(0.. some View { 107 | NavigationLink { 108 | ProfileFriendDetailView( 109 | previous: .ListView, 110 | inActive: $observed.inActive, 111 | user: observed.user, 112 | friend: friend 113 | ) 114 | } label: { 115 | HStack { 116 | Text("\(friend.name) | \(friend.nickname)") 117 | .font(.headline) 118 | Spacer() 119 | Image(systemName: "chevron.forward") 120 | } 121 | .foregroundColor(.black) 122 | .padding(.horizontal, 19) 123 | .padding(.vertical, 23) 124 | .frame(maxWidth: .infinity, maxHeight: 56) 125 | .background(Color.buttonBackground) 126 | .cornerRadius(15) 127 | } 128 | } 129 | 130 | var scannerView: some View { 131 | VStack { 132 | ZStack { 133 | Text("코드스캔") 134 | HStack { 135 | Spacer() 136 | Button { 137 | observed.didTapXButton() 138 | } label: { 139 | Text("Done") 140 | } 141 | .padding() 142 | } 143 | } 144 | .frame(height: 51) 145 | CodeScannerView( 146 | codeTypes: [.qr], 147 | simulatedData: "6A8254C2-1054-4A5B-9F30-602684D329F9", 148 | completion: observed.handleScan 149 | ) 150 | HStack { 151 | Text("QR코드를 스캔해 보세요. 프로필 상세 정보를 확인할 수 있습니다.") 152 | .font(.caption2) 153 | } 154 | .frame(height: 70) 155 | } 156 | } 157 | 158 | var scannedFriendDetail: some View { 159 | NavigationView { 160 | VStack { 161 | ProfileFriendDetailView( 162 | previous: .ProfileView, 163 | inActive: $observed.isShowingUserDetail, 164 | user: observed.user, 165 | friend: observed.scannedFriend 166 | ) 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /AsyncSwift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "abseil-cpp-binary", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/google/abseil-cpp-binary.git", 7 | "state" : { 8 | "revision" : "bfc0b6f81adc06ce5121eb23f628473638d67c5c", 9 | "version" : "1.2022062300.0" 10 | } 11 | }, 12 | { 13 | "identity" : "cocoalumberjack", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", 16 | "state" : { 17 | "revision" : "67ec5818a757aba4d7c534e21a905d878d128dbf", 18 | "version" : "3.8.1" 19 | } 20 | }, 21 | { 22 | "identity" : "codescanner", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/twostraws/CodeScanner", 25 | "state" : { 26 | "revision" : "bf5d7087015620b250ee6c865b3c9039fc159d1a", 27 | "version" : "2.3.3" 28 | } 29 | }, 30 | { 31 | "identity" : "firebase-ios-sdk", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/firebase/firebase-ios-sdk.git", 34 | "state" : { 35 | "revision" : "837d4af6ead57cec1fc38007892500d3139c7556", 36 | "version" : "10.16.0" 37 | } 38 | }, 39 | { 40 | "identity" : "googleappmeasurement", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/google/GoogleAppMeasurement.git", 43 | "state" : { 44 | "revision" : "56f681586ff006a7982b53dc94082eea31971acf", 45 | "version" : "10.16.0" 46 | } 47 | }, 48 | { 49 | "identity" : "googledatatransport", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/google/GoogleDataTransport.git", 52 | "state" : { 53 | "revision" : "aae45a320fd0d11811820335b1eabc8753902a40", 54 | "version" : "9.2.5" 55 | } 56 | }, 57 | { 58 | "identity" : "googleutilities", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/google/GoogleUtilities.git", 61 | "state" : { 62 | "revision" : "c38ce365d77b04a9a300c31061c5227589e5597b", 63 | "version" : "7.11.5" 64 | } 65 | }, 66 | { 67 | "identity" : "grpc-binary", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/google/grpc-binary.git", 70 | "state" : { 71 | "revision" : "a673bc2937fbe886dd1f99c401b01b6d977a9c98", 72 | "version" : "1.49.1" 73 | } 74 | }, 75 | { 76 | "identity" : "gtm-session-fetcher", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/google/gtm-session-fetcher.git", 79 | "state" : { 80 | "revision" : "d415594121c9e8a4f9d79cecee0965cf35e74dbd", 81 | "version" : "3.1.1" 82 | } 83 | }, 84 | { 85 | "identity" : "interop-ios-for-google-sdks", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/google/interop-ios-for-google-sdks.git", 88 | "state" : { 89 | "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", 90 | "version" : "100.0.0" 91 | } 92 | }, 93 | { 94 | "identity" : "leveldb", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/firebase/leveldb.git", 97 | "state" : { 98 | "revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b", 99 | "version" : "1.22.2" 100 | } 101 | }, 102 | { 103 | "identity" : "nanopb", 104 | "kind" : "remoteSourceControl", 105 | "location" : "https://github.com/firebase/nanopb.git", 106 | "state" : { 107 | "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", 108 | "version" : "2.30909.0" 109 | } 110 | }, 111 | { 112 | "identity" : "promises", 113 | "kind" : "remoteSourceControl", 114 | "location" : "https://github.com/google/promises.git", 115 | "state" : { 116 | "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", 117 | "version" : "2.3.1" 118 | } 119 | }, 120 | { 121 | "identity" : "sdwebimage", 122 | "kind" : "remoteSourceControl", 123 | "location" : "https://github.com/SDWebImage/SDWebImage.git", 124 | "state" : { 125 | "revision" : "936f1c7067728d16c362ba4fb93c17df78b5fd79", 126 | "version" : "5.18.2" 127 | } 128 | }, 129 | { 130 | "identity" : "sdwebimageswiftui", 131 | "kind" : "remoteSourceControl", 132 | "location" : "https://github.com/SDWebImage/SDWebImageSwiftUI.git", 133 | "state" : { 134 | "revision" : "e837c37d45449fbd3b4745c10c5b5274e73edead", 135 | "version" : "2.2.3" 136 | } 137 | }, 138 | { 139 | "identity" : "svgkit", 140 | "kind" : "remoteSourceControl", 141 | "location" : "https://github.com/SVGKit/SVGKit.git", 142 | "state" : { 143 | "revision" : "58152b9f7c85eab239160b36ffdfd364aa43d666", 144 | "version" : "3.0.0" 145 | } 146 | }, 147 | { 148 | "identity" : "swift-log", 149 | "kind" : "remoteSourceControl", 150 | "location" : "https://github.com/apple/swift-log.git", 151 | "state" : { 152 | "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", 153 | "version" : "1.5.3" 154 | } 155 | }, 156 | { 157 | "identity" : "swift-protobuf", 158 | "kind" : "remoteSourceControl", 159 | "location" : "https://github.com/apple/swift-protobuf.git", 160 | "state" : { 161 | "revision" : "3c54ab05249f59f2c6641dd2920b8358ea9ed127", 162 | "version" : "1.24.0" 163 | } 164 | } 165 | ], 166 | "version" : 2 167 | } 168 | -------------------------------------------------------------------------------- /AsyncSwift/Views/EventView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScheduleView.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/09/06. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EventView: View { 11 | 12 | @StateObject var observed = EventViewObserved() 13 | 14 | var body: some View { 15 | NavigationView { 16 | ScrollView { 17 | if observed.isLoading { 18 | VStack(spacing: 0) { 19 | onLoadingHeader 20 | VStack(alignment: .leading, spacing: 0) { 21 | ForEach(observed.onLoadingCells, id: \.self) { _ in 22 | onLoadingCell 23 | } 24 | } 25 | } 26 | } else { 27 | Header 28 | LazyVStack { 29 | ForEach(observed.event.sessions) { session in 30 | makeSessionCell(for: session) 31 | } 32 | } 33 | } 34 | } 35 | .navigationTitle(Tab.event.title) 36 | .onAppear { observed.getEventData() } 37 | } 38 | } 39 | } 40 | 41 | private extension EventView { 42 | 43 | var onLoadingHeader: some View { 44 | HStack { 45 | VStack(alignment: .leading, spacing: 9) { 46 | Rectangle() 47 | .frame(width: 202, height: 30) 48 | .cornerRadius(4) 49 | .foregroundColor(.gray) 50 | .opacity(0.8) 51 | HStack { 52 | Rectangle() 53 | .frame(width: 151, height: 21) 54 | .cornerRadius(13) 55 | .foregroundColor(.gray) 56 | Rectangle() 57 | .frame(width: 70, height: 21) 58 | .cornerRadius(13) 59 | .foregroundColor(.gray) 60 | } 61 | .opacity(0.4) 62 | .padding(.bottom, 2) 63 | Rectangle() 64 | .frame(width: 106, height: 13) 65 | .cornerRadius(4) 66 | .foregroundColor(.gray) 67 | .opacity(0.2) 68 | } 69 | Spacer() 70 | } 71 | .padding(.horizontal, 16) 72 | .padding(.vertical, 32) 73 | } 74 | 75 | var Header: some View { 76 | VStack(alignment: .leading, spacing: 8) { 77 | Text(observed.event.subject) 78 | .font(.title) 79 | .fontWeight(.bold) 80 | HStack { 81 | Text(observed.event.title) 82 | .font(.caption2) 83 | .fontWeight(.bold) 84 | .foregroundColor(.white) 85 | .padding(.vertical, 4) 86 | .padding(.horizontal, 8) 87 | .background(Color.seminarOrange) 88 | .cornerRadius(20) 89 | Text(observed.eventStatus.rawValue) 90 | .font(.caption2) 91 | .fontWeight(.bold) 92 | .foregroundColor(observed.eventStatus.statusColor) 93 | .padding(.vertical, 4) 94 | .padding(.horizontal, 8) 95 | .overlay( 96 | RoundedRectangle(cornerRadius: 20) 97 | .stroke(observed.eventStatus.statusColor, lineWidth: 1) 98 | ) 99 | Spacer() 100 | } 101 | NavigationLink { 102 | EventDetailView(event: observed.event) 103 | } label: { 104 | Text("\(observed.event.type) 살펴보기 \(Image(systemName: "arrow.right"))") 105 | .font(.footnote) 106 | .fontWeight(.bold) 107 | } 108 | } 109 | .padding(.horizontal, 16) 110 | .padding(.vertical, 30) 111 | } 112 | 113 | @ViewBuilder 114 | var onLoadingCell: some View { 115 | VStack(alignment: .leading, spacing: 0) { 116 | customDivider 117 | VStack(alignment: .leading, spacing: 0) { 118 | Rectangle() 119 | .frame(width: 250, height: 20) 120 | .cornerRadius(4) 121 | .foregroundColor(.gray) 122 | .opacity(0.4) 123 | .padding(.bottom, 4) 124 | Rectangle() 125 | .frame(width: 70, height: 20) 126 | .cornerRadius(4) 127 | .foregroundColor(.gray) 128 | .opacity(0.2) 129 | } 130 | .padding(.horizontal) 131 | .padding(.bottom, 27) 132 | .padding(.top, 31) 133 | } 134 | } 135 | 136 | @ViewBuilder 137 | func makeSessionCell(for session: Session) -> some View { 138 | NavigationLink { 139 | SessionView(session: session) 140 | } label: { 141 | VStack { 142 | customDivider 143 | HStack { 144 | VStack(alignment: .leading, spacing: 2) { 145 | Text(session.title) 146 | .font(.headline) 147 | .foregroundColor(.black) 148 | .multilineTextAlignment(.leading) 149 | Text("\(session.speaker.name) 님") 150 | .font(.body) 151 | .foregroundColor(.black) 152 | } 153 | .padding(.vertical, 30) 154 | Spacer() 155 | VStack { 156 | Image(systemName: "chevron.right") 157 | .font(Font.system(size: 30, weight: .light)) 158 | .foregroundColor(.black) 159 | } 160 | } 161 | .padding(.horizontal) 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /AsyncSwift/Views/Profile/ProfileEditView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileEditView.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/11/04. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProfileEditView: View { 11 | @Environment(\.dismiss) private var dismiss 12 | @ObservedObject var observed: ProfileEditViewObserved 13 | 14 | init(user: User) { 15 | observed = ProfileEditViewObserved(user: user) 16 | } 17 | 18 | var body: some View { 19 | VStack(spacing: 0) { 20 | Header 21 | ScrollView { 22 | VStack(spacing: 0) { 23 | nameTextField 24 | nicknameTextField 25 | jobTitleTextField 26 | descriptionTextField 27 | linkedInTextField 28 | privateURLTextField 29 | } 30 | } 31 | Spacer() 32 | } 33 | .navigationBarTitle("Edit", displayMode: .large) 34 | .toolbar { 35 | submitButton 36 | } 37 | } 38 | } 39 | 40 | private extension ProfileEditView { 41 | var Header: some View { 42 | VStack(spacing: 0) { 43 | VStack(spacing: 0) { 44 | HStack(spacing: 0) { 45 | Text("프로필을 수정") 46 | .font(.title3) 47 | .fontWeight(.bold) 48 | .frame(minHeight: 24) 49 | .padding(.bottom, 3) 50 | Spacer() 51 | } 52 | HStack(spacing: 0) { 53 | Text("특수문자는 입력할 수 없어요") 54 | .font(.footnote) 55 | .frame(minHeight: 18) 56 | Spacer() 57 | } 58 | } 59 | .padding(.top, 38) 60 | .padding(.bottom, 20) 61 | .padding(.horizontal) 62 | customDivider 63 | } 64 | .alert("프로필 등록 완료", isPresented: $observed.isShowingSuccessAlert, actions: { 65 | Button("확인", role: .cancel) { dismiss() } 66 | }, message: { 67 | Text("개인 프로필이 수정되었습니다.") 68 | }) 69 | .alert("프로필 등록 오류", isPresented: $observed.isShowingFailureAlert, actions: { 70 | Button("다시 시도", role: .cancel) { } 71 | }, message: { 72 | Text("입력되지 않은 내용이 있습니다.\n필수 입력란을 확인해주세요.") 73 | }) 74 | .alert("프로필 입력 오류", isPresented: $observed.isShowingInputFailureAlert, actions: { 75 | Button("다시 시도", role: .cancel) { } 76 | }, message: { 77 | Text("확인되지 않은 주소입니다.\nURL을 확인해주세요.") 78 | }) 79 | } 80 | 81 | 82 | var nameTextField: some View { 83 | VStack(spacing: 0) { 84 | HStack(spacing: 0) { 85 | Text("이름") 86 | .profileInputTitle 87 | TextField("", text: $observed.user.name) 88 | .profileTextField 89 | .placeholder( 90 | when: observed.user.name.isEmpty, 91 | text: "Required", 92 | isTextField: true 93 | ) 94 | Spacer() 95 | } 96 | customDivider 97 | .padding(.top, 15) 98 | } 99 | .padding(.leading) 100 | .padding(.top, 23) 101 | } 102 | 103 | var nicknameTextField: some View { 104 | VStack(spacing: 0) { 105 | HStack(spacing: 0) { 106 | Text("닉네임") 107 | .profileInputTitle 108 | TextField("", text: $observed.user.nickname) 109 | .profileTextField 110 | .placeholder( 111 | when: observed.user.nickname.isEmpty, 112 | text: "Optional", 113 | isTextField: true 114 | ) 115 | Spacer() 116 | } 117 | customDivider 118 | .padding(.top, 15) 119 | } 120 | .padding(.leading) 121 | .padding(.top, 23) 122 | } 123 | 124 | var jobTitleTextField: some View { 125 | VStack(spacing: 0) { 126 | HStack(spacing: 0) { 127 | Text("직군") 128 | .profileInputTitle 129 | TextField("", text: $observed.user.role) 130 | .profileTextField 131 | .placeholder( 132 | when: observed.user.role.isEmpty, 133 | text: "Required", 134 | isTextField: true 135 | ) 136 | Spacer() 137 | } 138 | customDivider 139 | .padding(.top, 15) 140 | } 141 | .padding(.leading) 142 | .padding(.top, 23) 143 | } 144 | 145 | var descriptionTextField: some View { 146 | VStack(spacing: 0) { 147 | HStack(alignment: .top, spacing: 0) { 148 | Text("소개") 149 | .profileInputTitle 150 | if #available(iOS 16.0, *) { 151 | TextEditor(text: $observed.description) 152 | .profileTextEditor 153 | .scrollContentBackground(.hidden) 154 | .placeholder( 155 | when: observed.description.isEmpty, 156 | text: "Optional, 80자 이내", 157 | isTextField: false 158 | ) 159 | .offset(x: -2, y: -8) 160 | } else { 161 | TextEditor(text: $observed.description) 162 | .profileTextEditor 163 | .placeholder( 164 | when: observed.description.isEmpty, 165 | text: "Optional, 80자 이내", 166 | isTextField: false 167 | ) 168 | } 169 | 170 | } 171 | customDivider 172 | .padding(.top, 15) 173 | } 174 | .padding(.leading) 175 | .padding(.top, 23) 176 | .frame(height: 91) 177 | } 178 | 179 | var linkedInTextField: some View { 180 | VStack(spacing: 0) { 181 | HStack(spacing: 0) { 182 | Text("링크드인 프로필 URL") 183 | .profileInputTitle 184 | Spacer() 185 | } 186 | .padding(.top, 20) 187 | HStack(spacing: 0) { 188 | TextField("", text: $observed.linkedInURL) 189 | .profileTextField 190 | .placeholder( 191 | when: observed.linkedInURL.isEmpty, 192 | text: "Optional", 193 | isTextField: true 194 | ) 195 | } 196 | .padding(.top, 5) 197 | customDivider 198 | .padding(.top, 15) 199 | } 200 | .padding(.leading) 201 | } 202 | 203 | var privateURLTextField: some View { 204 | VStack(spacing: 0) { 205 | HStack(spacing: 0) { 206 | Text("개인 페이지 URL") 207 | .profileInputTitle 208 | Spacer() 209 | } 210 | .padding(.top, 20) 211 | HStack(spacing: 0) { 212 | TextField("", text: $observed.profileURL) 213 | .profileTextField 214 | .placeholder( 215 | when: observed.profileURL.isEmpty, 216 | text: "Optional", 217 | isTextField: true 218 | ) 219 | } 220 | .padding(.top, 5) 221 | } 222 | .padding(.leading) 223 | } 224 | 225 | var submitButton: some View { 226 | Button { 227 | if observed.isButtonAvailable() { 228 | observed.didTapRegisterButton() 229 | } 230 | } label: { 231 | Text("Save") 232 | .foregroundColor( 233 | observed.isButtonAvailable() ? 234 | Color.accentColor : 235 | Color.unavailableButtonBackground 236 | ) 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /AsyncSwift/Views/Profile/ProfileRegisterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileRegisterView.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/10/28. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProfileRegisterView: View { 11 | @Environment(\.dismiss) private var dismiss 12 | @ObservedObject var observed: ProfileRegisterViewObserved 13 | 14 | init(hasRegisteredProfile: Binding, userID: Binding) { 15 | observed = ProfileRegisterViewObserved( 16 | hasRegisteredProfile: hasRegisteredProfile, 17 | userID: userID 18 | ) 19 | } 20 | 21 | var body: some View { 22 | VStack(spacing: 0) { 23 | Header 24 | ScrollView { 25 | VStack(spacing: 0) { 26 | nameTextField 27 | nicknameTextField 28 | jobTitleTextField 29 | descriptionTextField 30 | linkedInTextField 31 | privateURLTextField 32 | } 33 | } 34 | Spacer() 35 | } 36 | .navigationBarTitle("Register", displayMode: .large) 37 | .toolbar { 38 | submitButton 39 | } 40 | } 41 | } 42 | 43 | private extension ProfileRegisterView { 44 | var Header: some View { 45 | VStack(spacing: 0) { 46 | VStack(spacing: 0) { 47 | HStack(spacing: 0) { 48 | Text("프로필을 등록해주세요") 49 | .font(.title3) 50 | .fontWeight(.bold) 51 | .frame(minHeight: 24) 52 | .padding(.bottom, 3) 53 | Spacer() 54 | } 55 | HStack(spacing: 0) { 56 | Text("특수문자는 입력할 수 없어요") 57 | .font(.footnote) 58 | .frame(minHeight: 18) 59 | Spacer() 60 | } 61 | } 62 | .padding(.top, 38) 63 | .padding(.bottom, 20) 64 | .padding(.horizontal) 65 | customDivider 66 | } 67 | .alert("프로필 등록 완료", isPresented: $observed.isShowingSuccessAlert, actions: { 68 | Button("확인", role: .cancel) { dismiss() } 69 | }, message: { 70 | Text("개인 프로필이 추가되었습니다.") 71 | }) 72 | .alert("프로필 등록 오류", isPresented: $observed.isShowingFailureAlert, actions: { 73 | Button("다시 시도", role: .cancel) { } 74 | }, message: { 75 | Text("입력되지 않은 내용이 있습니다.\n필수 입력란을 확인해주세요.") 76 | }) 77 | .alert("프로필 입력 오류", isPresented: $observed.isShowingInputFailureAlert, actions: { 78 | Button("다시 시도", role: .cancel) { } 79 | }, message: { 80 | Text("확인되지 않은 주소입니다.\nURL을 확인해주세요.") 81 | }) 82 | } 83 | 84 | 85 | var nameTextField: some View { 86 | VStack(spacing: 0) { 87 | HStack(spacing: 0) { 88 | Text("이름") 89 | .profileInputTitle 90 | TextField("", text: $observed.name) 91 | .profileTextField 92 | .placeholder( 93 | when: observed.name.isEmpty, 94 | text: "Required", 95 | isTextField: true 96 | ) 97 | Spacer() 98 | } 99 | customDivider 100 | .padding(.top, 15) 101 | } 102 | .padding(.leading) 103 | .padding(.top, 23) 104 | } 105 | 106 | var nicknameTextField: some View { 107 | VStack(spacing: 0) { 108 | HStack(spacing: 0) { 109 | Text("닉네임") 110 | .profileInputTitle 111 | TextField("", text: $observed.nickname) 112 | .profileTextField 113 | .placeholder( 114 | when: observed.nickname.isEmpty, 115 | text: "Optional", 116 | isTextField: true 117 | ) 118 | Spacer() 119 | } 120 | customDivider 121 | .padding(.top, 15) 122 | } 123 | .padding(.leading) 124 | .padding(.top, 23) 125 | } 126 | 127 | var jobTitleTextField: some View { 128 | VStack(spacing: 0) { 129 | HStack(spacing: 0) { 130 | Text("직군") 131 | .profileInputTitle 132 | TextField("", text: $observed.role) 133 | .profileTextField 134 | .placeholder( 135 | when: observed.role.isEmpty, 136 | text: "Required", 137 | isTextField: true 138 | ) 139 | Spacer() 140 | } 141 | customDivider 142 | .padding(.top, 15) 143 | } 144 | .padding(.leading) 145 | .padding(.top, 23) 146 | } 147 | 148 | var descriptionTextField: some View { 149 | VStack(spacing: 0) { 150 | HStack(alignment: .top, spacing: 0) { 151 | Text("소개") 152 | .profileInputTitle 153 | if #available(iOS 16.0, *) { 154 | TextEditor(text: $observed.description) 155 | .profileTextEditor 156 | .scrollContentBackground(.hidden) 157 | .placeholder( 158 | when: observed.description.isEmpty, 159 | text: "Optional, 80자 이내", 160 | isTextField: false 161 | ) 162 | .offset(x: -2, y: -8) 163 | } else { 164 | TextEditor(text: $observed.description) 165 | .profileTextEditor 166 | .placeholder( 167 | when: observed.description.isEmpty, 168 | text: "Optional, 80자 이내", 169 | isTextField: false 170 | ) 171 | } 172 | 173 | } 174 | customDivider 175 | .padding(.top, 15) 176 | } 177 | .padding(.leading) 178 | .padding(.top, 23) 179 | .frame(height: 91) 180 | } 181 | 182 | var linkedInTextField: some View { 183 | VStack(spacing: 0) { 184 | HStack(spacing: 0) { 185 | Text("링크드인 프로필 URL") 186 | .profileInputTitle 187 | Spacer() 188 | } 189 | .padding(.top, 20) 190 | HStack(spacing: 0) { 191 | TextField("", text: $observed.linkedInURL) 192 | .profileTextField 193 | .placeholder( 194 | when: observed.linkedInURL.isEmpty, 195 | text: "Optional", 196 | isTextField: true 197 | ) 198 | } 199 | .padding(.top, 5) 200 | customDivider 201 | .padding(.top, 15) 202 | } 203 | .padding(.leading) 204 | } 205 | 206 | var privateURLTextField: some View { 207 | VStack(spacing: 0) { 208 | HStack(spacing: 0) { 209 | Text("개인 페이지 URL") 210 | .profileInputTitle 211 | Spacer() 212 | } 213 | .padding(.top, 20) 214 | HStack(spacing: 0) { 215 | TextField("", text: $observed.profileURL) 216 | .profileTextField 217 | .placeholder( 218 | when: observed.profileURL.isEmpty, 219 | text: "Optional", 220 | isTextField: true 221 | ) 222 | } 223 | .padding(.top, 5) 224 | } 225 | .padding(.leading) 226 | } 227 | 228 | var submitButton: some View { 229 | Button { 230 | if observed.isButtonAvailable() { 231 | observed.didTapRegisterButton() 232 | } 233 | } label: { 234 | Text("Save") 235 | .foregroundColor( 236 | observed.isButtonAvailable() ? 237 | Color.accentColor : 238 | Color.unavailableButtonBackground 239 | ) 240 | } 241 | } 242 | } 243 | 244 | -------------------------------------------------------------------------------- /AsyncSwift/Views/Profile/ProfileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileView.swift 3 | // AsyncSwift 4 | // 5 | // Created by Kim Insub on 2022/10/16. 6 | // 7 | 8 | import CodeScanner 9 | import SwiftUI 10 | 11 | // TODO 12 | // 1 : Enter 치면 다음 input focused 되도록 변경 13 | 14 | struct ProfileView: View { 15 | 16 | @StateObject var observed = ProfileViewObserved() 17 | 18 | var body: some View { 19 | NavigationView { 20 | VStack(spacing: 0) { 21 | header 22 | Spacer() 23 | if observed.hasRegisteredProfile { 24 | friendsListLinkButton 25 | } 26 | editProfileLinkButton 27 | } 28 | .navigationTitle("Profile") 29 | .navigationBarItems(trailing: codeScannerButton) 30 | .fullScreenCover( 31 | isPresented: $observed.isShowingScanner, 32 | content: { scannerView } 33 | ) 34 | .fullScreenCover( 35 | isPresented: $observed.isShowingUserDetail, 36 | content: { scannedFriendDetail } 37 | ) 38 | .onAppear { 39 | observed.onAppear() 40 | } 41 | } 42 | .alert("프로필 등록 오류", isPresented: $observed.isShowingFailureAlert, actions: { 43 | Button("확인", role: .cancel) { observed.isShowingFailureAlert = false } 44 | }, message: { 45 | Text("이미 등록된 프로필입니다.") 46 | }) 47 | .alert("QR 등록 오류", isPresented: $observed.isShowingScanErrorAlert, actions: { 48 | Button("취소", role: .cancel) { observed.isShowingScanErrorAlert = false } 49 | }, message: { 50 | Text("등록할 수 없는 QR코드입니다.") 51 | }) 52 | } 53 | } 54 | 55 | private extension ProfileView { 56 | @ViewBuilder 57 | var header: some View { 58 | if observed.hasRegisteredProfile { 59 | if observed.isLoading { 60 | hasRegisteredHeaderLoadingView 61 | } else { 62 | hasRegisteredHeader 63 | } 64 | } else { 65 | registerHeader 66 | } 67 | } 68 | 69 | var hasRegisteredHeaderLoadingView: some View { 70 | VStack(spacing: 0) { 71 | customDivider 72 | .padding(.top, 10) 73 | .padding(.bottom, 56) 74 | Rectangle() 75 | .frame(width: 130, height: 130) 76 | .foregroundColor(Color.skeletonQR) 77 | .padding(.bottom, 26) 78 | Rectangle() 79 | .frame(width: 165, height: 23) 80 | .foregroundColor(Color.skeletonName) 81 | .cornerRadius(4) 82 | .padding(.bottom, 7) 83 | Rectangle() 84 | .frame(width: 91, height: 16) 85 | .foregroundColor(Color.skeletonName) 86 | .cornerRadius(4) 87 | .padding(.bottom, 25) 88 | VStack(alignment: .leading, spacing: 0) { 89 | Rectangle() 90 | .frame(width: 309, height: 16) 91 | .foregroundColor(Color.skeletonDescription) 92 | .cornerRadius(4) 93 | .padding(.bottom, 3) 94 | Rectangle() 95 | .frame(width: 309, height: 16) 96 | .foregroundColor(Color.skeletonDescription) 97 | .cornerRadius(4) 98 | .padding(.bottom, 3) 99 | Rectangle() 100 | .frame(width: 221, height: 16) 101 | .foregroundColor(Color.skeletonDescription) 102 | .cornerRadius(4) 103 | } 104 | } 105 | } 106 | 107 | var hasRegisteredHeader: some View { 108 | VStack(spacing: 0) { 109 | customDivider 110 | .padding(.top, 10) 111 | .padding(.bottom, 55) 112 | Image(uiImage: observed.getQRCodeImage()) 113 | .interpolation(.none) 114 | .resizable() 115 | .frame(width: 157, height: 157) 116 | .padding(.bottom, 40) 117 | Text("\(observed.user.name) | \(observed.user.nickname)") 118 | .font(.title3) 119 | .fontWeight(.semibold) 120 | .padding(.bottom, 4) 121 | Text("\(observed.user.role)") 122 | .font(.subheadline) 123 | .fontWeight(.semibold) 124 | .foregroundColor(.profileGray) 125 | .padding(.bottom, 18) 126 | Text("\(observed.user.description)") 127 | .font(.footnote) 128 | .padding(.horizontal, 43) 129 | } 130 | } 131 | 132 | var registerHeader: some View { 133 | VStack(spacing: 0) { 134 | Image("QRplaceholder") 135 | .frame(width: 157) 136 | .padding(.bottom, 50) 137 | Text("등록한 프로필이 없습니다.") 138 | .foregroundColor(.profileGray) 139 | .font(.body) 140 | .padding(.bottom, 17) 141 | registerLink 142 | } 143 | .padding(.top, 68) 144 | } 145 | 146 | var registerLink: some View { 147 | NavigationLink { 148 | ProfileRegisterView( 149 | hasRegisteredProfile: $observed.hasRegisteredProfile, 150 | userID: $observed.userID 151 | ) 152 | } label: { 153 | Text("프로필 등록하기") 154 | .font(.headline) 155 | } 156 | } 157 | 158 | @ViewBuilder 159 | var friendsListLinkButton: some View { 160 | if observed.isLoading { 161 | Button { } label: { 162 | linkLabelButtonLabel(text: "Friends") 163 | .opacity(0.2) 164 | } 165 | .padding(.bottom, 16) 166 | } else { 167 | NavigationLink( 168 | destination: ProfileFriendsListView( 169 | inActive: $observed.isShowingFriends, 170 | user: observed.user), 171 | isActive: $observed.isShowingFriends, 172 | label: { 173 | Button { 174 | observed.isShowingFriends = true 175 | } label: { 176 | linkLabelButtonLabel(text: "Friends") 177 | } 178 | } 179 | ) 180 | .padding(.bottom, 16) 181 | } 182 | } 183 | 184 | @ViewBuilder 185 | var editProfileLinkButton: some View { 186 | switch observed.hasRegisteredProfile { 187 | case true: 188 | switch observed.isLoading { 189 | case true: 190 | Button { } label: { 191 | linkLabelButtonLabel(text: "Edit Profile") 192 | .opacity(0.2) 193 | } 194 | .padding(.bottom, 32) 195 | case false: 196 | NavigationLink( 197 | destination: ProfileEditView(user: observed.user), 198 | isActive: $observed.isShowingEdit, 199 | label: { 200 | Button { 201 | observed.isShowingEdit = true 202 | } label: { 203 | linkLabelButtonLabel(text: "Edit Profile") 204 | } 205 | }) 206 | .padding(.bottom, 32) 207 | } 208 | case false: 209 | NavigationLink { 210 | ProfileRegisterView( 211 | hasRegisteredProfile: $observed.hasRegisteredProfile, 212 | userID: $observed.userID 213 | ) 214 | } label: { 215 | linkLabelButtonLabel(text: "Edit Profile") 216 | } 217 | .padding(.bottom, 32) 218 | } 219 | } 220 | 221 | @ViewBuilder 222 | var codeScannerButton: some View { 223 | if observed.hasRegisteredProfile && !observed.isLoading { 224 | Button { 225 | observed.isShowingScanner = true 226 | } label: { 227 | Image(systemName: "qrcode.viewfinder") 228 | } 229 | } else { 230 | Button { 231 | 232 | } label: { 233 | Image(systemName: "qrcode.viewfinder") 234 | .foregroundColor(.gray) 235 | } 236 | } 237 | } 238 | 239 | var scannerView: some View { 240 | VStack { 241 | ZStack { 242 | Text("코드스캔") 243 | HStack { 244 | Spacer() 245 | Button { 246 | observed.didTapCloseButton() 247 | } label: { 248 | Text("Done") 249 | } 250 | .padding() 251 | } 252 | } 253 | .frame(height: 51) 254 | CodeScannerView( 255 | codeTypes: [.qr], 256 | simulatedData: "1AA5CC09-6F7F-4EC4-A2BE-819B93362B7B", 257 | completion: observed.handleScan 258 | ) 259 | HStack { 260 | Text("QR코드를 스캔해 보세요. 프로필 상세 정보를 확인할 수 있습니다.") 261 | .font(.caption2) 262 | } 263 | .frame(height: 70) 264 | } 265 | } 266 | 267 | func linkLabelButtonLabel(text: String) -> some View { 268 | HStack { 269 | Text(text) 270 | .font(.headline) 271 | Spacer() 272 | Image(systemName: "chevron.forward") 273 | } 274 | .foregroundColor(.black) 275 | .padding(.horizontal, 19) 276 | .padding(.vertical, 23) 277 | .frame(maxWidth: .infinity, maxHeight: 68) 278 | .background(Color.buttonBackground) 279 | .cornerRadius(15) 280 | .padding(.horizontal) 281 | } 282 | 283 | var scannedFriendDetail: some View { 284 | NavigationView { 285 | VStack { 286 | ProfileFriendDetailView( 287 | previous: .ProfileView, 288 | inActive: $observed.isShowingUserDetail, 289 | user: observed.user, 290 | friend: observed.scannedFriend 291 | ) 292 | } 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /AsyncSwift/Assets.xcassets/Seminar002StampFront.imageset/Seminar002StampFront.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << /Length 2 0 R >> 5 | stream 6 | 0.519531 0 0.006348 -0.171875 0.513184 0.695801 d1 7 | 8 | endstream 9 | endobj 10 | 11 | 2 0 obj 12 | 51 13 | endobj 14 | 15 | 3 0 obj 16 | << /Length 4 0 R >> 17 | stream 18 | 0.403000 0 0.012000 -0.013000 0.391000 0.433000 d1 19 | 20 | endstream 21 | endobj 22 | 23 | 4 0 obj 24 | 51 25 | endobj 26 | 27 | 5 0 obj 28 | << /Length 6 0 R >> 29 | stream 30 | 0.671387 0 0.013184 0.000000 0.658691 0.717773 d1 31 | 32 | endstream 33 | endobj 34 | 35 | 6 0 obj 36 | 50 37 | endobj 38 | 39 | 7 0 obj 40 | << /Length 8 0 R >> 41 | stream 42 | 0.528809 0 0.028320 -0.010254 0.506348 0.566895 d1 43 | 44 | endstream 45 | endobj 46 | 47 | 8 0 obj 48 | 51 49 | endobj 50 | 51 | 9 0 obj 52 | << /Length 10 0 R >> 53 | stream 54 | 0.228027 0 0.046387 0.000000 0.181641 0.771484 d1 55 | 56 | endstream 57 | endobj 58 | 59 | 10 0 obj 60 | 50 61 | endobj 62 | 63 | 11 0 obj 64 | << /Length 12 0 R >> 65 | stream 66 | 0.335449 0 0.009277 -0.010254 0.302246 0.668457 d1 67 | 68 | endstream 69 | endobj 70 | 71 | 12 0 obj 72 | 51 73 | endobj 74 | 75 | 13 0 obj 76 | << /Length 14 0 R >> 77 | stream 78 | 0.646000 0 0.040000 0.000000 0.605000 0.448000 d1 79 | 80 | endstream 81 | endobj 82 | 83 | 14 0 obj 84 | 50 85 | endobj 86 | 87 | 15 0 obj 88 | << /Length 16 0 R >> 89 | stream 90 | 0.496582 0 0.027344 -0.010254 0.469238 0.565918 d1 91 | 92 | endstream 93 | endobj 94 | 95 | 16 0 obj 96 | 51 97 | endobj 98 | 99 | 17 0 obj 100 | << /Length 18 0 R >> 101 | stream 102 | 0.762695 0 0.010742 0.000000 0.751953 0.528320 d1 103 | 104 | endstream 105 | endobj 106 | 107 | 18 0 obj 108 | 50 109 | endobj 110 | 111 | 19 0 obj 112 | << /Length 20 0 R >> 113 | stream 114 | 0.617188 0 0.032715 -0.012207 0.584961 0.761719 d1 115 | 116 | endstream 117 | endobj 118 | 119 | 20 0 obj 120 | 51 121 | endobj 122 | 123 | 21 0 obj 124 | << /Length 22 0 R >> 125 | stream 126 | 0.455000 0 0.012000 -0.013000 0.414000 0.433000 d1 127 | 128 | endstream 129 | endobj 130 | 131 | 22 0 obj 132 | 51 133 | endobj 134 | 135 | 23 0 obj 136 | << /Length 24 0 R >> 137 | stream 138 | 0.558105 0 0.049805 0.000000 0.510742 0.578125 d1 139 | 140 | endstream 141 | endobj 142 | 143 | 24 0 obj 144 | 50 145 | endobj 146 | 147 | 25 0 obj 148 | << /Length 26 0 R >> 149 | stream 150 | 0.336914 0 0.012695 0.000000 0.308594 0.727539 d1 151 | 152 | endstream 153 | endobj 154 | 155 | 26 0 obj 156 | 50 157 | endobj 158 | 159 | 27 0 obj 160 | << /Length 28 0 R >> 161 | stream 162 | 0.465000 0 0.023000 0.000000 0.431000 0.646000 d1 163 | 164 | endstream 165 | endobj 166 | 167 | 28 0 obj 168 | 50 169 | endobj 170 | 171 | 29 0 obj 172 | << /Length 30 0 R >> 173 | stream 174 | 0.465000 0 0.015000 -0.016000 0.450000 0.654000 d1 175 | 176 | endstream 177 | endobj 178 | 179 | 30 0 obj 180 | 51 181 | endobj 182 | 183 | 31 0 obj 184 | << /Length 32 0 R >> 185 | stream 186 | 0.295000 0 0.040000 0.000000 0.299000 0.448000 d1 187 | 188 | endstream 189 | endobj 190 | 191 | 32 0 obj 192 | 50 193 | endobj 194 | 195 | 33 0 obj 196 | << /Length 34 0 R >> 197 | stream 198 | 0.334000 0 0.000000 0.000000 0.334000 1.000000 d1 199 | 200 | endstream 201 | endobj 202 | 203 | 34 0 obj 204 | 50 205 | endobj 206 | 207 | 35 0 obj 208 | << /Length 36 0 R >> 209 | stream 210 | 0.435000 0 0.040000 0.000000 0.394000 0.448000 d1 211 | 212 | endstream 213 | endobj 214 | 215 | 36 0 obj 216 | 50 217 | endobj 218 | 219 | 37 0 obj 220 | << /Length 38 0 R >> 221 | stream 222 | 0.204000 0 0.045000 0.000000 0.159000 0.663000 d1 223 | 224 | endstream 225 | endobj 226 | 227 | 38 0 obj 228 | 50 229 | endobj 230 | 231 | 39 0 obj 232 | << /Length 40 0 R >> 233 | stream 234 | 0.423000 0 0.019000 -0.016000 0.405000 0.658000 d1 235 | 236 | endstream 237 | endobj 238 | 239 | 40 0 obj 240 | 51 241 | endobj 242 | 243 | 41 0 obj 244 | [ 0.335449 0.336914 0.528809 0.519531 0.228027 0.762695 0.617188 0.496582 0.558105 0.671387 0.465000 0.465000 0.295000 0.334000 0.435000 0.455000 0.646000 0.204000 0.403000 0.423000 ] 245 | endobj 246 | 247 | 42 0 obj 248 | << /Length 43 0 R >> 249 | stream 250 | /CIDInit /ProcSet findresource begin 251 | 12 dict begin 252 | begincmap 253 | /CIDSystemInfo 254 | << /Registry (FigmaPDF) 255 | /Ordering (FigmaPDF) 256 | /Supplement 0 257 | >> def 258 | /CMapName /A-B-C def 259 | /CMapType 2 def 260 | 1 begincodespacerange 261 | <00> 262 | endcodespacerange 263 | 1 beginbfchar 264 | <00> <0074> 265 | endbfchar 266 | 1 beginbfchar 267 | <01> <0066> 268 | endbfchar 269 | 1 beginbfchar 270 | <02> <0063> 271 | endbfchar 272 | 1 beginbfchar 273 | <03> <0079> 274 | endbfchar 275 | 1 beginbfchar 276 | <04> <0069> 277 | endbfchar 278 | 1 beginbfchar 279 | <05> <0077> 280 | endbfchar 281 | 1 beginbfchar 282 | <06> <0053> 283 | endbfchar 284 | 1 beginbfchar 285 | <07> <0073> 286 | endbfchar 287 | 1 beginbfchar 288 | <08> <006E> 289 | endbfchar 290 | 1 beginbfchar 291 | <09> <0041> 292 | endbfchar 293 | 1 beginbfchar 294 | <0A> <0032> 295 | endbfchar 296 | 1 beginbfchar 297 | <0B> <0030> 298 | endbfchar 299 | 1 beginbfchar 300 | <0C> <0072> 301 | endbfchar 302 | 1 beginbfchar 303 | <0D> <0020> 304 | endbfchar 305 | 1 beginbfchar 306 | <0E> <006E> 307 | endbfchar 308 | 1 beginbfchar 309 | <0F> <0061> 310 | endbfchar 311 | 1 beginbfchar 312 | <10> <006D> 313 | endbfchar 314 | 1 beginbfchar 315 | <11> <0069> 316 | endbfchar 317 | 1 beginbfchar 318 | <12> <0065> 319 | endbfchar 320 | 1 beginbfchar 321 | <13> <0053> 322 | endbfchar 323 | endcmap 324 | CMapName currentdict /CMap defineresource pop 325 | end 326 | end 327 | endstream 328 | endobj 329 | 330 | 43 0 obj 331 | 1016 332 | endobj 333 | 334 | 44 0 obj 335 | << /Subtype /Type3 336 | /CharProcs << /C9 5 0 R 337 | /C3 1 0 R 338 | /C18 3 0 R 339 | /C2 7 0 R 340 | /C4 9 0 R 341 | /C7 15 0 R 342 | /C0 11 0 R 343 | /C16 13 0 R 344 | /C5 17 0 R 345 | /C6 19 0 R 346 | /C15 21 0 R 347 | /C8 23 0 R 348 | /C1 25 0 R 349 | /C10 27 0 R 350 | /C11 29 0 R 351 | /C12 31 0 R 352 | /C13 33 0 R 353 | /C14 35 0 R 354 | /C17 37 0 R 355 | /C19 39 0 R 356 | >> 357 | /Encoding << /Type /Encoding 358 | /Differences [ 0 /C0 /C1 /C2 /C3 /C4 /C5 /C6 /C7 /C8 /C9 /C10 /C11 /C12 /C13 /C14 /C15 /C16 /C17 /C18 /C19 ] 359 | >> 360 | /Widths 41 0 R 361 | /FontBBox [ 0.000000 0.000000 0.000000 0.000000 ] 362 | /FontMatrix [ 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 ] 363 | /Type /Font 364 | /ToUnicode 42 0 R 365 | /FirstChar 0 366 | /LastChar 19 367 | /Resources << >> 368 | >> 369 | endobj 370 | 371 | 45 0 obj 372 | << /Font << /F1 44 0 R >> >> 373 | endobj 374 | 375 | 46 0 obj 376 | << /Length 47 0 R >> 377 | stream 378 | /DeviceRGB CS 379 | /DeviceRGB cs 380 | q 381 | 1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm 382 | 1.000000 1.000000 1.000000 scn 383 | 0.000000 500.000000 m 384 | 0.000000 516.568542 13.431458 530.000000 30.000000 530.000000 c 385 | 288.000000 530.000000 l 386 | 304.568542 530.000000 318.000000 516.568542 318.000000 500.000000 c 387 | 318.000000 30.000000 l 388 | 318.000000 13.431458 304.568542 0.000000 288.000000 0.000000 c 389 | 29.999996 0.000000 l 390 | 13.431454 0.000000 0.000000 13.431458 0.000000 30.000000 c 391 | 0.000000 500.000000 l 392 | h 393 | f 394 | n 395 | Q 396 | q 397 | 1.000000 0.000000 -0.000000 1.000000 84.000000 0.000000 cm 398 | 1.000000 0.266667 0.149020 scn 399 | 0.000000 32.179817 m 400 | 0.000000 53.718853 17.460831 71.179688 38.999866 71.179688 c 401 | 38.999866 71.179688 l 402 | 60.538902 71.179688 77.999733 53.718853 77.999733 32.179817 c 403 | 77.999733 0.000015 l 404 | 0.000000 0.000015 l 405 | 0.000000 32.179817 l 406 | h 407 | f 408 | n 409 | Q 410 | q 411 | 1.000000 0.000000 -0.000000 1.000000 162.000000 0.000000 cm 412 | 1.000000 0.266667 0.149020 scn 413 | 0.000000 77.388809 m 414 | 0.000000 98.927849 17.460831 116.388672 38.999866 116.388672 c 415 | 38.999866 116.388672 l 416 | 60.538902 116.388672 77.999733 98.927834 77.999733 77.388794 c 417 | 77.999733 0.000290 l 418 | 0.000000 0.000290 l 419 | 0.000000 77.388809 l 420 | h 421 | f 422 | n 423 | Q 424 | q 425 | 1.000000 0.000000 -0.000000 1.000000 240.000000 0.000000 cm 426 | 1.000000 0.266667 0.149020 scn 427 | 0.000000 122.597664 m 428 | 0.000000 144.136765 17.460896 161.597656 39.000000 161.597656 c 429 | 39.000000 161.597656 l 430 | 60.539104 161.597656 78.000000 144.136765 78.000000 122.597656 c 431 | 78.000000 30.000549 l 432 | 78.000000 13.432007 64.568542 0.000565 48.000000 0.000565 c 433 | 0.000000 0.000565 l 434 | 0.000000 122.597664 l 435 | h 436 | f 437 | n 438 | Q 439 | q 440 | 1.000000 0.000000 -0.000000 1.000000 18.000000 289.060547 cm 441 | 0.000000 0.000000 0.000000 scn 442 | 0.000000 -1.060547 m 443 | h 444 | 0.263672 -1.060547 m 445 | 2.900391 -1.060547 l 446 | 4.091797 2.582031 l 447 | 9.335938 2.582031 l 448 | 10.527344 -1.060547 l 449 | 13.173828 -1.060547 l 450 | 8.134766 13.031250 l 451 | 5.302734 13.031250 l 452 | 0.263672 -1.060547 l 453 | h 454 | 6.689453 10.521484 m 455 | 6.748047 10.521484 l 456 | 8.701172 4.525391 l 457 | 4.726562 4.525391 l 458 | 6.689453 10.521484 l 459 | h 460 | 13.758906 -1.060547 m 461 | h 462 | 18.797970 -1.265625 m 463 | 21.337032 -1.265625 23.143673 0.042969 23.143673 1.996094 c 464 | 23.143673 2.005859 l 465 | 23.143673 3.509766 22.313595 4.349609 20.174923 4.828125 c 466 | 18.446407 5.208984 l 467 | 17.372189 5.453125 16.971798 5.843750 16.971798 6.439453 c 468 | 16.971798 6.449219 l 469 | 16.971798 7.210938 17.645626 7.708984 18.719845 7.708984 c 470 | 19.852657 7.708984 20.516720 7.103516 20.624142 6.273438 c 471 | 20.633907 6.195312 l 472 | 22.899532 6.195312 l 473 | 22.889767 6.302734 l 474 | 22.801876 8.089844 21.297970 9.505859 18.719845 9.505859 c 475 | 16.219845 9.505859 14.569453 8.226562 14.569453 6.332031 c 476 | 14.569453 6.322266 l 477 | 14.569453 4.789062 15.565547 3.822266 17.547970 3.382812 c 478 | 19.266720 3.001953 l 479 | 20.340939 2.757812 20.702267 2.406250 20.702267 1.781250 c 480 | 20.702267 1.771484 l 481 | 20.702267 1.009766 19.989376 0.531250 18.807735 0.531250 c 482 | 17.577267 0.531250 16.903439 1.058594 16.717892 1.947266 c 483 | 16.698360 2.035156 l 484 | 14.305781 2.035156 l 485 | 14.315547 1.947266 l 486 | 14.530391 0.023438 16.083126 -1.265625 18.797970 -1.265625 c 487 | h 488 | 24.041250 -1.060547 m 489 | h 490 | 26.424063 -4.498047 m 491 | 28.582266 -4.498047 29.793203 -3.687500 30.584219 -1.412109 c 492 | 34.304924 9.291016 l 493 | 31.746328 9.291016 l 494 | 29.314688 1.009766 l 495 | 29.246328 1.009766 l 496 | 26.824453 9.291016 l 497 | 24.168203 9.291016 l 498 | 27.888906 -1.070312 l 499 | 27.742422 -1.490234 l 500 | 27.429922 -2.359375 26.912344 -2.623047 25.984610 -2.623047 c 501 | 25.642813 -2.623047 25.359610 -2.574219 25.174063 -2.535156 c 502 | 25.174063 -4.390625 l 503 | 25.467031 -4.439453 25.955313 -4.498047 26.424063 -4.498047 c 504 | h 505 | 34.811874 -1.060547 m 506 | h 507 | 35.807968 -1.060547 m 508 | 38.239609 -1.060547 l 509 | 38.239609 4.945312 l 510 | 38.239609 6.439453 39.128281 7.455078 40.524765 7.455078 c 511 | 41.911484 7.455078 42.595078 6.625000 42.595078 5.150391 c 512 | 42.595078 -1.060547 l 513 | 45.026718 -1.060547 l 514 | 45.026718 5.638672 l 515 | 45.026718 8.041016 43.727890 9.505859 41.452499 9.505859 c 516 | 39.880234 9.505859 38.825546 8.783203 38.288437 7.669922 c 517 | 38.239609 7.669922 l 518 | 38.239609 9.291016 l 519 | 35.807968 9.291016 l 520 | 35.807968 -1.060547 l 521 | h 522 | 46.363750 -1.060547 m 523 | h 524 | 51.930157 -1.265625 m 525 | 54.547344 -1.265625 56.295391 0.375000 56.480938 2.533203 c 526 | 56.490704 2.601562 l 527 | 54.195782 2.601562 l 528 | 54.176250 2.513672 l 529 | 53.951641 1.429688 53.170391 0.697266 51.939922 0.697266 c 530 | 50.406719 0.697266 49.410625 1.947266 49.410625 4.105469 c 531 | 49.410625 4.115234 l 532 | 49.410625 6.234375 50.396954 7.533203 51.930157 7.533203 c 533 | 53.219219 7.533203 53.961407 6.722656 54.166485 5.716797 c 534 | 54.186016 5.628906 l 535 | 56.480938 5.628906 l 536 | 56.471172 5.707031 l 537 | 56.324688 7.777344 54.625469 9.505859 51.900860 9.505859 c 538 | 48.883282 9.505859 46.930157 7.435547 46.930157 4.134766 c 539 | 46.930157 4.125000 l 540 | 46.930157 0.804688 48.853985 -1.265625 51.930157 -1.265625 c 541 | h 542 | 57.329689 -1.060547 m 543 | h 544 | 63.521095 -1.304688 m 545 | 66.958595 -1.304688 69.028908 0.365234 69.028908 2.992188 c 546 | 69.028908 3.001953 l 547 | 69.028908 5.199219 67.749611 6.390625 64.819923 6.996094 c 548 | 63.296486 7.308594 l 549 | 61.597267 7.660156 60.825783 8.246094 60.825783 9.242188 c 550 | 60.825783 9.251953 l 551 | 60.825783 10.375000 61.851173 11.146484 63.501564 11.156250 c 552 | 65.083595 11.156250 66.167580 10.423828 66.333595 9.193359 c 553 | 66.353127 9.076172 l 554 | 68.765236 9.076172 l 555 | 68.755470 9.242188 l 556 | 68.608986 11.654297 66.597267 13.275391 63.521095 13.275391 c 557 | 60.483986 13.275391 58.306252 11.595703 58.296486 9.115234 c 558 | 58.296486 9.105469 l 559 | 58.296486 7.005859 59.663673 5.716797 62.437111 5.140625 c 560 | 63.950783 4.828125 l 561 | 65.767189 4.447266 66.499611 3.880859 66.499611 2.826172 c 562 | 66.499611 2.816406 l 563 | 66.499611 1.605469 65.386330 0.814453 63.608986 0.814453 c 564 | 61.841408 0.814453 60.601173 1.566406 60.415627 2.777344 c 565 | 60.396095 2.894531 l 566 | 57.983986 2.894531 l 567 | 57.993752 2.748047 l 568 | 58.159767 0.218750 60.288673 -1.304688 63.521095 -1.304688 c 569 | h 570 | 70.053436 -1.060547 m 571 | h 572 | 73.071014 -1.060547 m 573 | 75.610077 -1.060547 l 574 | 77.641327 6.507812 l 575 | 77.690155 6.507812 l 576 | 79.731171 -1.060547 l 577 | 82.289764 -1.060547 l 578 | 85.092499 9.291016 l 579 | 82.690155 9.291016 l 580 | 80.942108 1.341797 l 581 | 80.883514 1.341797 l 582 | 78.862030 9.291016 l 583 | 76.528046 9.291016 l 584 | 74.506561 1.341797 l 585 | 74.457733 1.341797 l 586 | 72.709686 9.291016 l 587 | 70.268280 9.291016 l 588 | 73.071014 -1.060547 l 589 | h 590 | 85.687347 -1.060547 m 591 | h 592 | 87.972504 10.775391 m 593 | 88.734222 10.775391 89.320160 11.380859 89.320160 12.103516 c 594 | 89.320160 12.845703 88.734222 13.441406 87.972504 13.441406 c 595 | 87.210785 13.441406 86.615082 12.845703 86.615082 12.103516 c 596 | 86.615082 11.380859 87.210785 10.775391 87.972504 10.775391 c 597 | h 598 | 86.751801 -1.060547 m 599 | 89.183441 -1.060547 l 600 | 89.183441 9.291016 l 601 | 86.751801 9.291016 l 602 | 86.751801 -1.060547 l 603 | h 604 | 90.637657 -1.060547 m 605 | h 606 | 92.307579 -1.060547 m 607 | 94.739220 -1.060547 l 608 | 94.739220 7.416016 l 609 | 96.770470 7.416016 l 610 | 96.770470 9.291016 l 611 | 94.690392 9.291016 l 612 | 94.690392 10.208984 l 613 | 94.690392 11.048828 95.129845 11.488281 96.077110 11.488281 c 614 | 96.340782 11.488281 96.614220 11.468750 96.809532 11.439453 c 615 | 96.809532 13.138672 l 616 | 96.506798 13.197266 95.989220 13.236328 95.510704 13.236328 c 617 | 93.264610 13.236328 92.307579 12.298828 92.307579 10.277344 c 618 | 92.307579 9.291016 l 619 | 90.891563 9.291016 l 620 | 90.891563 7.416016 l 621 | 92.307579 7.416016 l 622 | 92.307579 -1.060547 l 623 | h 624 | 97.755936 -1.060547 m 625 | h 626 | 102.599686 -1.265625 m 627 | 103.087967 -1.265625 103.517654 -1.216797 103.800858 -1.177734 c 628 | 103.800858 0.648438 l 629 | 103.634842 0.638672 103.449295 0.609375 103.214920 0.609375 c 630 | 102.326248 0.609375 101.877029 0.931641 101.877029 1.947266 c 631 | 101.877029 7.416016 l 632 | 103.800858 7.416016 l 633 | 103.800858 9.291016 l 634 | 101.877029 9.291016 l 635 | 101.877029 11.917969 l 636 | 99.406326 11.917969 l 637 | 99.406326 9.291016 l 638 | 97.941483 9.291016 l 639 | 97.941483 7.416016 l 640 | 99.406326 7.416016 l 641 | 99.406326 1.742188 l 642 | 99.406326 -0.386719 100.431717 -1.265625 102.599686 -1.265625 c 643 | h 644 | f 645 | n 646 | Q 647 | q 648 | 1.000000 0.000000 -0.000000 1.000000 18.000000 289.060547 cm 649 | BT 650 | 20.000000 0.000000 0.000000 20.000000 0.000000 -1.060547 Tm 651 | /F1 1.000000 Tf 652 | [ (\t) -16.558599 (\007) -17.535162 (\003) -18.999958 (\010) -19.488335 (\002) -19.488335 (\006) -18.999863 (\005) -19.000244 (\004) -19.488144 (\001) -18.999863 (\000) ] TJ 653 | ET 654 | Q 655 | q 656 | 1.000000 0.000000 -0.000000 1.000000 18.000000 258.439453 cm 657 | 0.000000 0.000000 0.000000 scn 658 | 0.000000 -7.439453 m 659 | h 660 | 13.480000 11.920547 m 661 | 12.440001 13.387215 11.120000 14.120548 9.520000 14.120548 c 662 | 8.506667 14.120548 7.653334 13.813882 6.960001 13.200548 c 663 | 6.266667 12.587214 5.920000 11.760548 5.920000 10.720548 c 664 | 5.920000 9.707213 7.173333 8.493879 9.680000 7.080547 c 665 | 12.213334 5.667213 13.493334 4.960546 13.520001 4.960546 c 666 | 15.306667 3.627214 16.200001 2.053881 16.200001 0.240547 c 667 | 16.200001 -2.239452 15.480000 -4.252787 14.040001 -5.799454 c 668 | 12.626667 -7.319454 10.680000 -8.079453 8.200001 -8.079453 c 669 | 5.053334 -8.079453 2.573334 -6.506119 0.760000 -3.359453 c 670 | 3.360000 -0.799454 l 671 | 3.706667 -1.732786 4.346667 -2.586121 5.280000 -3.359453 c 672 | 6.213334 -4.132786 7.173334 -4.519453 8.160001 -4.519453 c 673 | 9.333334 -4.519453 10.253333 -4.119453 10.920000 -3.319452 c 674 | 11.613334 -2.519453 11.960001 -1.546120 11.960001 -0.399452 c 675 | 11.960001 0.427214 11.706667 1.147215 11.200000 1.760548 c 676 | 10.693334 2.400547 8.973333 3.360546 6.040000 4.640547 c 677 | 3.133333 5.947214 1.680000 7.800549 1.680000 10.200547 c 678 | 1.680000 12.253881 2.346667 13.973881 3.680000 15.360549 c 679 | 5.040000 16.773882 6.733334 17.480549 8.760000 17.480549 c 680 | 11.560000 17.480549 13.773334 16.573881 15.400001 14.760548 c 681 | 13.480000 11.920547 l 682 | h 683 | 16.914062 -7.439453 m 684 | h 685 | 21.514063 0.080547 m 686 | 21.514063 -1.439453 21.847397 -2.692787 22.514063 -3.679453 c 687 | 23.180729 -4.666119 24.100729 -5.159452 25.274063 -5.159452 c 688 | 27.114063 -5.159452 28.287397 -4.252787 28.794064 -2.439453 c 689 | 32.474064 -3.039454 l 690 | 31.274063 -6.319454 28.874063 -7.959454 25.274063 -7.959454 c 691 | 22.874063 -7.959454 20.954063 -7.199455 19.514063 -5.679453 c 692 | 18.100729 -4.132786 17.394062 -2.132786 17.394062 0.320547 c 693 | 17.394062 2.907213 18.074062 4.973881 19.434063 6.520548 c 694 | 20.820730 8.093882 22.687397 8.880548 25.034063 8.880548 c 695 | 27.407396 8.880548 29.247396 8.133883 30.554064 6.640549 c 696 | 31.887396 5.173882 32.554062 3.160547 32.554062 0.600548 c 697 | 32.554062 0.080547 l 698 | 21.514063 0.080547 l 699 | h 700 | 28.954063 2.240547 m 701 | 28.954063 3.333881 28.607397 4.293882 27.914062 5.120548 c 702 | 27.247396 5.947214 26.367395 6.360548 25.274063 6.360548 c 703 | 24.207396 6.360548 23.327396 5.920547 22.634064 5.040546 c 704 | 21.967398 4.187214 21.634064 3.253881 21.634064 2.240547 c 705 | 28.954063 2.240547 l 706 | h 707 | 33.046875 -7.439453 m 708 | h 709 | 38.526875 6.960548 m 710 | 39.513542 8.240549 41.033543 8.880548 43.086876 8.880548 c 711 | 44.873543 8.880548 46.286877 8.053881 47.326878 6.400547 c 712 | 48.340210 8.053881 49.980209 8.880548 52.246876 8.880548 c 713 | 55.580208 8.880548 57.246876 6.720549 57.246876 2.400547 c 714 | 57.246876 -7.439453 l 715 | 53.406876 -7.439453 l 716 | 53.406876 0.880547 l 717 | 53.406876 4.240547 52.486877 5.920547 50.646877 5.920547 c 718 | 48.806877 5.920547 47.886875 4.053881 47.886875 0.320547 c 719 | 47.886875 -7.439453 l 720 | 44.006874 -7.439453 l 721 | 44.006874 0.560547 l 722 | 44.006874 4.133881 43.193542 5.920547 41.566875 5.920547 c 723 | 39.540207 5.920547 38.526875 4.053881 38.526875 0.320547 c 724 | 38.526875 -7.439453 l 725 | 34.646873 -7.439453 l 726 | 34.646873 8.360548 l 727 | 38.526875 8.360548 l 728 | 38.526875 6.960548 l 729 | h 730 | 58.906250 -7.439453 m 731 | h 732 | 65.266251 15.000549 m 733 | 65.266251 14.387216 65.039581 13.853882 64.586250 13.400548 c 734 | 64.132919 12.947214 63.599583 12.720547 62.986252 12.720547 c 735 | 62.372917 12.720547 61.839584 12.947214 61.386250 13.400548 c 736 | 60.932915 13.853882 60.706249 14.387216 60.706249 15.000549 c 737 | 60.706249 15.613883 60.932915 16.147217 61.386250 16.600548 c 738 | 61.839584 17.053883 62.372917 17.280548 62.986252 17.280548 c 739 | 63.599583 17.280548 64.132919 17.053883 64.586250 16.600548 c 740 | 65.039581 16.147217 65.266251 15.613883 65.266251 15.000549 c 741 | h 742 | 64.946251 -7.439453 m 743 | 61.066250 -7.439453 l 744 | 61.066250 8.360548 l 745 | 64.946251 8.360548 l 746 | 64.946251 -7.439453 l 747 | h 748 | 67.070312 -7.439453 m 749 | h 750 | 72.550316 6.920547 m 751 | 73.643646 8.227215 75.256981 8.880548 77.390312 8.880548 c 752 | 81.016983 8.880548 82.830315 6.653881 82.830315 2.200548 c 753 | 82.830315 -7.439453 l 754 | 78.990311 -7.439453 l 755 | 78.990311 0.920547 l 756 | 78.990311 4.253881 77.963646 5.920547 75.910316 5.920547 c 757 | 73.670319 5.920547 72.550316 4.133881 72.550316 0.560547 c 758 | 72.550316 -7.439453 l 759 | 68.670311 -7.439453 l 760 | 68.670311 8.360548 l 761 | 72.550316 8.360548 l 762 | 72.550316 6.920547 l 763 | h 764 | 84.453125 -7.439453 m 765 | h 766 | 101.013123 -7.439453 m 767 | 97.213127 -7.439453 l 768 | 97.213127 -5.999453 l 769 | 96.093124 -7.306122 94.426460 -7.959454 92.213127 -7.959454 c 770 | 89.999794 -7.959454 88.226463 -7.106121 86.893127 -5.399452 c 771 | 85.586464 -3.692785 84.933128 -1.692785 84.933128 0.600548 c 772 | 84.933128 2.893881 85.599792 4.840548 86.933128 6.440548 c 773 | 88.293121 8.067215 90.026459 8.880548 92.133125 8.880548 c 774 | 94.373123 8.880548 96.066460 8.213881 97.213127 6.880547 c 775 | 97.213127 8.360548 l 776 | 101.013123 8.360548 l 777 | 101.013123 -7.439453 l 778 | h 779 | 97.653122 0.480547 m 780 | 97.653122 1.947214 97.239792 3.147215 96.413124 4.080547 c 781 | 95.586456 5.040546 94.519791 5.520546 93.213127 5.520546 c 782 | 91.933121 5.520546 90.879791 5.053879 90.053123 4.120546 c 783 | 89.253128 3.213881 88.853127 2.000547 88.853127 0.480547 c 784 | 88.853127 -1.039454 89.266457 -2.266121 90.093124 -3.199453 c 785 | 90.919792 -4.132786 91.986458 -4.599453 93.293129 -4.599453 c 786 | 94.599792 -4.599453 95.653130 -4.146120 96.453125 -3.239452 c 787 | 97.253120 -2.332785 97.653122 -1.092785 97.653122 0.480547 c 788 | h 789 | 102.656250 -7.439453 m 790 | h 791 | 108.136253 6.720547 m 792 | 109.122917 8.160547 110.669586 8.880548 112.776253 8.880548 c 793 | 113.442917 8.880548 114.056252 8.747215 114.616249 8.480549 c 794 | 114.136253 4.960546 l 795 | 113.549583 5.333879 112.909584 5.520546 112.216248 5.520546 c 796 | 109.496254 5.520546 108.136253 3.573879 108.136253 -0.319452 c 797 | 108.136253 -7.439453 l 798 | 104.256248 -7.439453 l 799 | 104.256248 8.360548 l 800 | 108.136253 8.360548 l 801 | 108.136253 6.720547 l 802 | h 803 | 127.812500 -7.439453 m 804 | h 805 | 145.812500 4.680548 m 806 | 145.812500 1.160547 145.039169 -1.852787 143.492508 -4.359453 c 807 | 141.945831 -6.839455 139.812500 -8.079453 137.092499 -8.079453 c 808 | 134.399170 -8.079453 132.279175 -6.826118 130.732498 -4.319452 c 809 | 129.185837 -1.812786 128.412506 1.187214 128.412506 4.680548 c 810 | 128.412506 8.307215 129.185837 11.347214 130.732498 13.800548 c 811 | 132.305832 16.253881 134.439163 17.480549 137.132507 17.480549 c 812 | 139.825836 17.480549 141.945831 16.227215 143.492508 13.720549 c 813 | 145.039169 11.240547 145.812500 8.227215 145.812500 4.680548 c 814 | h 815 | 132.412506 4.720547 m 816 | 132.412506 2.267214 132.839172 0.093880 133.692505 -1.799454 c 817 | 134.545837 -3.666121 135.705841 -4.599453 137.172501 -4.599453 c 818 | 138.639160 -4.599453 139.772491 -3.719454 140.572495 -1.959454 c 819 | 141.399170 -0.199453 141.812500 2.027214 141.812500 4.720547 c 820 | 141.812500 7.200548 141.385834 9.360548 140.532501 11.200548 c 821 | 139.679169 13.067214 138.532501 14.000547 137.092499 14.000547 c 822 | 135.679169 14.000547 134.545837 13.027214 133.692505 11.080548 c 823 | 132.839172 9.160549 132.412506 7.040548 132.412506 4.720547 c 824 | h 825 | 146.406250 -7.439453 m 826 | h 827 | 164.406250 4.680548 m 828 | 164.406250 1.160547 163.632919 -1.852787 162.086258 -4.359453 c 829 | 160.539581 -6.839455 158.406250 -8.079453 155.686249 -8.079453 c 830 | 152.992920 -8.079453 150.872925 -6.826118 149.326248 -4.319452 c 831 | 147.779587 -1.812786 147.006256 1.187214 147.006256 4.680548 c 832 | 147.006256 8.307215 147.779587 11.347214 149.326248 13.800548 c 833 | 150.899582 16.253881 153.032913 17.480549 155.726257 17.480549 c 834 | 158.419586 17.480549 160.539581 16.227215 162.086258 13.720549 c 835 | 163.632919 11.240547 164.406250 8.227215 164.406250 4.680548 c 836 | h 837 | 151.006256 4.720547 m 838 | 151.006256 2.267214 151.432922 0.093880 152.286255 -1.799454 c 839 | 153.139587 -3.666121 154.299591 -4.599453 155.766251 -4.599453 c 840 | 157.232910 -4.599453 158.366241 -3.719454 159.166245 -1.959454 c 841 | 159.992920 -0.199453 160.406250 2.027214 160.406250 4.720547 c 842 | 160.406250 7.200548 159.979584 9.360548 159.126251 11.200548 c 843 | 158.272919 13.067214 157.126251 14.000547 155.686249 14.000547 c 844 | 154.272919 14.000547 153.139587 13.027214 152.286255 11.080548 c 845 | 151.432922 9.160549 151.006256 7.040548 151.006256 4.720547 c 846 | h 847 | 165.000000 -7.439453 m 848 | h 849 | 182.240005 -3.959454 m 850 | 182.240005 -7.439453 l 851 | 165.919998 -7.439453 l 852 | 172.080002 0.360548 l 853 | 175.013336 3.987215 176.733337 6.253881 177.240005 7.160547 c 854 | 177.773331 8.093880 178.039993 8.853880 178.039993 9.440548 c 855 | 178.066666 9.600548 178.080002 9.760548 178.080002 9.920547 c 856 | 178.080002 10.987214 177.720001 11.933881 177.000000 12.760547 c 857 | 176.306671 13.587214 175.386673 14.000547 174.240005 14.000547 c 858 | 173.093338 14.000547 172.186676 13.533881 171.520004 12.600549 c 859 | 170.880005 11.667215 170.559998 10.533881 170.559998 9.200546 c 860 | 166.240005 9.200546 l 861 | 166.240005 11.600548 166.946671 13.573881 168.360001 15.120547 c 862 | 169.800003 16.693882 171.746674 17.480549 174.199997 17.480549 c 863 | 176.413330 17.480549 178.279999 16.733881 179.800003 15.240548 c 864 | 181.320007 13.747215 182.080002 11.880548 182.080002 9.640548 c 865 | 182.080002 7.533880 180.466675 4.600548 177.240005 0.840548 c 866 | 173.119995 -3.959454 l 867 | 182.240005 -3.959454 l 868 | h 869 | f 870 | n 871 | Q 872 | q 873 | 1.000000 0.000000 -0.000000 1.000000 18.000000 258.439453 cm 874 | BT 875 | 40.000000 0.000000 0.000000 40.000000 0.000000 -7.439453 Tm 876 | /F1 1.000000 Tf 877 | [ (\023) (\022) (\020) (\021) (\016) (\017) (\014) (\015) (\013) (\013) (\n) ] TJ 878 | ET 879 | Q 880 | 881 | endstream 882 | endobj 883 | 884 | 47 0 obj 885 | 16907 886 | endobj 887 | 888 | 48 0 obj 889 | << /Annots [] 890 | /Type /Page 891 | /MediaBox [ 0.000000 0.000000 318.000000 530.000000 ] 892 | /Resources 45 0 R 893 | /Contents 46 0 R 894 | /Parent 49 0 R 895 | >> 896 | endobj 897 | 898 | 49 0 obj 899 | << /Kids [ 48 0 R ] 900 | /Count 1 901 | /Type /Pages 902 | >> 903 | endobj 904 | 905 | 50 0 obj 906 | << /Pages 49 0 R 907 | /Type /Catalog 908 | >> 909 | endobj 910 | 911 | xref 912 | 0 51 913 | 0000000000 65535 f 914 | 0000000010 00000 n 915 | 0000000117 00000 n 916 | 0000000138 00000 n 917 | 0000000245 00000 n 918 | 0000000266 00000 n 919 | 0000000372 00000 n 920 | 0000000393 00000 n 921 | 0000000500 00000 n 922 | 0000000521 00000 n 923 | 0000000628 00000 n 924 | 0000000650 00000 n 925 | 0000000759 00000 n 926 | 0000000781 00000 n 927 | 0000000889 00000 n 928 | 0000000911 00000 n 929 | 0000001020 00000 n 930 | 0000001042 00000 n 931 | 0000001150 00000 n 932 | 0000001172 00000 n 933 | 0000001281 00000 n 934 | 0000001303 00000 n 935 | 0000001412 00000 n 936 | 0000001434 00000 n 937 | 0000001542 00000 n 938 | 0000001564 00000 n 939 | 0000001672 00000 n 940 | 0000001694 00000 n 941 | 0000001802 00000 n 942 | 0000001824 00000 n 943 | 0000001933 00000 n 944 | 0000001955 00000 n 945 | 0000002063 00000 n 946 | 0000002085 00000 n 947 | 0000002193 00000 n 948 | 0000002215 00000 n 949 | 0000002323 00000 n 950 | 0000002345 00000 n 951 | 0000002453 00000 n 952 | 0000002475 00000 n 953 | 0000002584 00000 n 954 | 0000002606 00000 n 955 | 0000002809 00000 n 956 | 0000003883 00000 n 957 | 0000003907 00000 n 958 | 0000005001 00000 n 959 | 0000005049 00000 n 960 | 0000022014 00000 n 961 | 0000022039 00000 n 962 | 0000022218 00000 n 963 | 0000022294 00000 n 964 | trailer 965 | << /ID [ (some) (id) ] 966 | /Root 50 0 R 967 | /Size 51 968 | >> 969 | startxref 970 | 22355 971 | %%EOF --------------------------------------------------------------------------------