├── .gitattributes ├── .gitignore ├── .swiftlint.yml ├── LICENSE ├── Pico.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── Pico.xcscheme ├── Pico ├── Chat │ ├── Cell │ │ ├── ChatReceiveListTableViewCell.swift │ │ ├── ChatSendListTableViewCell.swift │ │ └── RoomListTableViewCell.swift │ ├── ChatDetailViewController.swift │ ├── RoomTableListController.swift │ └── ViewModel │ │ ├── ChatDetailViewModel.swift │ │ └── RoomViewModel.swift ├── Common │ ├── Constraints │ │ ├── CommonConstraints.swift │ │ ├── Defaults.swift │ │ ├── MypageViewConstraints.swift │ │ ├── Screen.swift │ │ └── SignViewConstraints.swift │ ├── Transition │ │ └── CustomTransitionAnimator.swift │ └── View │ │ ├── CommonButton.swift │ │ ├── CommonTextField.swift │ │ ├── CustomIndicator.swift │ │ ├── FooterView.swift │ │ ├── InputPopupViewController.swift │ │ ├── Loading.swift │ │ ├── LoadingAnimationView.swift │ │ ├── MBTILabelView.swift │ │ ├── PaddingLabel.swift │ │ ├── PopupViewController.swift │ │ └── SwitchButton.swift ├── Config │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ └── Group 127.png │ │ ├── AppIcon │ │ │ ├── AppIcon_gray.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── Group 124 (2).png │ │ │ └── Contents.json │ │ ├── Background │ │ │ ├── Contents.json │ │ │ └── background.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── background.png │ │ ├── Contents.json │ │ ├── Logo │ │ │ ├── Contents.json │ │ │ ├── logo.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── pico (2).png │ │ │ ├── logo_black.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── logo_black.png │ │ │ └── logo_white.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── pico (7).png │ │ ├── detail │ │ │ ├── Contents.json │ │ │ ├── religion.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── reilgion.png │ │ │ └── smoke.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── smoke.png │ │ ├── game │ │ │ ├── Contents.json │ │ │ ├── banner.imageset │ │ │ │ ├── 'Pico (10)..png │ │ │ │ └── Contents.json │ │ │ ├── gameBackground.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── Group 141.png │ │ │ ├── gameMusic.dataset │ │ │ │ ├── Contents.json │ │ │ │ └── gameMusic.mp3 │ │ │ └── vsImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── vsImage.png │ │ └── images │ │ │ ├── Contents.json │ │ │ ├── chu.imageset │ │ │ ├── Contents.json │ │ │ └── chu.png │ │ │ ├── detectiveChu.imageset │ │ │ ├── Contents.json │ │ │ └── Image.png │ │ │ ├── detectiveChu2.imageset │ │ │ ├── Contents.json │ │ │ └── Image.png │ │ │ ├── guideGesture.imageset │ │ │ ├── Contents.json │ │ │ └── Image.png │ │ │ ├── guideTab.imageset │ │ │ ├── Contents.json │ │ │ └── Image.png │ │ │ ├── infp.imageset │ │ │ ├── Contents.json │ │ │ └── infp.png │ │ │ ├── locationPointImage.imageset │ │ │ ├── Contents.json │ │ │ └── locationPointImage.png │ │ │ ├── magnifier.imageset │ │ │ ├── Contents.json │ │ │ └── search_1.png │ │ │ ├── mbtiImage.imageset │ │ │ ├── Contents.json │ │ │ └── mbtiImage.png │ │ │ ├── myChat.imageset │ │ │ ├── Contents.json │ │ │ ├── sendMessage1.png │ │ │ ├── sendMessage2.png │ │ │ └── sendMessage3.png │ │ │ ├── pageStick.imageset │ │ │ ├── Contents.json │ │ │ └── Image.png │ │ │ ├── pencilImage.imageset │ │ │ ├── Contents.json │ │ │ └── pencilImage.png │ │ │ ├── premiumImage.imageset │ │ │ ├── Contents.json │ │ │ └── premiumImage.png │ │ │ ├── randomBoxImage.imageset │ │ │ ├── Contents.json │ │ │ └── randomBoxImage.png │ │ │ ├── tempImage.imageset │ │ │ ├── Contents.json │ │ │ └── tempImage.png │ │ │ └── yourChat.imageset │ │ │ ├── Contents.json │ │ │ ├── receive1.png │ │ │ ├── receive2.png │ │ │ └── receive3.png │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ ├── SceneDelegate.swift │ └── ko.lproj │ │ └── LaunchScreen.strings ├── Ent │ ├── Cell │ │ ├── WorldCupCollectionViewCell.swift │ │ └── WorldCupUserInfoStackView.swift │ ├── EntViewController.swift │ ├── ViewModel │ │ ├── WorldCupResultViewModel.swift │ │ └── WorldCupViewModel.swift │ ├── WorldCupGameViewController.swift │ ├── WorldCupResultViewController.swift │ └── worldcupAnimation.json ├── Extension │ ├── Data │ │ ├── Array+Extensions.swift │ │ ├── Bundle+Extenstions.swift │ │ ├── Double+Extensions.swift │ │ ├── Encodable+Extensions.swift │ │ ├── Int+Extensions.swift │ │ └── String+Extensions.swift │ ├── Namespace │ │ ├── UIColor+Extensions.swift │ │ └── UIFont+Extensions.swift │ └── UI │ │ ├── DismissKeyboard.swift │ │ ├── TrickTextField+Extensions.swift │ │ ├── UIButton+Extensions.swift │ │ ├── UICollectionView+Extensions.swift │ │ ├── UIImage+Extensions.swift │ │ ├── UIImageView+Extensions.swift │ │ ├── UILabel+Extensions.swift │ │ ├── UIStackView+Extensions.swift │ │ ├── UITableView+Extensions.swift │ │ ├── UITextFieldDelegate+Extensions.swift │ │ ├── UIView+Extensions.swift │ │ └── UIViewController+Extensions.swift ├── Home │ ├── Cell │ │ └── MBTILabelCollectionViewCell.swift │ ├── CompatibilityView.swift │ ├── Detail │ │ ├── AboutMeViewController.swift │ │ ├── BasicInformationViewContoller.swift │ │ ├── IntroViewController.swift │ │ ├── SubInfomationViewController.swift │ │ ├── UserDetailViewCell │ │ │ ├── AboutMeCollectionViewCell.swift │ │ │ ├── HobbyCollectionViewCell.swift │ │ │ ├── LeftAlignedCollectionViewFlowLayout.swift │ │ │ └── MbtiCollectionViewCell.swift │ │ ├── UserDetailViewController.swift │ │ ├── UserImageViewController.swift │ │ └── ViewModel │ │ │ └── UserDetailViewModel.swift │ ├── HomeEmptyLottie.json │ ├── HomeEmptyView.swift │ ├── HomeFilterViewController.swift │ ├── HomeGuideView.swift │ ├── HomeUserCardViewController.swift │ ├── HomeViewController.swift │ ├── MBTICollectionViewController.swift │ ├── RangeSliderView.swift │ └── ViewModel │ │ ├── HomeUserCardViewModel.swift │ │ └── HomeViewModel.swift ├── Like │ ├── Cell │ │ ├── CollectionViewFooterLoadingCell.swift │ │ └── LikeCollectionViewCell.swift │ ├── EmptyViewController.swift │ ├── LikeMeViewController.swift │ ├── LikeUViewController.swift │ ├── LikeViewController.swift │ └── ViewModel │ │ ├── LikeMeViewModel.swift │ │ └── LikeUViewModel.swift ├── Mail │ ├── Cell │ │ └── MailListTableViewCell.swift │ ├── MailListTableViewCell.swift │ ├── MailReceiveTableListController.swift │ ├── MailReceiveViewController.swift │ ├── MailSendTableListController.swift │ ├── MailSendViewController.swift │ ├── MailViewController.swift │ └── ViewModel │ │ ├── MailReceiveViewModel.swift │ │ └── MailSendViewModel.swift ├── Model │ ├── Block │ │ └── Block.swift │ ├── Chat │ │ └── Chat.swift │ ├── Like │ │ └── Like.swift │ ├── Location │ │ └── Location.swift │ ├── MBTI │ │ ├── MBTI.swift │ │ └── MBTIManager.swift │ ├── Mail │ │ └── Mail.swift │ ├── Notification │ │ └── Notification.swift │ ├── Payment │ │ └── Payment.swift │ ├── Report │ │ └── Report.swift │ ├── Stop │ │ └── Stop.swift │ ├── SubInfo │ │ └── SubInfo.swift │ ├── Token │ │ └── Token.swift │ ├── Unsubscribe │ │ └── Unsubscribe.swift │ └── User │ │ ├── CurrentUser.swift │ │ └── User.swift ├── Mypage │ ├── AdvertisementViewController.swift │ ├── Cell │ │ ├── MyPageCollectionCell.swift │ │ ├── MyPageCollectionTableCell.swift │ │ ├── MyPageDefaultTableCell.swift │ │ └── MyPageMatchingTableCell.swift │ ├── CircularProgressBarView.swift │ ├── MyPageTableView.swift │ ├── MypageViewController.swift │ ├── PremiumViewController.swift │ ├── ProfileEdit │ │ ├── Cell │ │ │ ├── ProfileEditCollectionCell.swift │ │ │ ├── ProfileEditEmptyCollectionCell.swift │ │ │ ├── ProfileEditImageTableCell.swift │ │ │ ├── ProfileEditIntroTabelCell.swift │ │ │ ├── ProfileEditLoactionTabelCell.swift │ │ │ ├── ProfileEditModalCollectionCell.swift │ │ │ ├── ProfileEditModalMbtiCell.swift │ │ │ ├── ProfileEditNicknameTabelCell.swift │ │ │ ├── ProfileEditTextModalCollectionCell.swift │ │ │ └── ProfileEditTextTabelCell.swift │ │ ├── CenterAlignedCollectionViewFlowLayout.swift │ │ ├── ProfileEditCollTextModalViewController.swift │ │ ├── ProfileEditCollectionModalViewController.swift │ │ ├── ProfileEditNicknameModalViewController.swift │ │ ├── ProfileEditPickerViewController.swift │ │ ├── ProfileEditTableHeaderView.swift │ │ ├── ProfileEditTextModalViewController.swift │ │ ├── ProfileEditViewController.swift │ │ └── ViewModel │ │ │ ├── ProfileEditModalViewModel.swift │ │ │ ├── ProfileEditViewModel.swift │ │ │ └── SectionModel.swift │ ├── ProfileView.swift │ ├── RandomBox │ │ ├── RandomBoxViewController.swift │ │ ├── ViewModel │ │ │ └── RandomBoxViewModel.swift │ │ └── randomBox.json │ ├── Setting │ │ ├── Cell │ │ │ ├── SettingNotiTableCell.swift │ │ │ ├── SettingPrivateTableCell.swift │ │ │ └── SettingTableCell.swift │ │ ├── SettingDetail │ │ │ ├── SettingLicenseView.swift │ │ │ ├── SettingLicenseViewController.swift │ │ │ ├── SettingSecessionViewController.swift │ │ │ └── SettingSecessionViewModel.swift │ │ ├── SettingTableHeaderView.swift │ │ └── SettingViewController.swift │ ├── Store │ │ ├── StoreModel.swift │ │ ├── StoreTableBannerCell.swift │ │ ├── StoreTableCell.swift │ │ ├── StoreViewController.swift │ │ └── StoreViewModel.swift │ └── ViewModel │ │ ├── CircularProgressBarViewModel.swift │ │ └── ProfileViewModel.swift ├── Notification │ ├── NotificationTableViewCell.swift │ ├── NotificationViewController.swift │ └── NotificationViewModel.swift ├── Pico.entitlements ├── Service │ ├── CheckService.swift │ ├── FirestoreService.swift │ ├── KakaoAuthService.swift │ ├── KeyboardService.swift │ ├── LocationService.swift │ ├── NotificationService.swift │ ├── PictureService.swift │ ├── SMSAuthService.swift │ ├── StorageService.swift │ ├── UserService.swift │ ├── VersionService.swift │ └── VisionService.swift ├── Sign │ ├── AuthManager.swift │ ├── LocationService │ │ └── LocationService.swift │ ├── SignIn │ │ ├── LoginSuccessViewController.swift │ │ ├── SignInViewController.swift │ │ └── ViewModel │ │ │ └── SignInViewModel.swift │ ├── SignUp │ │ ├── Demo │ │ │ └── termsOfServiceText.swift │ │ ├── MbtiModalViewController.swift │ │ ├── SignUpAgeViewController.swift │ │ ├── SignUpCell │ │ │ └── SignUpPictureEditCollectionCell.swift │ │ ├── SignUpGenderViewController.swift │ │ ├── SignUpNickNameViewController.swift │ │ ├── SignUpPhoneNumberViewController.swift │ │ ├── SignUpPictureViewController.swift │ │ ├── SignUpTermsOfServiceViewController.swift │ │ ├── SignUpViewController.swift │ │ ├── TermsOfServiceText │ │ │ ├── TermsOfServiceModalViewController.swift │ │ │ └── TermsOfServiceText.swift │ │ └── ViewModel │ │ │ └── SignUpViewModel.swift │ └── SignViewController.swift ├── TabBar │ └── TabBarController.swift ├── UserDefaults │ └── UserDefaultsManager.swift └── Utils │ ├── BaseViewController.swift │ └── ViewModelType.swift ├── PicoTests └── PicoTests.swift ├── PicoUITests ├── PicoUITests.swift └── PicoUITestsLaunchTests.swift ├── README.md └── 사용자메뉴얼.pdf /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj binary merge=union 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | # 프로젝트의 모든 경로에서 .DS_Store 파일을 포함하지 않도록 함 93 | **/.DS_Store 94 | 95 | # 프로젝트의 모든 경로에서 .plist 파일을 포함하지 않도록 함 96 | /**/*.plist 97 | 98 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # Default Rules에서 비활성화할 규칙 2 | 3 | # 라인 뒤에 공백이 없어야 합니다. https://realm.github.io/SwiftLint/trailing_whitespace.html 4 | - trailing_whitespace 5 | 6 | # 강제 캐스팅은 피해야합니다. https://realm.github.io/SwiftLint/force_cast.html 7 | - force_cast 8 | 9 | # 강제 언래핑은 피해야합니다. https://realm.github.io/SwiftLint/force_unwrapping.html 10 | - force_unwrapping 11 | 12 | - line_length 13 | - function_body_length 14 | - function_parameter_count 15 | - type_body_length 16 | - void_return 17 | - cyclomatic_complexity 18 | - file_length 19 | - identifier_name 20 | - for_where 21 | 22 | opt_in_rules: # 기본(default) 룰이 아닌 룰들을 활성화 23 | # .count==0 보다는 .isEmpty를 사용하는 것이 좋습니다. https://realm.github.io/SwiftLint/empty_count.html 24 | - empty_count 25 | 26 | # 빈 String 문자열과 비교하는 것 보다는 .isEmpty를 사용하는 것이 좋습니다. https://realm.github.io/SwiftLint/empty_string.html 27 | - empty_string 28 | 29 | # operation 사용시 양옆에 공백이 있어야 합니다. https://realm.github.io/SwiftLint/operator_whitespace.html 30 | - operator_usage_whitespace 31 | 32 | # {}사용시 앞에 공백이 있어야 합니다. https://realm.github.io/SwiftLint/opening_brace.html 33 | - opening_brace 34 | 35 | # comma 앞에는 여백이 없고 뒤에는 공백이 있어야합니다. https://realm.github.io/SwiftLint/comma.html 36 | - comma 37 | 38 | # 주석 // 다음에 공백이 있어야 합니다. 39 | - comment_spacing 40 | 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ojeomsun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pico.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Pico.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Pico/Chat/Cell/ChatSendListTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatSendListTableViewCell.swift 3 | // Pico 4 | // 5 | // Created by 양성혜 on 2023/12/21. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | import RxSwift 11 | 12 | final class ChatSendListTableViewCell: UITableViewCell { 13 | 14 | private let chatView = UIView() 15 | 16 | private let messageLabel: UILabel = { 17 | let label = UILabel() 18 | label.font = UIFont.picoContentFont 19 | label.textColor = .white 20 | label.textAlignment = .right 21 | label.lineBreakMode = .byWordWrapping 22 | label.numberOfLines = 0 23 | label.setLineSpacing(spacing: 10) 24 | return label 25 | }() 26 | 27 | private var backgroundImageView: UIImageView = { 28 | let imageView = UIImageView(image: UIImage(named: ChatType.send.imageStyle)) 29 | return imageView 30 | }() 31 | 32 | private let dateLabel: UILabel = { 33 | let label = UILabel() 34 | label.font = UIFont.picoDescriptionFont 35 | label.textColor = .gray 36 | label.textAlignment = .right 37 | return label 38 | }() 39 | 40 | // MARK: - MailCell +LifeCycle 41 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 42 | super.init(style: style, reuseIdentifier: reuseIdentifier) 43 | self.contentView.backgroundColor = .clear 44 | addViews() 45 | makeConstraints() 46 | } 47 | 48 | @available(*, unavailable) 49 | required init?(coder: NSCoder) { 50 | fatalError("init(coder:) has not been implemented") 51 | } 52 | 53 | override func prepareForReuse() { 54 | messageLabel.text = "" 55 | dateLabel.text = "" 56 | } 57 | 58 | // MARK: - MailCell +UI 59 | func config(chatInfo: ChatDetail.ChatInfo) { 60 | self.messageLabel.text = chatInfo.message 61 | let date = chatInfo.sendedDate.timeAgoSinceDate() 62 | self.dateLabel.text = date 63 | } 64 | 65 | private func addViews() { 66 | contentView.addSubview([chatView]) 67 | chatView.addSubview([dateLabel, backgroundImageView, messageLabel]) 68 | } 69 | 70 | private func makeConstraints() { 71 | chatView.snp.makeConstraints { make in 72 | make.edges.equalToSuperview() 73 | } 74 | 75 | messageLabel.snp.makeConstraints { make in 76 | make.top.equalTo(chatView).offset(20) 77 | make.trailing.equalTo(chatView).offset(-20) 78 | make.bottom.equalTo(-10) 79 | } 80 | 81 | backgroundImageView.snp.makeConstraints { make in 82 | make.top.equalTo(messageLabel).offset(-10) 83 | make.leading.equalTo(messageLabel).offset(-10) 84 | make.trailing.equalTo(messageLabel).offset(15) 85 | make.bottom.equalTo(messageLabel).offset(10) 86 | } 87 | 88 | dateLabel.snp.makeConstraints { make in 89 | make.leading.equalTo(10) 90 | make.trailing.equalTo(backgroundImageView.snp.leading).offset(-10) 91 | make.bottom.equalTo(backgroundImageView.snp.bottom) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Pico/Common/Constraints/CommonConstraints.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonConstraints.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 10/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct CommonConstraints { 11 | /// 버튼 높이 (50) 12 | static let buttonHeight: CGFloat = 50 13 | } 14 | -------------------------------------------------------------------------------- /Pico/Common/Constraints/Defaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Defaults.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 1/18/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Defaults { 11 | static let userImageURLString = "https://firebasestorage.googleapis.com/v0/b/picotest-1692c.appspot.com/o/default_UserImage.png?alt=media&token=2d1916dd-fb1f-4c5f-a699-327d50f524c8" 12 | 13 | static let logoImageURLString = "https://firebasestorage.googleapis.com/v0/b/pico-9a941.appspot.com/o/logoImage.png?alt=media&token=6aab6173-106a-444f-be3e-90767aea5275" 14 | } 15 | -------------------------------------------------------------------------------- /Pico/Common/Constraints/MypageViewConstraints.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MypageViewConstraints.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 10/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct MypageView { 11 | /// 마이페이지의 프로필 뷰 높이 12 | static var profileViewHeight: CGFloat = 260 13 | 14 | /// 마이페이지의 프로필 최대 높이 (260) 15 | static let profileViewMaxHeight: CGFloat = 260 16 | } 17 | -------------------------------------------------------------------------------- /Pico/Common/Constraints/Screen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Screen.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import UIKit 9 | 10 | struct Screen { 11 | static let height = UIScreen.main.bounds.height 12 | static let width = UIScreen.main.bounds.width 13 | } 14 | -------------------------------------------------------------------------------- /Pico/Common/Constraints/SignViewConstraints.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignViewConstraints.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 10/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SignView { 11 | /// 제목 padding (15) 12 | static let padding: CGFloat = 15 13 | 14 | /// 서브제목 top padding (5) 15 | static let subPadding: CGFloat = 5 16 | 17 | /// 내용 padding (20) 18 | static let contentPadding: CGFloat = 20 19 | 20 | /// 버튼 bottomPadding (-30) 21 | static let bottomPadding: CGFloat = -30 22 | 23 | /// 프로그레스 뷰 cornerRadius (0) 24 | static let progressViewTopPadding: CGFloat = 0 25 | 26 | /// 프로그레스 뷰 cornerRadius (4) 27 | static let progressViewCornerRadius: CGFloat = 4 28 | } 29 | -------------------------------------------------------------------------------- /Pico/Common/Transition/CustomTransitionAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomTransitionAnimator.swift 3 | // Pico 4 | // 5 | // Created by 방유빈 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | 10 | final class CustomTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning { 11 | 12 | let viewControllers: [UIViewController]? 13 | let transitionDuration: Double = 0.3 14 | 15 | init(viewControllers: [UIViewController]?) { 16 | self.viewControllers = viewControllers 17 | } 18 | 19 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 20 | return TimeInterval(transitionDuration) 21 | } 22 | 23 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 24 | guard 25 | let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from), 26 | let fromView = fromVC.view, 27 | let fromIndex = getIndex(forViewController: fromVC), 28 | let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), 29 | let toView = toVC.view, 30 | let toIndex = getIndex(forViewController: toVC) 31 | else { 32 | transitionContext.completeTransition(false) 33 | return 34 | } 35 | 36 | let frame = transitionContext.initialFrame(for: fromVC) 37 | var fromFrameEnd = frame 38 | var toFrameStart = frame 39 | fromFrameEnd.origin.x = toIndex > fromIndex ? frame.origin.x - frame.width : frame.origin.x + frame.width 40 | toFrameStart.origin.x = toIndex > fromIndex ? frame.origin.x + frame.width : frame.origin.x - frame.width 41 | toView.frame = toFrameStart 42 | 43 | DispatchQueue.main.async { 44 | transitionContext.containerView.addSubview(toView) 45 | UIView.animate(withDuration: self.transitionDuration, animations: { 46 | fromView.frame = fromFrameEnd 47 | toView.frame = frame 48 | }, completion: {success in 49 | fromView.removeFromSuperview() 50 | transitionContext.completeTransition(success) 51 | }) 52 | } 53 | } 54 | 55 | private func getIndex(forViewController viewController: UIViewController) -> Int? { 56 | guard let viewControllers = self.viewControllers else { return nil } 57 | for (index, thisVC) in viewControllers.enumerated() where thisVC == viewController { 58 | return index 59 | } 60 | return nil 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Pico/Common/View/CommonButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonButton.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import UIKit 9 | 10 | final class CommonButton: UIButton { 11 | override init(frame: CGRect) { 12 | super.init(frame: frame) 13 | 14 | self.titleLabel?.font = .picoButtonFont 15 | self.setTitleColor(.white, for: .normal) 16 | self.backgroundColor = .picoBlue 17 | self.layer.cornerRadius = 10 18 | self.clipsToBounds = true 19 | } 20 | 21 | @available(*, unavailable) 22 | required init?(coder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Pico/Common/View/CustomIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomIndicator.swift 3 | // Pico 4 | // 5 | // Created by 방유빈 on 2023/10/13. 6 | // 7 | 8 | import Foundation 9 | import Kingfisher 10 | 11 | /* 12 | 사용법 13 | 킹피셔로 이미지 설정할때 인디케이터 타입 앞에 설정해주기 14 | //cycleSize 기본값 large 15 | userImageView.kf.indicatorType = .custom(indicator: CustomIndicator(cycleSize: .small)) 16 | userImageView.kf.setImage(with: url) 17 | */ 18 | /// 킹피셔 커스텀 인디케이터 19 | final class CustomIndicator: Indicator { 20 | func startAnimatingView() { 21 | animationView.animate() 22 | view.isHidden = false 23 | } 24 | 25 | func stopAnimatingView() { 26 | view.isHidden = true 27 | } 28 | 29 | var cycleSize: LoadingAnimationView.CircleSize? 30 | var animationView: LoadingAnimationView 31 | var view: Kingfisher.IndicatorView { 32 | return animationView 33 | } 34 | 35 | init(cycleSize: LoadingAnimationView.CircleSize? = nil) { 36 | animationView = LoadingAnimationView(circleSize: cycleSize ?? .large) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Pico/Common/View/FooterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FooterView.swift 3 | // Pico 4 | // 5 | // Created by 방유빈 on 2023/10/16. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | final class FooterView: UIView { 12 | let indicatorView: UIActivityIndicatorView = { 13 | let indicator = UIActivityIndicatorView(style: .large) 14 | indicator.color = .picoBlue 15 | return indicator 16 | }() 17 | 18 | override init(frame: CGRect) { 19 | super.init(frame: frame) 20 | 21 | makeConstraints() 22 | indicatorView.startAnimating() 23 | } 24 | 25 | private func makeConstraints() { 26 | addSubview(indicatorView) 27 | indicatorView.snp.makeConstraints { make in 28 | make.edges.equalToSuperview() 29 | } 30 | } 31 | 32 | @available(*, unavailable) 33 | required init?(coder: NSCoder) { 34 | fatalError() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Pico/Common/View/Loading.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Loading.swift 3 | // Pico 4 | // 5 | // Created by 방유빈 on 2023/10/04. 6 | // 7 | 8 | import UIKit 9 | 10 | final class Loading { 11 | static func showLoading(title: String = "", backgroundColor: UIColor = .black.withAlphaComponent(0.7)) { 12 | DispatchQueue.main.async { 13 | if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, 14 | let window = windowScene.windows.last { 15 | let loadingView: LoadingAnimationView 16 | if let existedView = window.subviews.first(where: { 17 | $0 is LoadingAnimationView 18 | }) as? LoadingAnimationView { 19 | loadingView = existedView 20 | } else { 21 | loadingView = LoadingAnimationView(title: title) 22 | loadingView.frame = window.frame 23 | loadingView.configBackgroundColor(color: backgroundColor) 24 | window.addSubview(loadingView) 25 | } 26 | loadingView.animate() 27 | } 28 | } 29 | } 30 | 31 | static func hideLoading() { 32 | DispatchQueue.main.async { 33 | if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, 34 | let window = windowScene.windows.last { 35 | window.subviews.filter({ $0 is LoadingAnimationView }).forEach { $0.removeFromSuperview() } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Pico/Common/View/MBTILabelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MBTILabelView.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | /* 12 | 사용법 13 | private let labelView: MBTILabelView = MBTILabelView(mbti: .entp) 14 | 15 | view.addSubview(labelView) 16 | 17 | labelView.snp.makeConstraints { make in 18 | // top, leading 제약조건 추가 19 | make.height.equalTo(labelView.frame.size.height) 20 | make.width.equalTo(labelView.frame.size.width) 21 | } 22 | */ 23 | 24 | final class MBTILabelView: UIView { 25 | enum LabelScale { 26 | case large 27 | case small 28 | 29 | var frameSize: CGRect { 30 | switch self { 31 | case .large: 32 | return CGRect(x: 0, y: 0, width: 80, height: 34) 33 | case .small: 34 | return CGRect(x: 0, y: 0, width: 50, height: 20) 35 | } 36 | } 37 | 38 | var font: UIFont { 39 | switch self { 40 | case .large: 41 | return .picoMBTILabelFont 42 | case .small: 43 | return .picoMBTISmallLabelFont 44 | } 45 | } 46 | 47 | var radius: CGFloat { 48 | switch self { 49 | case .large: 50 | return 8 51 | case .small: 52 | return 5 53 | } 54 | } 55 | } 56 | 57 | private let textLabel: UILabel = { 58 | let label = UILabel() 59 | label.textColor = .white 60 | return label 61 | }() 62 | 63 | private var mbti: MBTIType? 64 | private var labelScale: LabelScale 65 | 66 | init(mbti: MBTIType?, scale: LabelScale) { 67 | self.mbti = mbti 68 | self.labelScale = scale 69 | super.init(frame: labelScale.frameSize) 70 | configUI() 71 | addViews() 72 | makeConstraints() 73 | } 74 | 75 | @available(*, unavailable) 76 | required init?(coder: NSCoder) { 77 | fatalError("init(coder:) has not been implemented") 78 | } 79 | 80 | func setMbti(mbti: MBTIType?) { 81 | guard let mbti else { return } 82 | self.mbti = mbti 83 | configUI() 84 | } 85 | 86 | private func configUI() { 87 | guard let mbti else { return } 88 | textLabel.text = mbti.nameString 89 | textLabel.font = labelScale.font 90 | backgroundColor = UIColor(hex: mbti.colorName) 91 | layer.cornerRadius = labelScale.radius 92 | clipsToBounds = true 93 | } 94 | 95 | private func addViews() { 96 | self.addSubview(textLabel) 97 | } 98 | 99 | private func makeConstraints() { 100 | textLabel.snp.makeConstraints { make in 101 | make.centerX.centerY.equalToSuperview() 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Pico/Common/View/PaddingLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaddingLabel.swift 3 | // Pico 4 | // 5 | // Created by 신희권 on 10/20/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class PaddingLabel: UILabel { 11 | 12 | var topPadding: CGFloat = 0.0 13 | var leftPadding: CGFloat = 0.0 14 | var bottomPadding: CGFloat = 0.0 15 | var rightPadding: CGFloat = 0.0 16 | 17 | convenience init(padding: UIEdgeInsets) { 18 | self.init() 19 | self.topPadding = padding.top 20 | self.leftPadding = padding.left 21 | self.bottomPadding = padding.bottom 22 | self.rightPadding = padding.right 23 | } 24 | 25 | override func drawText(in rect: CGRect) { 26 | let padding = UIEdgeInsets.init(top: topPadding, left: leftPadding, bottom: bottomPadding, right: rightPadding) 27 | super.drawText(in: rect.inset(by: padding)) 28 | } 29 | 30 | override var intrinsicContentSize: CGSize { 31 | var contentSize = super.intrinsicContentSize 32 | contentSize.width += self.leftPadding + self.rightPadding 33 | contentSize.height += self.topPadding + self.bottomPadding 34 | return contentSize 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Pico/Config/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 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 127.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/AppIcon.appiconset/Group 127.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/AppIcon.appiconset/Group 127.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/AppIcon/AppIcon_gray.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 124 (2).png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/AppIcon/AppIcon_gray.imageset/Group 124 (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/AppIcon/AppIcon_gray.imageset/Group 124 (2).png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/AppIcon/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/Background/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/Background/background.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "background.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/Background/background.imageset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/Background/background.imageset/background.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/Logo/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/Logo/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pico (2).png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/Logo/logo.imageset/pico (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/Logo/logo.imageset/pico (2).png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/Logo/logo_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo_black.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/Logo/logo_black.imageset/logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/Logo/logo_black.imageset/logo_black.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/Logo/logo_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pico (7).png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/Logo/logo_white.imageset/pico (7).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/Logo/logo_white.imageset/pico (7).png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/detail/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/detail/religion.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "reilgion.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/detail/religion.imageset/reilgion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/detail/religion.imageset/reilgion.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/detail/smoke.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "smoke.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/detail/smoke.imageset/smoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/detail/smoke.imageset/smoke.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/game/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/game/banner.imageset/'Pico (10)..png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/game/banner.imageset/'Pico (10)..png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/game/banner.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "'Pico (10)..png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/game/gameBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 141.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/game/gameBackground.imageset/Group 141.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/game/gameBackground.imageset/Group 141.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/game/gameMusic.dataset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "data" : [ 3 | { 4 | "filename" : "gameMusic.mp3", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/game/gameMusic.dataset/gameMusic.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/game/gameMusic.dataset/gameMusic.mp3 -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/game/vsImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "vsImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/game/vsImage.imageset/vsImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/game/vsImage.imageset/vsImage.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/chu.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "chu.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/chu.imageset/chu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/chu.imageset/chu.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/detectiveChu.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Image.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/detectiveChu.imageset/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/detectiveChu.imageset/Image.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/detectiveChu2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Image.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/detectiveChu2.imageset/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/detectiveChu2.imageset/Image.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/guideGesture.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Image.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/guideGesture.imageset/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/guideGesture.imageset/Image.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/guideTab.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Image.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/guideTab.imageset/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/guideTab.imageset/Image.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/infp.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "infp.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/infp.imageset/infp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/infp.imageset/infp.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/locationPointImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "locationPointImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/locationPointImage.imageset/locationPointImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/locationPointImage.imageset/locationPointImage.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/magnifier.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "search_1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/magnifier.imageset/search_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/magnifier.imageset/search_1.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/mbtiImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mbtiImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/mbtiImage.imageset/mbtiImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/mbtiImage.imageset/mbtiImage.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/myChat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "sendMessage1.png", 5 | "idiom" : "universal", 6 | "resizing" : { 7 | "cap-insets" : { 8 | "bottom" : 2, 9 | "left" : 0, 10 | "right" : 7, 11 | "top" : 5 12 | }, 13 | "center" : { 14 | "height" : 1, 15 | "mode" : "tile", 16 | "width" : 1 17 | }, 18 | "mode" : "9-part" 19 | }, 20 | "scale" : "1x" 21 | }, 22 | { 23 | "filename" : "sendMessage2.png", 24 | "idiom" : "universal", 25 | "resizing" : { 26 | "cap-insets" : { 27 | "bottom" : 3, 28 | "left" : 2, 29 | "right" : 15, 30 | "top" : 10 31 | }, 32 | "center" : { 33 | "height" : 1, 34 | "mode" : "tile", 35 | "width" : 1 36 | }, 37 | "mode" : "9-part" 38 | }, 39 | "scale" : "2x" 40 | }, 41 | { 42 | "filename" : "sendMessage3.png", 43 | "idiom" : "universal", 44 | "resizing" : { 45 | "cap-insets" : { 46 | "bottom" : 4, 47 | "left" : 4, 48 | "right" : 23, 49 | "top" : 15 50 | }, 51 | "center" : { 52 | "height" : 1, 53 | "mode" : "tile", 54 | "width" : 1 55 | }, 56 | "mode" : "9-part" 57 | }, 58 | "scale" : "3x" 59 | } 60 | ], 61 | "info" : { 62 | "author" : "xcode", 63 | "version" : 1 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/myChat.imageset/sendMessage1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/myChat.imageset/sendMessage1.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/myChat.imageset/sendMessage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/myChat.imageset/sendMessage2.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/myChat.imageset/sendMessage3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/myChat.imageset/sendMessage3.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/pageStick.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Image.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/pageStick.imageset/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/pageStick.imageset/Image.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/pencilImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pencilImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/pencilImage.imageset/pencilImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/pencilImage.imageset/pencilImage.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/premiumImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "premiumImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/premiumImage.imageset/premiumImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/premiumImage.imageset/premiumImage.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/randomBoxImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "randomBoxImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/randomBoxImage.imageset/randomBoxImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/randomBoxImage.imageset/randomBoxImage.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/tempImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tempImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/tempImage.imageset/tempImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/tempImage.imageset/tempImage.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/yourChat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "receive1.png", 5 | "idiom" : "universal", 6 | "resizing" : { 7 | "cap-insets" : { 8 | "bottom" : 2, 9 | "left" : 6, 10 | "right" : 1, 11 | "top" : 5 12 | }, 13 | "center" : { 14 | "height" : 1, 15 | "mode" : "tile", 16 | "width" : 1 17 | }, 18 | "mode" : "9-part" 19 | }, 20 | "scale" : "1x" 21 | }, 22 | { 23 | "filename" : "receive2.png", 24 | "idiom" : "universal", 25 | "resizing" : { 26 | "cap-insets" : { 27 | "bottom" : 3, 28 | "left" : 13, 29 | "right" : 2, 30 | "top" : 10 31 | }, 32 | "center" : { 33 | "height" : 1, 34 | "mode" : "tile", 35 | "width" : 1 36 | }, 37 | "mode" : "9-part" 38 | }, 39 | "scale" : "2x" 40 | }, 41 | { 42 | "filename" : "receive3.png", 43 | "idiom" : "universal", 44 | "resizing" : { 45 | "cap-insets" : { 46 | "bottom" : 4, 47 | "left" : 23, 48 | "right" : 4, 49 | "top" : 15 50 | }, 51 | "center" : { 52 | "height" : 1, 53 | "mode" : "tile", 54 | "width" : 1 55 | }, 56 | "mode" : "9-part" 57 | }, 58 | "scale" : "3x" 59 | } 60 | ], 61 | "info" : { 62 | "author" : "xcode", 63 | "version" : 1 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/yourChat.imageset/receive1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/yourChat.imageset/receive1.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/yourChat.imageset/receive2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/yourChat.imageset/receive2.png -------------------------------------------------------------------------------- /Pico/Config/Assets.xcassets/images/yourChat.imageset/receive3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/Pico/Config/Assets.xcassets/images/yourChat.imageset/receive3.png -------------------------------------------------------------------------------- /Pico/Config/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FirebaseAppDelegateProxyEnabled 6 | 7 | NSSupportsSuddenTermination 8 | 9 | UIApplicationSceneManifest 10 | 11 | UIApplicationSupportsMultipleScenes 12 | 13 | UISceneConfigurations 14 | 15 | UIWindowSceneSessionRoleApplication 16 | 17 | 18 | UISceneConfigurationName 19 | Default Configuration 20 | UISceneDelegateClassName 21 | $(PRODUCT_MODULE_NAME).SceneDelegate 22 | 23 | 24 | 25 | 26 | UIBackgroundModes 27 | 28 | fetch 29 | remote-notification 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Pico/Config/ko.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Pico/Ent/Cell/WorldCupUserInfoStackView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorldCupUserInfoStackView.swift 3 | // Pico 4 | // 5 | // Created by 오영석 on 2023/09/26. 6 | // 7 | import UIKit 8 | import SnapKit 9 | 10 | final class WorldCupUserInfoStackView: UIView { 11 | 12 | private let labelTexts = ["키", "직업", "지역"] 13 | 14 | private lazy var labels: [UILabel] = labelTexts.map { text in 15 | let label = UILabel() 16 | label.text = text 17 | label.textAlignment = .left 18 | label.textColor = UIColor.picoFontGray.withAlphaComponent(0.5) 19 | label.font = UIFont.picoDescriptionFont 20 | return label 21 | } 22 | 23 | private lazy var dataLabels: [UILabel] = (0..<3).map { _ in 24 | let label = UILabel() 25 | label.textAlignment = .right 26 | label.textColor = UIColor.picoBlue 27 | label.font = UIFont.picoEntSubLabelFont 28 | label.numberOfLines = 0 29 | label.setContentHuggingPriority(.required, for: .horizontal) 30 | label.translatesAutoresizingMaskIntoConstraints = false 31 | label.heightAnchor.constraint(lessThanOrEqualToConstant: 60).isActive = true 32 | return label 33 | } 34 | 35 | private lazy var labelStackViews: [UIStackView] = { 36 | return (0..<3).map { index in 37 | let stackView = UIStackView(arrangedSubviews: [labels[index], dataLabels[index]]) 38 | stackView.axis = .horizontal 39 | stackView.spacing = 24 40 | return stackView 41 | } 42 | }() 43 | 44 | private lazy var dataStackView: UIStackView = { 45 | let stackView = UIStackView(arrangedSubviews: labelStackViews) 46 | stackView.axis = .vertical 47 | stackView.spacing = 16 48 | return stackView 49 | }() 50 | 51 | override init(frame: CGRect) { 52 | super.init(frame: frame) 53 | self.backgroundColor = .white 54 | addViews() 55 | makeConstraints() 56 | } 57 | 58 | @available(*, unavailable) 59 | required init?(coder: NSCoder) { 60 | fatalError("init(coder:) has not been implemented") 61 | } 62 | 63 | private func addViews() { 64 | addSubview(dataStackView) 65 | } 66 | 67 | private func makeConstraints() { 68 | let padding: CGFloat = 10 69 | 70 | dataStackView.snp.makeConstraints { make in 71 | make.top.equalToSuperview().offset(padding) 72 | make.leading.equalToSuperview().offset(padding) 73 | make.trailing.equalToSuperview().offset(-padding) 74 | make.bottom.equalToSuperview().offset(-padding) 75 | } 76 | } 77 | 78 | func setDataLabelTexts(_ texts: [String]) { 79 | guard texts.count == dataLabels.count else { 80 | return 81 | } 82 | 83 | for (index, text) in texts.enumerated() { 84 | dataLabels[index].text = text 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Pico/Ent/ViewModel/WorldCupResultViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorldCupResultViewModel.swift 3 | // Pico 4 | // 5 | // Created by 방유빈 on 2023/10/19. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | import FirebaseFirestore 11 | 12 | class WorldCupResultViewModel: ViewModelType { 13 | private let dbRef = Firestore.firestore() 14 | private let currentUser = UserDefaultsManager.shared.getUserData() 15 | private var currentChuCount = UserDefaultsManager.shared.getChuCount() 16 | 17 | struct Input { 18 | let requestMessage: Observable 19 | } 20 | 21 | struct Output { 22 | let resultRequestMessage: Observable 23 | } 24 | 25 | func transform(input: Input) -> Output { 26 | let resultRequestMessage = input.requestMessage 27 | .withUnretained(self) 28 | .flatMap { viewModel, resultUser -> Observable in 29 | Loading.showLoading() 30 | return viewModel.requestMessage(resultUser: resultUser) 31 | } 32 | .withUnretained(self) 33 | .flatMap { viewModel, _ -> Observable in 34 | viewModel.currentChuCount = UserDefaultsManager.shared.getChuCount() - 25 35 | return FirestoreService.shared.updateDocumentRx(collectionId: .users, documentId: viewModel.currentUser.userId, field: "chuCount", data: viewModel.currentChuCount) 36 | .flatMap { _ -> Observable in 37 | let payment: Payment.PaymentInfo = Payment.PaymentInfo(price: 0, purchaseChuCount: -25, paymentType: .worldCup) 38 | return FirestoreService.shared.saveDocumentRx(collectionId: .payment, documentId: viewModel.currentUser.userId, fieldId: "paymentInfos", data: payment) 39 | } 40 | } 41 | .withUnretained(self) 42 | .map { viewModel, _ in 43 | UserDefaultsManager.shared.updateChuCount(viewModel.currentChuCount) 44 | return DispatchQueue.main.async { 45 | Loading.hideLoading() 46 | } 47 | } 48 | 49 | return Output(resultRequestMessage: resultRequestMessage) 50 | } 51 | 52 | private func requestMessage(resultUser: User) -> Observable { 53 | return Observable.create { [weak self] emitter in 54 | guard let self = self else { return Disposables.create() } 55 | let newMail = Mail.MailInfo(sendedUserId: resultUser.id, receivedUserId: currentUser.userId, mailType: .receive, message: "월드컵 우승자에게 메일을 보내보세요!", sendedDate: Date().timeIntervalSince1970, isReading: false) 56 | dbRef.collection(Collections.mail.name).document(currentUser.userId).setData( 57 | [ 58 | "userId": currentUser.userId, 59 | "receiveMailInfo": FieldValue.arrayUnion([newMail.asDictionary()]) 60 | ], merge: true) { error in 61 | if let error = error { 62 | print("평가 업데이트 에러: \(error)") 63 | emitter.onError(error) 64 | } else { 65 | print("평가 업데이트 성공") 66 | emitter.onNext(()) 67 | } 68 | } 69 | return Disposables.create() 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Pico/Extension/Data/Array+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 10/9/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Array { 11 | subscript (safe index: Int) -> Element? { 12 | return indices ~= index ? self[index] : nil 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Pico/Extension/Data/Bundle+Extenstions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Extenstions.swift 3 | // Pico 4 | // 5 | // Created by 방유빈 on 2023/10/18. 6 | // 7 | 8 | import Foundation 9 | 10 | enum APIKey: String { 11 | case FirebaseAPIKeys 12 | case AuthKeys 13 | 14 | var name: String { 15 | return self.rawValue 16 | } 17 | } 18 | 19 | extension Bundle { 20 | var notificationKey: String { 21 | guard let file = self.path(forResource: APIKey.FirebaseAPIKeys.name, ofType: "plist") else { return "" } 22 | 23 | guard let resource = NSDictionary(contentsOfFile: file) else { return "" } 24 | 25 | guard let key = resource["NOTIFICATION_KEY"] as? String else { 26 | fatalError("KEY를 찾을수없음") 27 | } 28 | return key 29 | } 30 | 31 | var testId: String { 32 | guard let file = self.path(forResource: APIKey.FirebaseAPIKeys.name, ofType: "plist") else { return "" } 33 | 34 | guard let resource = NSDictionary(contentsOfFile: file) else { return "" } 35 | 36 | guard let key = resource["TEST_ID"] as? String else { 37 | fatalError("KEY를 찾을수없음") 38 | } 39 | return key 40 | } 41 | 42 | var testAuthNum: String { 43 | guard let file = self.path(forResource: APIKey.AuthKeys.name, ofType: "plist") else { return "" } 44 | 45 | guard let resource = NSDictionary(contentsOfFile: file) else { return "" } 46 | 47 | guard let key = resource["TEST_AUTH_NUM"] as? String else { 48 | fatalError("KEY를 찾을수없음") 49 | } 50 | return key 51 | } 52 | 53 | var testPhoneNumber: String { 54 | guard let file = self.path(forResource: APIKey.AuthKeys.name, ofType: "plist") else { return "" } 55 | 56 | guard let resource = NSDictionary(contentsOfFile: file) else { return "" } 57 | 58 | guard let key = resource["TEST_PHONE_NUMBER"] as? String else { 59 | fatalError("KEY를 찾을수없음") 60 | } 61 | return key 62 | } 63 | 64 | var kakaoAppKey: String { 65 | guard let file = self.path(forResource: APIKey.AuthKeys.name, ofType: "plist") else { return "" } 66 | 67 | guard let resource = NSDictionary(contentsOfFile: file) else { return "" } 68 | 69 | guard let key = resource["KAKAO_APP_KEY"] as? String else { 70 | fatalError("KEY를 찾을수없음") 71 | } 72 | return key 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Pico/Extension/Data/Double+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 10/6/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Double { 11 | enum DateSeparator { 12 | case dash 13 | case dot 14 | } 15 | 16 | /// Double -> yyyy-mm-dd 으로 변환 17 | func toString(dateSeparator: DateSeparator = .dash) -> String { 18 | let date = Date(timeIntervalSince1970: self) 19 | 20 | let dateFormatter = DateFormatter() 21 | switch dateSeparator { 22 | case .dash: 23 | dateFormatter.dateFormat = "yyyy-MM-dd" 24 | case .dot: 25 | dateFormatter.dateFormat = "yyyy.MM.dd" 26 | } 27 | 28 | let formattedDate = dateFormatter.string(from: date) 29 | return formattedDate 30 | } 31 | 32 | func toStringTime(dateSeparator: DateSeparator = .dash) -> String { 33 | let date = Date(timeIntervalSince1970: self) 34 | 35 | let dateFormatter = DateFormatter() 36 | switch dateSeparator { 37 | case .dash: 38 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm" 39 | case .dot: 40 | dateFormatter.dateFormat = "yyyy.MM.dd HH:mm" 41 | } 42 | 43 | let formattedDate = dateFormatter.string(from: date) 44 | return formattedDate 45 | } 46 | 47 | func timeAgoSinceDate() -> String { 48 | let date = Date(timeIntervalSince1970: self) 49 | let currentDate = Date() 50 | 51 | let seconds: Int = Int(currentDate.timeIntervalSince(date)) 52 | if seconds < 60 { 53 | return "\(seconds)초 전" 54 | } 55 | 56 | let minutes = seconds / 60 57 | if minutes < 60 { 58 | return "\(minutes)분 전" 59 | } 60 | 61 | let hour = minutes / 60 62 | if hour < 24 { 63 | return "\(hour)시간 전" 64 | } 65 | 66 | let day = hour / 24 67 | if day < 30 { 68 | return "\(day)일 전" 69 | } 70 | 71 | let formatter = DateFormatter() 72 | formatter.dateFormat = "MM월 dd일" 73 | let result = formatter.string(from: date) 74 | 75 | return result 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Pico/Extension/Data/Encodable+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Encodable+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Encodable { 11 | func asDictionary() -> [String: Any] { 12 | guard let data = try? JSONEncoder().encode(self) else { 13 | return [:] 14 | } 15 | 16 | do { 17 | let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] 18 | return json ?? [:] 19 | } catch { 20 | return [:] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Pico/Extension/Data/Int+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 10/17/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Int { 11 | func formattedSeparator() -> String { 12 | let formatter = NumberFormatter() 13 | formatter.numberStyle = .decimal 14 | return formatter.string(from: NSNumber(value: self)) ?? "" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Pico/Extension/Data/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 방유빈 on 2023/10/04. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | /// String -> Date 바꾸기 12 | func toDate() -> Date { 13 | let dateFormatter = DateFormatter() 14 | dateFormatter.dateFormat = "yyyy-MM-dd" 15 | dateFormatter.timeZone = TimeZone(identifier: "UTC") 16 | return dateFormatter.date(from: self) ?? Date() 17 | } 18 | 19 | /// 전화번호 3자리 7자리에 "-" 넣기 20 | func formattedTextFieldText() -> String { 21 | var formattedText: String = "" 22 | 23 | for (index, character) in self.enumerated() { 24 | if index == 3 || index == 7 { 25 | formattedText += "-" 26 | } 27 | formattedText.append(character) 28 | } 29 | 30 | return formattedText 31 | } 32 | 33 | /// 좌우 공백자르기 34 | func trimmed() -> String { 35 | return self.trimmingCharacters(in: .whitespacesAndNewlines) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Pico/Extension/Namespace/UIColor+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIColor { 11 | convenience init(hex: String, alpha: Double = 1.0) { 12 | var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) 13 | hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") 14 | 15 | var rgb: UInt64 = 0 16 | Scanner(string: hexSanitized).scanHexInt64(&rgb) 17 | 18 | let red = Double((rgb & 0xFF0000) >> 16) / 255.0 19 | let green = Double((rgb & 0x00FF00) >> 8) / 255.0 20 | let blue = Double(rgb & 0x0000FF) / 255.0 21 | 22 | self.init(red: red, green: green, blue: blue, alpha: alpha) 23 | } 24 | 25 | /// 메인 블루 색상 26 | static let picoBlue: UIColor = UIColor(hex: "#727EED") 27 | 28 | /// 투명도 70% 적용된 메인 블루 색상 29 | static let picoAlphaBlue: UIColor = UIColor(hex: "#727EED", alpha: 0.7) 30 | 31 | /// 투명도 24% 적용된 메인 블루 색상 32 | static let picoBetaBlue: UIColor = UIColor(hex: "#727EED", alpha: 0.24) 33 | 34 | /// 메인 그레이 색상 35 | static let picoGray: UIColor = UIColor(hex: "#E3E3E3") 36 | 37 | /// 투명도 80% 적용된 화이트 색상 38 | static let picoAlphaWhite: UIColor = .white.withAlphaComponent(0.8) 39 | 40 | /// 테이블뷰 백그라운드 색상 41 | static let picoLightGray: UIColor = UIColor(hex: "#F1F1F1") 42 | 43 | /// 그라이데이션 중간 색상 44 | static let picoGradientMedium: UIColor = UIColor(hex: "#A07BEE") 45 | 46 | /// 그라이데이션 중간2 색상 47 | static let picoGradientMedium2: UIColor = UIColor(hex: "#B58FF4") 48 | 49 | /// 그라이데이션 마지막 색상 50 | static let picoGradientLast: UIColor = UIColor(hex: "#DFB7FF") 51 | 52 | /// 폰트 블랙 색상 53 | static let picoFontBlack: UIColor = .label 54 | 55 | /// 폰트 그레이 색상 56 | static let picoFontGray: UIColor = .secondaryLabel 57 | 58 | /// 별 옐로우 색상 59 | static let picoStarYellow: UIColor = UIColor(hex: "#FFEB84") 60 | } 61 | -------------------------------------------------------------------------------- /Pico/Extension/Namespace/UIFont+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIFont+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIFont { 11 | /// 제목 폰트사이즈 (22, bold) 12 | static var picoTitleFont: UIFont { 13 | return UIFont.systemFont(ofSize: 22, weight: .bold) 14 | } 15 | /// 제목 폰트 큰사이즈 (25, bold) 16 | static var picoLargeTitleFont: UIFont { 17 | return UIFont.systemFont(ofSize: 25, weight: .bold) 18 | } 19 | /// 부제목 폰트사이즈 (18, semibold) 20 | static var picoSubTitleFont: UIFont { 21 | return UIFont.systemFont(ofSize: 18, weight: .semibold) 22 | } 23 | /// 부제목 폰트사이즈 (16, semibold) 24 | static var picoSubTitleSmallFont: UIFont { 25 | return UIFont.systemFont(ofSize: 16, weight: .semibold) 26 | } 27 | /// 제목에 대한 설명 폰트사이즈 (13, regular) 28 | static var picoDescriptionFont: UIFont { 29 | return UIFont.systemFont(ofSize: 13) 30 | } 31 | /// 제목에 대한 설명 폰트사이즈 (13, semibold) 32 | static var picoDescriptionFont2: UIFont { 33 | return UIFont.systemFont(ofSize: 13, weight: .semibold) 34 | } 35 | /// 내용 폰트 사이즈 (15, regular) 36 | static var picoContentFont: UIFont { 37 | return UIFont.systemFont(ofSize: 15) 38 | } 39 | /// 내용 굵은 폰트 사이즈 (15, semibold) 40 | static var picoContentBoldFont: UIFont { 41 | return UIFont.systemFont(ofSize: 15, weight: .semibold) 42 | } 43 | /// 버튼 폰트 사이즈 (15, bold) 44 | static var picoButtonFont: UIFont { 45 | return UIFont.systemFont(ofSize: 15, weight: .bold) 46 | } 47 | /// mbti 라벨 폰트 사이트 (22, bold) 48 | static var picoMBTILabelFont: UIFont { 49 | return UIFont.systemFont(ofSize: 22, weight: .bold) 50 | } 51 | /// mbti small 라벨 폰트 사이즈 (14, bold) 52 | static var picoMBTISmallLabelFont: UIFont { 53 | return UIFont.systemFont(ofSize: 14, weight: .bold) 54 | } 55 | /// 로그인 뷰 MBTI 라벨 폰트 사이트 (40, medium) 56 | static var picoMBTISelectedLabelFont: UIFont { 57 | return UIFont.systemFont(ofSize: 40, weight: .medium) 58 | } 59 | /// 로그인 뷰 MBTI 설명 라벨 폰트 사이트 (13, thin) 60 | static var picoMBTISelectedSubLabelFont: UIFont { 61 | return UIFont.systemFont(ofSize: 13, weight: .thin) 62 | } 63 | /// 엔터 뷰 얇은 폰트 사이트 (12, thin) 64 | static var picoEntSubLabelFont: UIFont { 65 | return UIFont.systemFont(ofSize: 12, weight: .thin) 66 | } 67 | /// 프로필 라벨 폰트 사이즈 (14, regular) 68 | static var picoProfileLabelFont: UIFont { 69 | return UIFont.systemFont(ofSize: 14) 70 | } 71 | /// 프로필 이름 폰트 사이즈 (25, medium) 72 | static var picoProfileNameFont: UIFont { 73 | return UIFont.systemFont(ofSize: 25, weight: .medium) 74 | } 75 | /// 게임 제목 폰트 사이즈 (40, semibold) 76 | static var picoGameTitleFont: UIFont { 77 | return UIFont.systemFont(ofSize: 40, weight: .semibold) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Pico/Extension/UI/DismissKeyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | /// 기본 배경색 설정 12 | func configBgColor() { 13 | self.backgroundColor = .systemBackground 14 | } 15 | 16 | /// 배경 탭하면 키보드 내리기 17 | func tappedDismissKeyboard() { 18 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) 19 | tapGesture.cancelsTouchesInView = false 20 | self.addGestureRecognizer(tapGesture) 21 | } 22 | 23 | @objc private func dismissKeyboard() { 24 | self.endEditing(true) 25 | } 26 | 27 | /// 원형 이미지 만들기 28 | /// -> viewDidLayoutSubviews에서 호출 29 | func setCircleImageView(imageView: UIImageView, border: CGFloat = 0, borderColor: CGColor = UIColor.clear.cgColor) { 30 | imageView.layer.cornerRadius = imageView.frame.width / 2.0 31 | imageView.layer.borderWidth = border 32 | imageView.layer.borderColor = borderColor 33 | imageView.clipsToBounds = true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Pico/Extension/UI/TrickTextField+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrickTextField+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 오영석 on 12/15/23. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | private var AssociatedObjectHandle = 0 12 | 13 | extension UIView { 14 | private var secureTextField: UITextField { 15 | if let textField = objc_getAssociatedObject(self, &AssociatedObjectHandle) as? UITextField { 16 | return textField 17 | } else { 18 | let textField = UITextField() 19 | addSubview(textField) 20 | textField.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 21 | textField.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 22 | textField.layer.removeFromSuperlayer() 23 | layer.superlayer?.insertSublayer(textField.layer, at: 0) 24 | textField.layer.sublayers?.last?.addSublayer(layer) 25 | objc_setAssociatedObject(self, &AssociatedObjectHandle, textField, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) 26 | return textField 27 | } 28 | } 29 | 30 | func secureMode(enable: Bool) { 31 | secureTextField.isSecureTextEntry = enable 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Pico/Extension/UI/UIButton+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIButton { 11 | /// 버튼 눌렸을 때 애니메이션 12 | func tappedAnimation() { 13 | UIView.animate(withDuration: 0.25, animations: { 14 | self.transform = CGAffineTransform(scaleX: 0.98, y: 0.98) 15 | }, completion: { _ in 16 | UIView.animate(withDuration: 0.255, animations: { 17 | self.transform = CGAffineTransform.identity 18 | }) 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Pico/Extension/UI/UICollectionView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 방유빈 on 2023/10/10. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UICollectionReusableView: Reusable { } 11 | 12 | extension UICollectionView { 13 | func cellForItem(atIndexPath indexPath: IndexPath) -> T { 14 | guard 15 | let cell = cellForItem(at: indexPath) as? T 16 | else { 17 | fatalError("Could not cellForItemAt at indexPath: \(T.reuseIdentifier)") 18 | } 19 | return cell 20 | } 21 | 22 | func dequeueReusableCell(forIndexPath indexPath: IndexPath, cellType: T.Type = T.self) -> T { 23 | guard let cell = dequeueReusableCell(withReuseIdentifier: T.reuseIdentifier, for: indexPath) as? T else { 24 | fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") 25 | } 26 | return cell 27 | } 28 | 29 | func register(cell: T.Type) where T: UICollectionViewCell { 30 | register(cell, forCellWithReuseIdentifier: cell.reuseIdentifier) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Pico/Extension/UI/UIImage+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 10/9/23. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIImage { 11 | /// 이미지 비율에 맞춰서 가로 세로 높이 구하기 12 | func getRatio(height: CGFloat = 0, width: CGFloat = 0) -> CGFloat { 13 | let widthRatio = CGFloat(self.size.width / self.size.height) 14 | let heightRatio = CGFloat(self.size.height / self.size.width) 15 | 16 | if height != 0 { 17 | return height / heightRatio 18 | } 19 | if width != 0 { 20 | return width / widthRatio 21 | } 22 | return 0 23 | } 24 | 25 | // UIGraphicsBeginImageContextWithOptions 이거 없어질 친구니까 변경해야함 26 | func resized(toSize newSize: CGSize) -> UIImage { 27 | UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0) 28 | self.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)) 29 | let newImage = UIGraphicsGetImageFromCurrentImageContext() 30 | UIGraphicsEndImageContext() 31 | return newImage ?? self 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Pico/Extension/UI/UIImageView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 10/9/23. 6 | // 7 | 8 | import UIKit 9 | import RxSwift 10 | import RxCocoa 11 | 12 | extension UIImageView { 13 | /// 원형 이미지 만들기 14 | /// -> viewDidLayoutSubviews에서 호출 15 | func setCircleImageView(border: CGFloat = 0, borderColor: CGColor = UIColor.clear.cgColor) { 16 | self.layer.cornerRadius = self.frame.width / 2.0 17 | self.layer.borderWidth = border 18 | self.layer.borderColor = borderColor 19 | self.clipsToBounds = true 20 | } 21 | 22 | func showLoadingImageView() { 23 | DispatchQueue.main.async { 24 | let loadingView = LoadingAnimationView(circleSize: .small) 25 | loadingView.frame = self.bounds 26 | loadingView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 27 | self.addSubview(loadingView) 28 | loadingView.animate() 29 | } 30 | } 31 | 32 | func hideLoadingImageView() { 33 | DispatchQueue.main.async { 34 | for subview in self.subviews { 35 | if let loadingView = subview as? LoadingAnimationView { 36 | loadingView.removeFromSuperview() 37 | } 38 | } 39 | } 40 | } 41 | 42 | func load(url: URL) { 43 | showLoadingImageView() 44 | DispatchQueue.global().async { [weak self] in 45 | if let data = try? Data(contentsOf: url) { 46 | DispatchQueue.main.async { 47 | self?.image = UIImage(data: data) 48 | self?.hideLoadingImageView() 49 | } 50 | } else { 51 | DispatchQueue.main.async { 52 | self?.image = UIImage(named: "chu") 53 | self?.hideLoadingImageView() 54 | } 55 | } 56 | } 57 | } 58 | 59 | func loadImage(url: String, disposeBag: DisposeBag) { 60 | Observable.just(url) 61 | .flatMap(loadImageObservable) 62 | .observe(on: MainScheduler.instance) 63 | .bind(to: rx.image) 64 | .disposed(by: disposeBag) 65 | } 66 | 67 | private func loadImageObservable(url: String) -> Observable { 68 | return Observable.create { emitter in 69 | guard let url = URL(string: url) else { 70 | emitter.onNext(UIImage(named: "chu")) 71 | emitter.onCompleted() 72 | return Disposables.create() 73 | } 74 | 75 | let task = URLSession.shared.dataTask(with: url) { data, _, error in 76 | if let error = error { 77 | emitter.onError(error) 78 | return 79 | } 80 | guard let data = data, 81 | let image = UIImage(data: data) else { 82 | emitter.onNext(UIImage(named: "chu")) 83 | emitter.onCompleted() 84 | return 85 | } 86 | emitter.onNext(image) 87 | emitter.onCompleted() 88 | } 89 | task.resume() 90 | return Disposables.create() 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Pico/Extension/UI/UILabel+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UILabel+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 10/22/23. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UILabel { 11 | func setLineSpacing(spacing: CGFloat) { 12 | guard let text = text else { return } 13 | let attributeString = NSMutableAttributedString(string: text) 14 | let style = NSMutableParagraphStyle() 15 | style.lineSpacing = spacing 16 | attributeString.addAttribute(.paragraphStyle, 17 | value: style, 18 | range: NSRange(location: 0, length: attributeString.length)) 19 | attributedText = attributeString 20 | } 21 | 22 | var rotation: Int { 23 | get { 24 | return 0 25 | } set { 26 | let radians = ((CGFloat.pi) * CGFloat(newValue) / CGFloat(180.0)) 27 | self.transform = CGAffineTransform(rotationAngle: radians) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Pico/Extension/UI/UIStackView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStackView+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 양성혜 on 2023/10/14. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIStackView { 11 | /// [UIView] 배열을 stackView에 추가하기 12 | func addArrangedSubview(_ views: [UIView]) { 13 | views.forEach { item in 14 | self.addArrangedSubview(item) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Pico/Extension/UI/UITableView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 10/10/23. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol Reusable: AnyObject { 11 | static var reuseIdentifier: String { get } 12 | } 13 | 14 | extension Reusable { 15 | static var reuseIdentifier: String { 16 | return String(describing: self) 17 | } 18 | } 19 | 20 | extension UITableViewCell: Reusable { } 21 | 22 | extension UITableViewHeaderFooterView: Reusable { } 23 | 24 | extension UITableView { 25 | func cellForRow(atIndexPath indexPath: IndexPath) -> T { 26 | guard 27 | let cell = cellForRow(at: indexPath) as? T 28 | else { 29 | fatalError("Could not cellForItemAt at \(T.reuseIdentifier) cell") 30 | } 31 | return cell 32 | } 33 | 34 | func dequeueReusableCell(forIndexPath indexPath: IndexPath, cellType: T.Type = T.self) -> T { 35 | guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { 36 | fatalError("Fail to dequeue: \(T.reuseIdentifier) cell") 37 | } 38 | return cell 39 | } 40 | 41 | func dequeueReusableHeaderFooterView() -> T { 42 | guard let cell = dequeueReusableHeaderFooterView(withIdentifier: T.reuseIdentifier) as? T else { 43 | fatalError("Fail to dequeue: \(T.reuseIdentifier) cell") 44 | } 45 | return cell 46 | } 47 | 48 | func register(cell: T.Type) where T: UITableViewCell { 49 | register(cell, forCellReuseIdentifier: cell.reuseIdentifier) 50 | } 51 | 52 | func register(headerFooterView: T.Type, forCellReuseIdentifier reuseIdentifier: String = T.reuseIdentifier) where T: UITableViewHeaderFooterView { 53 | register(headerFooterView, forHeaderFooterViewReuseIdentifier: reuseIdentifier) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Pico/Extension/UI/UITextFieldDelegate+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITextFieldDelegate+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/29. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UITextFieldDelegate { 11 | func changePhoneNumDigits(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String, isFull: (Bool) -> ()) -> Bool { 12 | let currentText = textField.text ?? "" 13 | let updatedText = (currentText as NSString).replacingCharacters(in: range, with: string) 14 | let digits = CharacterSet.decimalDigits 15 | let filteredText = updatedText.components(separatedBy: digits.inverted).joined() 16 | 17 | if filteredText.count > 11 { 18 | return false 19 | } 20 | 21 | if filteredText.count <= 3 || (filteredText.count > 3 && filteredText.count <= 10) { 22 | textField.text = filteredText.formattedTextFieldText() 23 | isFull(false) 24 | return false 25 | } 26 | 27 | if filteredText.count == 11 { 28 | textField.text = filteredText.formattedTextFieldText() 29 | isFull(true) 30 | return false 31 | } 32 | 33 | return true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Pico/Extension/UI/UIView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Extensions.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | /// 기본 배경색 설정 12 | func configBackgroundColor(color: UIColor = .systemBackground) { 13 | self.backgroundColor = color 14 | } 15 | 16 | /// 배경 탭하면 키보드 내리기 17 | func tappedDismissKeyboard() { 18 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) 19 | tapGesture.cancelsTouchesInView = false 20 | self.addGestureRecognizer(tapGesture) 21 | } 22 | @objc private func dismissKeyboard() { 23 | self.endEditing(true) 24 | } 25 | 26 | /// [UIView] 배열을 view 에 추가하기 27 | func addSubview(_ views: [UIView]) { 28 | views.forEach { 29 | self.addSubview($0) 30 | } 31 | } 32 | } 33 | 34 | // MARK: - 그림자 관련 35 | extension UIView { 36 | enum VerticalLocation { 37 | case bottom 38 | case top 39 | case left 40 | case right 41 | } 42 | 43 | func addShadow(location: VerticalLocation, color: UIColor = .black, opacity: Float = 0.8, radius: CGFloat = 5.0) { 44 | switch location { 45 | case .bottom: 46 | addShadow(offset: CGSize(width: 0, height: 10), color: color, opacity: opacity, radius: radius) 47 | case .top: 48 | addShadow(offset: CGSize(width: 0, height: -10), color: color, opacity: opacity, radius: radius) 49 | case .left: 50 | addShadow(offset: CGSize(width: -10, height: 0), color: color, opacity: opacity, radius: radius) 51 | case .right: 52 | addShadow(offset: CGSize(width: 10, height: 0), color: color, opacity: opacity, radius: radius) 53 | } 54 | } 55 | 56 | func addShadow(offset: CGSize, color: UIColor = .gray, opacity: Float = 0.1, radius: CGFloat = 3.0) { 57 | self.layer.masksToBounds = false 58 | self.layer.shadowColor = color.cgColor 59 | self.layer.shadowOffset = offset 60 | self.layer.shadowOpacity = opacity 61 | self.layer.shadowRadius = radius 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Pico/Home/Cell/MBTILabelCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MBTILabelCollectionViewCell.swift 3 | // Pico 4 | // 5 | // Created by 임대진 on 2023/09/27. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MBTILabelCollectionViewCell: UICollectionViewCell { 11 | 12 | private let mbtiButton: UIButton = { 13 | let button = UIButton() 14 | button.setTitleColor(.white, for: .normal) 15 | return button 16 | }() 17 | 18 | override init(frame: CGRect) { 19 | super.init(frame: frame) 20 | configureUI() 21 | } 22 | @available(*, unavailable) 23 | required init?(coder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | private func configureUI() { 28 | backgroundColor = .picoGray 29 | layer.cornerRadius = 10 30 | clipsToBounds = true 31 | 32 | addSubview(mbtiButton) 33 | mbtiButton.snp.makeConstraints { make in 34 | make.edges.equalToSuperview() 35 | } 36 | 37 | } 38 | private func updateButtonAppearance() { 39 | if let buttonText = mbtiButton.titleLabel?.text?.lowercased(), let mbti = MBTIType(rawValue: buttonText) { 40 | backgroundColor = mbtiButton.isSelected ? UIColor(hex: mbti.colorName) : .picoGray 41 | } 42 | } 43 | 44 | func configureWithMBTI(_ mbti: MBTIType) { 45 | mbtiButton.setTitle(mbti.rawValue.uppercased(), for: .normal) 46 | mbtiButton.titleLabel?.font = .picoMBTILabelFont 47 | mbtiButton.isSelected = HomeViewModel.filterMbti.contains(mbti) 48 | backgroundColor = mbtiButton.isSelected ? UIColor(hex: mbti.colorName) : .picoGray 49 | mbtiButton.isUserInteractionEnabled = true 50 | mbtiButton.addTarget(self, action: #selector(buttonTouch), for: .touchUpInside) 51 | } 52 | 53 | @objc func buttonTouch() { 54 | mbtiButton.isSelected.toggle() 55 | if let buttonText = mbtiButton.titleLabel?.text?.lowercased(), let mbti = MBTIType(rawValue: buttonText) { 56 | backgroundColor = mbtiButton.isSelected ? UIColor(hex: mbti.colorName) : .picoGray 57 | if mbtiButton.isSelected { 58 | HomeViewModel.filterMbti.append(mbti) 59 | 60 | let mbtiData = try? JSONEncoder().encode(HomeViewModel.filterMbti) 61 | UserDefaults.standard.set(mbtiData, forKey: UserDefaultsManager.Key.filterMbti.rawValue) 62 | 63 | } else { 64 | if let index = HomeViewModel.filterMbti.firstIndex(of: mbti) { 65 | HomeViewModel.filterMbti.remove(at: index) 66 | 67 | let mbtiData = try? JSONEncoder().encode(HomeViewModel.filterMbti) 68 | UserDefaults.standard.set(mbtiData, forKey: UserDefaultsManager.Key.filterMbti.rawValue) 69 | } 70 | } 71 | HomeFilterViewController.filterChangeState = true 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Pico/Home/Detail/IntroViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntroViewController.swift 3 | // Pico 4 | // 5 | // Created by 신희권 on 10/23/23. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | class IntroViewController: UIViewController { 12 | private let introLabel: UILabel = { 13 | let label = PaddingLabel(padding: UIEdgeInsets(top: 12, left: 10, bottom: 12, right: 5)) 14 | label.text = "" 15 | label.font = .picoSubTitleFont 16 | label.textAlignment = .center 17 | label.numberOfLines = 0 18 | label.backgroundColor = .systemGray6 19 | label.layer.masksToBounds = true 20 | label.layer.cornerRadius = 10 21 | return label 22 | }() 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | addViews() 27 | makeConstraints() 28 | } 29 | 30 | func config(intro: String?) { 31 | if let intro = intro { 32 | introLabel.text = intro 33 | } else { 34 | view.isHidden = true 35 | } 36 | } 37 | } 38 | // MARK: - UI어쩌구~ 39 | extension IntroViewController { 40 | private func addViews() { 41 | view.addSubview(introLabel) 42 | } 43 | 44 | private func makeConstraints() { 45 | introLabel.snp.makeConstraints { make in 46 | make.edges.equalToSuperview() 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Pico/Home/Detail/UserDetailViewCell/AboutMeCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutMeCollectionViewCell.swift 3 | // Pico 4 | // 5 | // Created by 신희권 on 10/18/23. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | class AboutMeCollectionViewCell: UICollectionViewCell { 12 | 13 | private let stackView: UIStackView = { 14 | let stackView = UIStackView() 15 | stackView.axis = .horizontal 16 | stackView.alignment = .center 17 | stackView.distribution = .fill 18 | stackView.spacing = 10 19 | return stackView 20 | }() 21 | 22 | private let imageView: UIImageView = { 23 | let imageView = UIImageView() 24 | imageView.tintColor = .darkGray 25 | imageView.contentMode = .scaleAspectFit 26 | return imageView 27 | }() 28 | 29 | private let titleLabel: UILabel = { 30 | let label = UILabel() 31 | label.textColor = .darkGray 32 | label.font = .picoContentBoldFont 33 | return label 34 | }() 35 | 36 | override init(frame: CGRect) { 37 | super.init(frame: frame) 38 | addViews() 39 | makeConstraints() 40 | } 41 | 42 | func config(image: String, title: String) { 43 | if image == "smoke" || image == "religion" { 44 | self.imageView.image = UIImage(named: image) 45 | } else { 46 | self.imageView.image = UIImage(systemName: image) 47 | } 48 | self.titleLabel.text = title 49 | } 50 | 51 | private func addViews() { 52 | contentView.addSubview(stackView) 53 | stackView.addArrangedSubview([imageView, titleLabel]) 54 | } 55 | 56 | private func makeConstraints() { 57 | 58 | stackView.snp.makeConstraints { make in 59 | make.edges.equalToSuperview() 60 | } 61 | 62 | imageView.snp.makeConstraints { make in 63 | // make.leading.equalToSuperview() 64 | make.width.height.equalTo(20) 65 | } 66 | // 67 | // titleLabel.snp.makeConstraints { make in 68 | // make.top.equalTo(imageView) 69 | // make.leading.equalTo(imageView.snp.trailing).offset(10) 70 | // } 71 | } 72 | 73 | @available(*, unavailable) 74 | required init?(coder: NSCoder) { 75 | fatalError("init(coder:) has not been implemented") 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Pico/Home/Detail/UserDetailViewCell/HobbyCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HobbyCollectionViewCell.swift 3 | // Pico 4 | // 5 | // Created by 신희권 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | 10 | final class HobbyCollectionViewCell: UICollectionViewCell { 11 | private let label: UILabel = { 12 | let label = UILabel() 13 | label.layer.masksToBounds = true 14 | label.layer.cornerRadius = 4 15 | label.font = .picoButtonFont 16 | label.textAlignment = .center 17 | label.backgroundColor = .systemGray6 18 | return label 19 | }() 20 | 21 | override init(frame: CGRect) { 22 | super.init(frame: frame) 23 | addViews() 24 | makeConstraints() 25 | } 26 | 27 | func config(labelText: String) { 28 | label.text = labelText 29 | } 30 | 31 | private func addViews() { 32 | contentView.addSubview(label) 33 | } 34 | 35 | private func makeConstraints() { 36 | label.snp.makeConstraints { make in 37 | make.edges.equalToSuperview() 38 | } 39 | } 40 | @available(*, unavailable) 41 | required init?(coder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Pico/Home/Detail/UserDetailViewCell/LeftAlignedCollectionViewFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LeftAlignedCollectionViewFlowLayout.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/10/23. 6 | // 7 | 8 | import UIKit 9 | 10 | final class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { 11 | 12 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 13 | let attributes = super.layoutAttributesForElements(in: rect) 14 | 15 | var leftMargin = sectionInset.left 16 | var maxY: CGFloat = -1.0 17 | attributes?.forEach { layoutAttribute in 18 | if layoutAttribute.frame.origin.y >= maxY { 19 | leftMargin = sectionInset.left 20 | } 21 | 22 | layoutAttribute.frame.origin.x = leftMargin 23 | 24 | leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing 25 | maxY = max(layoutAttribute.frame.maxY, maxY) 26 | } 27 | 28 | return attributes 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Pico/Home/Detail/UserDetailViewCell/MbtiCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MbtiCollectionViewCell.swift 3 | // Pico 4 | // 5 | // Created by 신희권 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MbtiCollectionViewCell: UICollectionViewCell { 11 | private var mbtiView: MBTILabelView = MBTILabelView(mbti: .infp, scale: .small) 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | addViews() 16 | makeConstraints() 17 | } 18 | 19 | func config(mbtiType: MBTIType) { 20 | mbtiView.setMbti(mbti: mbtiType) 21 | } 22 | 23 | private func addViews() { 24 | contentView.addSubview(mbtiView) 25 | } 26 | 27 | private func makeConstraints() { 28 | mbtiView.snp.makeConstraints { make in 29 | make.edges.equalToSuperview() 30 | } 31 | } 32 | @available(*, unavailable) 33 | required init?(coder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Pico/Home/MBTICollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // asd.swift 3 | // Pico 4 | // 5 | // Created by 임대진 on 2023/09/27. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MBTICollectionViewController: UIViewController { 11 | private let columns = 4 12 | private let collectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | addViews() 17 | makeConstraints() 18 | configCollectionView() 19 | } 20 | 21 | private func configCollectionView() { 22 | collectionView.dataSource = self 23 | collectionView.delegate = self 24 | collectionView.register(cell: MBTILabelCollectionViewCell.self) 25 | } 26 | 27 | private func addViews() { 28 | view.addSubview(collectionView) 29 | } 30 | 31 | private func makeConstraints() { 32 | collectionView.snp.makeConstraints { make in 33 | make.top.leading.equalToSuperview() 34 | make.trailing.bottom.equalToSuperview() 35 | } 36 | } 37 | } 38 | extension MBTICollectionViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 39 | 40 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 41 | return MBTIType.allCases.count 42 | } 43 | 44 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 45 | let cell = collectionView.dequeueReusableCell(forIndexPath: indexPath, cellType: MBTILabelCollectionViewCell.self) 46 | let mbti = MBTIType.allCases[indexPath.item] 47 | cell.configureWithMBTI(mbti) 48 | return cell 49 | } 50 | 51 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 52 | let cellWidth = (collectionView.frame.width - CGFloat(columns - 1) * 10) / CGFloat(columns) 53 | return CGSize(width: cellWidth, height: 50) 54 | } 55 | 56 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 57 | return 10 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Pico/Like/Cell/CollectionViewFooterLoadingCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewFooterLoadingCell.swift 3 | // Pico 4 | // 5 | // Created by 방유빈 on 2023/10/16. 6 | // 7 | 8 | import UIKit 9 | 10 | final class CollectionViewFooterLoadingCell: UICollectionReusableView { 11 | private let indicatorView: UIActivityIndicatorView = { 12 | let indicator = UIActivityIndicatorView(style: .large) 13 | indicator.color = .picoBlue 14 | return indicator 15 | }() 16 | 17 | override init(frame: CGRect) { 18 | super.init(frame: frame) 19 | makeConstraints() 20 | } 21 | 22 | private func makeConstraints() { 23 | addSubview(indicatorView) 24 | indicatorView.snp.makeConstraints { make in 25 | make.center.equalToSuperview() 26 | } 27 | } 28 | 29 | func startLoading() { 30 | indicatorView.startAnimating() 31 | } 32 | 33 | func stopLoading() { 34 | indicatorView.stopAnimating() 35 | } 36 | 37 | @available(*, unavailable) 38 | required init?(coder aDecoder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Pico/Mail/MailListTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MailListTableViewCell.swift 3 | // Pico 4 | // 5 | // Created by 양성혜 on 2023/10/12. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /Pico/Model/Block/Block.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Block.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Block: Codable { 11 | let userId: String? 12 | var sendBlock: [BlockInfo]? 13 | var recivedBlock: [BlockInfo]? 14 | 15 | struct BlockInfo: Codable { 16 | var id: String { 17 | return userId 18 | } 19 | let userId: String 20 | let birth: String 21 | let nickName: String 22 | let mbti: MBTIType 23 | let imageURL: String 24 | var age: Int { 25 | let calendar = Calendar.current 26 | let currentDate = Date() 27 | let birthdate = birth.toDate() 28 | let ageComponents = calendar.dateComponents([.year], from: birthdate, to: currentDate) 29 | return ageComponents.year ?? 0 30 | } 31 | let createdDate: Double 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Pico/Model/Chat/Chat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Chat.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 1/17/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ChatRoom: Codable { 11 | let roomInfo: [RoomInfo] 12 | 13 | struct RoomInfo: Codable { 14 | var roomId: String 15 | /// 대화상대 정보 16 | var opponentId: String 17 | var opponentNickName: String 18 | var opponentMbti: MBTIType 19 | var opponentImageURL: String 20 | /// 마지막 메시지 21 | var lastMessage: String 22 | var sendedDate: Double 23 | } 24 | } 25 | 26 | struct ChatDetail: Codable { 27 | let chatInfo: [ChatInfo] 28 | 29 | struct ChatInfo: Codable { 30 | /// 보낸 사람 31 | let sendUserId: String 32 | let message: String 33 | let sendedDate: Double 34 | let isReading: Bool 35 | } 36 | } 37 | 38 | enum ChatType: String, Codable { 39 | case send 40 | case receive 41 | 42 | var imageStyle: String { 43 | switch self { 44 | case .send: 45 | return "myChat" 46 | case .receive: 47 | return "yourChat" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Pico/Model/Like/Like.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Like.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Like: Codable { 11 | let userId: String 12 | var sendedlikes: [LikeInfo]? 13 | var recivedlikes: [LikeInfo]? 14 | 15 | struct LikeInfo: Codable { 16 | // var id: String = UUID().uuidString 17 | let likedUserId: String 18 | let likeType: LikeType 19 | let birth: String 20 | let nickName: String 21 | let mbti: MBTIType 22 | let imageURL: String 23 | var age: Int { 24 | let calendar = Calendar.current 25 | let currentDate = Date() 26 | let birthdate = birth.toDate() 27 | let ageComponents = calendar.dateComponents([.year], from: birthdate, to: currentDate) 28 | return ageComponents.year ?? 0 29 | } 30 | let createdDate: Double 31 | var isMatch: Bool { 32 | return likeType == .matching 33 | } 34 | } 35 | 36 | enum LikeType: String, Codable { 37 | case like 38 | case dislike 39 | case matching 40 | 41 | var nameString: String { 42 | return self.rawValue.uppercased() 43 | } 44 | } 45 | 46 | static let likeInfoSample = Like.LikeInfo(likedUserId: "", likeType: .dislike, birth: "", nickName: "", mbti: .enfj, imageURL: "", createdDate: 0) 47 | } 48 | -------------------------------------------------------------------------------- /Pico/Model/Location/Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Location.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Location: Codable { 11 | let address: String 12 | /// 위도 13 | let latitude: Double 14 | /// 경도 15 | let longitude: Double 16 | } 17 | -------------------------------------------------------------------------------- /Pico/Model/MBTI/MBTI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MBTI.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/26. 6 | // 7 | 8 | import Foundation 9 | 10 | enum MBTIType: String, Codable, CaseIterable { 11 | case enfj 12 | case entj 13 | case enfp 14 | case entp 15 | case esfp 16 | case esfj 17 | case estp 18 | case estj 19 | case infp 20 | case infj 21 | case intp 22 | case istp 23 | case isfp 24 | case isfj 25 | case istj 26 | case intj 27 | 28 | var nameString: String { 29 | return self.rawValue.uppercased() 30 | } 31 | 32 | var colorName: String { 33 | switch self { 34 | case .enfj: 35 | return "#A0CDE5" 36 | case .entj: 37 | return "#A0E5BC" 38 | case .enfp: 39 | return "#C3A0E5" 40 | case .entp: 41 | return "#E4A0E5" 42 | case .esfp: 43 | return "#EFB495" 44 | case .esfj: 45 | return "#E4E5A0" 46 | case .estp: 47 | return "#A0E5D4" 48 | case .estj: 49 | return "#B5D5C5" 50 | case .infp: 51 | return "#FF9B9B" 52 | case .infj: 53 | return "#007CBE" 54 | case .intp: 55 | return "#116A7B" 56 | case .istp: 57 | return "#E5B1A0" 58 | case .isfp: 59 | return "#FFD966" 60 | case .isfj: 61 | return "#DA6969" 62 | case .istj: 63 | return "#EF9595" 64 | case .intj: 65 | return "#7C96AB" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Pico/Model/Mail/Mail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mail.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Mail: Codable { 11 | let userId: String 12 | var sendMailInfo: [MailInfo]? 13 | var receiveMailInfo: [MailInfo]? 14 | 15 | struct MailInfo: Codable { 16 | var id: String = UUID().uuidString 17 | let sendedUserId: String 18 | let receivedUserId: String 19 | let mailType: MailType 20 | let message: String 21 | let sendedDate: Double 22 | let isReading: Bool 23 | } 24 | } 25 | 26 | enum MailType: String, Codable { 27 | case send 28 | case receive 29 | 30 | var typeString: String { 31 | switch self { 32 | case .receive: 33 | return "받은 쪽지" 34 | case .send: 35 | return "보낸 쪽지" 36 | } 37 | } 38 | } 39 | 40 | enum MailSendType: Codable { 41 | case message 42 | case matching 43 | } 44 | -------------------------------------------------------------------------------- /Pico/Model/Notification/Notification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import UIKit 9 | 10 | // struct Notification: Codable { 11 | // let userId: String 12 | // let isNotification: Bool 13 | // let notiInfos: [NotiInfo] 14 | // 15 | // struct NotiInfo: Codable { 16 | // var id: String = UUID().uuidString 17 | // let notiType: NotiType 18 | // let sendUserId: String 19 | // let notiDate: Double 20 | // } 21 | // } 22 | 23 | enum NotiType: String, Codable { 24 | case like 25 | case message 26 | case matching 27 | 28 | var content: String { 29 | switch self { 30 | case .like: 31 | return "님이 좋아요를 누르셨습니다." 32 | case .message: 33 | return "님이 쪽지를 보냈습니다." 34 | case .matching: 35 | return "님과 매칭이 되었습니다. 채팅을 보내보세요." 36 | } 37 | } 38 | 39 | var iconSystemImageName: String { 40 | switch self { 41 | case .like: 42 | return "heart.fill" 43 | case .message: 44 | return "message.fill" 45 | case .matching: 46 | return "bolt.heart.fill" 47 | } 48 | } 49 | 50 | var iconColor: UIColor { 51 | switch self { 52 | case .like: 53 | return .systemPink 54 | case .message: 55 | return .picoBlue 56 | case .matching: 57 | return .systemPink 58 | } 59 | } 60 | } 61 | 62 | struct Noti: Codable { 63 | var id: String? = UUID().uuidString 64 | let receiveId: String // 알림 받는 사람 id 65 | let sendId: String // 보내는 사람 id 66 | let name: String // 보내는사람 이름 67 | let birth: String // 보내는사람 생년월일 68 | let imageUrl: String // 보내는 사람 첫번째 이미지 69 | let notiType: NotiType 70 | let mbti: MBTIType // 보내는 사람 mbti 71 | let createDate: Double // 알림 보낸 시간 72 | 73 | var age: Int { 74 | let calendar = Calendar.current 75 | let currentDate = Date() 76 | let birthdate = birth.toDate() 77 | let ageComponents = calendar.dateComponents([.year], from: birthdate, to: currentDate) 78 | return ageComponents.year ?? 0 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Pico/Model/Payment/Payment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Payment.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import Foundation 9 | 10 | enum PaymentType: String, Codable { 11 | case purchase 12 | case randombox 13 | case randomboxObtain 14 | case worldCup 15 | case mail 16 | case changeNickname 17 | case backCard 18 | 19 | var name: String { 20 | return self.rawValue 21 | } 22 | 23 | var koreaName: String { 24 | switch self { 25 | case .purchase: 26 | return "결제" 27 | case .randombox: 28 | return "랜덤박스" 29 | case .randomboxObtain: 30 | return "랜덤박스에서 획득" 31 | case .worldCup: 32 | return "월드컵" 33 | case .mail: 34 | return "메일 보내기" 35 | case .changeNickname: 36 | return "닉네임 변경하기" 37 | case .backCard: 38 | return "되돌리기" 39 | } 40 | } 41 | } 42 | 43 | struct Payment: Codable { 44 | let paymentInfos: [PaymentInfo]? 45 | 46 | struct PaymentInfo: Codable { 47 | var id: String = UUID().uuidString 48 | let price: Int 49 | let purchaseChuCount: Int 50 | let paymentType: PaymentType 51 | var purchasedDate: Double = Date().timeIntervalSince1970 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Pico/Model/Report/Report.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Report.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Report: Codable { 11 | let userId: String? 12 | var sendReport: [ReportInfo]? 13 | var recivedReport: [ReportInfo]? 14 | 15 | struct ReportInfo: Codable { 16 | var id: String = UUID().uuidString 17 | let reportedUserId: String 18 | let reason: String 19 | let birth: String 20 | let nickName: String 21 | let mbti: MBTIType 22 | let imageURL: String 23 | var age: Int { 24 | let calendar = Calendar.current 25 | let currentDate = Date() 26 | let birthdate = birth.toDate() 27 | let ageComponents = calendar.dateComponents([.year], from: birthdate, to: currentDate) 28 | return ageComponents.year ?? 0 29 | } 30 | let createdDate: Double 31 | } 32 | } 33 | 34 | struct AdminReport: Codable, Hashable { 35 | var id: String = UUID().uuidString 36 | let reportUserId: String 37 | let reportNickname: String 38 | let reportedUserId: String 39 | let reportedNickname: String 40 | let reason: String 41 | let birth: String 42 | let mbti: MBTIType 43 | let imageURL: String 44 | var age: Int { 45 | let calendar = Calendar.current 46 | let currentDate = Date() 47 | let birthdate = birth.toDate() 48 | let ageComponents = calendar.dateComponents([.year], from: birthdate, to: currentDate) 49 | return ageComponents.year ?? 0 50 | } 51 | let createdDate: Double 52 | 53 | static func == (lhs: AdminReport, rhs: AdminReport) -> Bool { 54 | return lhs.id == rhs.id 55 | } 56 | 57 | func hash(into hasher: inout Hasher) { 58 | hasher.combine(id) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Pico/Model/Stop/Stop.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stop.swift 3 | // Pico.admin 4 | // 5 | // Created by 최하늘 on 12/15/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 정지 11 | struct Stop: Codable { 12 | /// 정지된 날짜 13 | let createdDate: Double 14 | /// 정지일 수 15 | let during: Int 16 | let phoneNumber: String 17 | let user: User 18 | 19 | var endDate: Date? { 20 | return Calendar.current.date(byAdding: .day, value: self.during, to: Date(timeIntervalSince1970: self.createdDate)) 21 | } 22 | 23 | var endDateString: String { 24 | return endDate?.timeIntervalSince1970.toString(dateSeparator: .dot) ?? "0000.00.00" 25 | } 26 | } 27 | 28 | enum DuringType: CaseIterable { 29 | case oneDay 30 | case threeDay 31 | case senvenDay 32 | case oneMonth 33 | 34 | /// 숫자 35 | var number: Int { 36 | switch self { 37 | case .oneDay: 38 | return 1 39 | case .threeDay: 40 | return 3 41 | case .senvenDay: 42 | return 7 43 | case .oneMonth: 44 | return 30 45 | } 46 | } 47 | 48 | /// ?일 49 | var name: String { 50 | return "\(self.number)일" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Pico/Model/SubInfo/SubInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubInfo.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SubInfo: Codable { 11 | let intro: String? 12 | let height: Int? 13 | let drinkStatus: FrequencyType? 14 | let smokeStatus: FrequencyType? 15 | let religion: ReligionType? 16 | let education: EducationType? 17 | let job: String? 18 | 19 | let hobbies: [String]? 20 | let personalities: [String]? 21 | let favoriteMBTIs: [MBTIType]? 22 | } 23 | 24 | enum FrequencyType: String, CaseIterable, Codable { 25 | case usually 26 | case nomal 27 | case never 28 | 29 | var name: String { 30 | switch self { 31 | case .usually: 32 | return "자주" 33 | case .nomal: 34 | return "가끔" 35 | case .never: 36 | return "아예 안함" 37 | } 38 | } 39 | } 40 | 41 | enum ReligionType: String, CaseIterable, Codable { 42 | /// 무교 43 | case none 44 | /// 기독교 45 | case christianity 46 | /// 불교 47 | case buddhism 48 | /// 천주교 49 | case catholic 50 | /// 원불교 51 | case wonBuddhism 52 | /// 이슬람 53 | case islam 54 | /// 힌두교 55 | case hinduism 56 | /// 민속신앙 57 | case folk 58 | /// 기타 59 | case etc 60 | 61 | var name: String { 62 | switch self { 63 | case .none: 64 | "무교" 65 | case .christianity: 66 | "기독교" 67 | case .buddhism: 68 | "불교" 69 | case .catholic: 70 | "천주교" 71 | case .wonBuddhism: 72 | "원불교" 73 | case .islam: 74 | "이슬람" 75 | case .hinduism: 76 | "힌두교" 77 | case .folk: 78 | "민속신앙" 79 | case .etc: 80 | "기타" 81 | } 82 | } 83 | } 84 | 85 | enum EducationType: String, CaseIterable, Codable { 86 | case middle 87 | case high 88 | case college 89 | case university 90 | case graduate 91 | 92 | var name: String { 93 | switch self { 94 | case .middle: 95 | "중학교" 96 | case .high: 97 | "고등학교" 98 | case .college: 99 | "전문대학교" 100 | case .university: 101 | "대학교" 102 | case .graduate: 103 | "대학원" 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Pico/Model/Token/Token.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Token.swift 3 | // Pico 4 | // 5 | // Created by 방유빈 on 2023/10/18. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Token: Codable { 11 | let fcmToken: String 12 | let badgeCount: Int 13 | } 14 | -------------------------------------------------------------------------------- /Pico/Model/Unsubscribe/Unsubscribe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Unsubscribe.swift 3 | // Pico.admin 4 | // 5 | // Created by 최하늘 on 12/15/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 탈퇴 11 | struct Unsubscribe: Codable { 12 | /// 탈퇴된 날짜 13 | let createdDate: Double 14 | let phoneNumber: String 15 | let user: User 16 | } 17 | -------------------------------------------------------------------------------- /Pico/Model/User/CurrentUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CurrentUser.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 10/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct CurrentUser { 11 | let userId: String 12 | let nickName: String 13 | let mbti: String 14 | let imageURL: String 15 | let birth: String 16 | let latitude: Double 17 | let longitude: Double 18 | let phoneNumber: String 19 | } 20 | -------------------------------------------------------------------------------- /Pico/Model/User/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import Foundation 9 | 10 | struct User: Codable, Hashable { 11 | var id: String = UUID().uuidString 12 | let mbti: MBTIType 13 | let phoneNumber: String 14 | let gender: GenderType 15 | let birth: String 16 | let nickName: String 17 | var location: Location 18 | var imageURLs: [String] 19 | let createdDate: Double 20 | 21 | /// 추가정보 22 | var subInfo: SubInfo? 23 | /// 나를 신고한 기록 24 | var reports: [Report]? 25 | /// 내가 차단한 기록 26 | var blocks: [Block]? 27 | 28 | let chuCount: Int 29 | let isSubscribe: Bool 30 | let isOnline: Bool? 31 | 32 | static let tempUser = User(mbti: .enfj, phoneNumber: "", gender: .etc, birth: "", nickName: "", location: Location(address: "", latitude: 0.0, longitude: 0.0), imageURLs: [Defaults.userImageURLString], createdDate: 0.0, chuCount: 0, isSubscribe: false, isOnline: false) 33 | 34 | var age: Int { 35 | let calendar = Calendar.current 36 | let currentDate = Date() 37 | let birthdate = birth.toDate() 38 | let ageComponents = calendar.dateComponents([.year], from: birthdate, to: currentDate) 39 | return ageComponents.year ?? 0 40 | } 41 | 42 | static func == (lhs: User, rhs: User) -> Bool { 43 | return lhs.id == rhs.id 44 | } 45 | 46 | func hash(into hasher: inout Hasher) { 47 | hasher.combine(id) 48 | } 49 | } 50 | 51 | enum GenderType: String, Codable, CaseIterable { 52 | case male 53 | case female 54 | case etc 55 | } 56 | -------------------------------------------------------------------------------- /Pico/Mypage/AdvertisementViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdvertisementViewController.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/10/19. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | final class AdvertisementViewController: UIViewController { 14 | 15 | private let titleLabel: UILabel = { 16 | let label = UILabel() 17 | label.text = "준비중" 18 | label.textColor = .picoFontBlack 19 | label.textAlignment = .center 20 | label.font = .picoProfileNameFont 21 | return label 22 | }() 23 | 24 | private let subTitleLabel: UILabel = { 25 | let label = UILabel() 26 | label.text = "조금만 기다려주세요......" 27 | label.textColor = .picoFontBlack 28 | label.textAlignment = .center 29 | label.font = .picoContentFont 30 | return label 31 | }() 32 | 33 | private let imageView: UIImageView = { 34 | let view = UIImageView() 35 | view.image = UIImage(named: "chu") 36 | view.contentMode = .scaleAspectFit 37 | return view 38 | }() 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | viewConfig() 43 | addSubView() 44 | makeConstraints() 45 | } 46 | 47 | private func viewConfig() { 48 | title = "광고" 49 | view.configBackgroundColor() 50 | } 51 | 52 | private func addSubView() { 53 | view.addSubview([titleLabel, subTitleLabel, imageView]) 54 | } 55 | 56 | private func makeConstraints() { 57 | imageView.snp.makeConstraints { make in 58 | make.centerX.equalToSuperview() 59 | make.centerY.equalToSuperview().offset(-100) 60 | make.height.equalTo(180) 61 | make.width.equalTo(150) 62 | } 63 | titleLabel.snp.makeConstraints { make in 64 | make.top.equalTo(imageView.snp.bottom).offset(20) 65 | make.centerX.equalToSuperview() 66 | } 67 | 68 | subTitleLabel.snp.makeConstraints { make in 69 | make.top.equalTo(titleLabel.snp.bottom).offset(10) 70 | make.centerX.equalToSuperview() 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Pico/Mypage/Cell/MyPageCollectionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyPageCollectionCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | final class MyPageCollectionCell: UICollectionViewCell { 12 | 13 | private var imageSize: Int = 0 14 | 15 | private let image: UIImageView = { 16 | let image = UIImageView() 17 | image.contentMode = .scaleAspectFit 18 | return image 19 | }() 20 | private let titleLabel: UILabel = { 21 | let label = UILabel() 22 | label.textColor = .picoFontBlack 23 | label.font = .picoSubTitleFont 24 | return label 25 | }() 26 | 27 | private let subTitleLabel: UILabel = { 28 | let label = UILabel() 29 | label.textColor = .picoGradientMedium 30 | label.font = .picoMBTISmallLabelFont 31 | return label 32 | }() 33 | 34 | override init(frame: CGRect) { 35 | super.init(frame: frame) 36 | addSubView() 37 | makeConstraints() 38 | } 39 | 40 | @available(*, unavailable) 41 | required init?(coder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | 45 | func configure(imageName: String, title: String, subTitle: String) { 46 | image.image = UIImage(named: imageName) 47 | titleLabel.text = title 48 | subTitleLabel.text = subTitle 49 | 50 | imageSize = (imageName != "chu") ? 45 : 60 51 | image.snp.makeConstraints { make in 52 | make.height.equalTo(imageSize) 53 | make.width.equalTo(imageSize) 54 | } 55 | } 56 | 57 | private func addSubView() { 58 | [image, titleLabel, subTitleLabel].forEach { 59 | contentView.addSubview($0) 60 | } 61 | } 62 | 63 | private func makeConstraints() { 64 | titleLabel.snp.makeConstraints { make in 65 | make.leading.equalToSuperview().offset(15) 66 | make.centerY.equalToSuperview().offset(-10) 67 | } 68 | 69 | subTitleLabel.snp.makeConstraints { make in 70 | make.leading.equalTo(titleLabel) 71 | make.centerY.equalToSuperview().offset(10) 72 | } 73 | 74 | image.snp.makeConstraints { make in 75 | make.trailing.equalToSuperview().offset(-10) 76 | make.centerY.equalToSuperview() 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Pico/Mypage/Cell/MyPageDefaultTableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyPageDefaultTableCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | final class MyPageDefaultTableCell: UITableViewCell { 12 | 13 | private let tableImageView: UIImageView = { 14 | let imageView = UIImageView() 15 | imageView.tintColor = .darkGray 16 | return imageView 17 | }() 18 | 19 | private let tableLabel: UILabel = { 20 | let label = UILabel() 21 | label.textColor = .picoFontBlack 22 | label.font = UIFont.picoSubTitleFont 23 | return label 24 | }() 25 | 26 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 27 | super.init(style: style, reuseIdentifier: reuseIdentifier) 28 | addSubView() 29 | makeConstraints() 30 | } 31 | 32 | @available(*, unavailable) 33 | required init?(coder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | override func setSelected(_ selected: Bool, animated: Bool) { 38 | super.setSelected(selected, animated: animated) 39 | 40 | // Configure the view for the selected state 41 | } 42 | 43 | func configure(imageName: String, title: String) { 44 | tableImageView.image = UIImage(systemName: imageName) 45 | tableLabel.text = title 46 | } 47 | 48 | private func addSubView() { 49 | [tableImageView, tableLabel].forEach { 50 | contentView.addSubview($0) 51 | } 52 | } 53 | 54 | private func makeConstraints() { 55 | tableImageView.snp.makeConstraints { make in 56 | make.centerY.equalToSuperview() 57 | make.leading.equalToSuperview().offset(10) 58 | make.height.width.equalTo(25) 59 | } 60 | 61 | tableLabel.snp.makeConstraints { make in 62 | make.centerY.equalToSuperview() 63 | make.leading.equalTo(tableImageView.snp.trailing).offset(10) 64 | make.trailing.equalToSuperview().offset(-10) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Pico/Mypage/Cell/MyPageMatchingTableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyPageMatchingTableCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | final class MyPageMatchingTableCell: UITableViewCell { 12 | 13 | private let titleLabel: UILabel = { 14 | let label = UILabel() 15 | label.textColor = .picoFontBlack 16 | label.font = UIFont.picoSubTitleFont 17 | label.text = "파워 매칭 서비스 제공" 18 | return label 19 | }() 20 | private let subtitleLabel: UILabel = { 21 | let label = UILabel() 22 | label.textColor = .picoFontGray 23 | label.font = UIFont.picoDescriptionFont 24 | label.text = "나와 성향이 잘 맞는 사람 우선 추천" 25 | return label 26 | }() 27 | private let premiumImage: UIImageView = { 28 | let imageView = UIImageView() 29 | imageView.contentMode = .scaleAspectFit 30 | imageView.image = UIImage(named: "premiumImage") 31 | return imageView 32 | }() 33 | private let premiumLabel: UILabel = { 34 | let label = UILabel() 35 | label.textColor = .picoFontBlack 36 | label.font = UIFont.picoSubTitleFont 37 | label.text = "premiumImage" 38 | label.textAlignment = .center 39 | label.textColor = .white 40 | label.backgroundColor = .purple 41 | label.layer.masksToBounds = true 42 | label.layer.cornerRadius = 17 43 | return label 44 | }() 45 | 46 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 47 | super.init(style: style, reuseIdentifier: reuseIdentifier) 48 | addSubView() 49 | makeConstraints() 50 | } 51 | 52 | @available(*, unavailable) 53 | required init?(coder: NSCoder) { 54 | fatalError("init(coder:) has not been implemented") 55 | } 56 | 57 | override func setSelected(_ selected: Bool, animated: Bool) { 58 | super.setSelected(selected, animated: animated) 59 | 60 | // Configure the view for the selected state 61 | } 62 | 63 | private func addSubView() { 64 | [premiumImage, titleLabel, subtitleLabel].forEach { 65 | contentView.addSubview($0) 66 | } 67 | } 68 | 69 | private func makeConstraints() { 70 | premiumImage.snp.makeConstraints { make in 71 | make.trailing.equalToSuperview().offset(-15) 72 | make.centerY.equalToSuperview() 73 | make.height.equalTo(40) 74 | make.width.equalTo(120) 75 | } 76 | titleLabel.snp.makeConstraints { make in 77 | make.top.equalTo(premiumImage) 78 | make.leading.equalToSuperview().offset(15) 79 | make.trailing.equalToSuperview().inset(10) 80 | } 81 | subtitleLabel.snp.makeConstraints { make in 82 | make.top.equalTo(titleLabel.snp.bottom).offset(5) 83 | make.leading.equalToSuperview().offset(15) 84 | make.trailing.equalToSuperview().inset(10) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Pico/Mypage/CircularProgressBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircularProgressBarView.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/10/04. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | import RxSwift 11 | 12 | final class CircularProgressBarView: UIView { 13 | 14 | private var circleLayer = CAShapeLayer() 15 | private var progressLayer = CAShapeLayer() 16 | private let startPoint = CGFloat(Double.pi * 0.7) 17 | 18 | private let circleLayerEndPoint = CGFloat(Double.pi * 2.3) 19 | private var endPointValue: Double = 0 20 | let disposeBag = DisposeBag() 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | } 25 | 26 | @available(*, unavailable) 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | override func layoutSubviews() { 32 | super.layoutSubviews() 33 | createCircularPath() 34 | } 35 | 36 | func triggerLayoutSubviews() { 37 | setNeedsLayout() 38 | layoutIfNeeded() 39 | } 40 | 41 | func binds(_ viewModel: CircularProgressBarViewModel) { 42 | viewModel.profilePerfection 43 | .subscribe { 44 | self.endPointValue = $0 45 | } 46 | .disposed(by: disposeBag) 47 | } 48 | 49 | private func createCircularPath() { 50 | let endPoint = CGFloat((Double.pi * 0.7) + (Double.pi * 1.6) * endPointValue) 51 | let center = CGPoint(x: bounds.midX, y: bounds.midY) 52 | let circularPath = UIBezierPath(arcCenter: center, radius: 80, startAngle: startPoint, endAngle: circleLayerEndPoint, clockwise: true) 53 | let progressPath = UIBezierPath(arcCenter: center, radius: 80, startAngle: startPoint, endAngle: endPoint, clockwise: true) 54 | circleLayer.path = circularPath.cgPath 55 | circleLayer.fillColor = UIColor.clear.cgColor 56 | circleLayer.lineCap = .round 57 | circleLayer.lineWidth = 7.0 58 | circleLayer.strokeEnd = 1.0 59 | circleLayer.strokeColor = UIColor.lightGray.cgColor 60 | layer.addSublayer(circleLayer) 61 | 62 | progressLayer.path = progressPath.cgPath 63 | progressLayer.fillColor = UIColor.clear.cgColor 64 | progressLayer.lineCap = .round 65 | progressLayer.lineWidth = 7.0 66 | progressLayer.strokeEnd = 0 67 | progressLayer.strokeColor = UIColor.picoBlue.cgColor 68 | layer.addSublayer(progressLayer) 69 | } 70 | 71 | func progressAnimation(duration: TimeInterval) { 72 | let circularProgressAnimation = CABasicAnimation(keyPath: "strokeEnd") 73 | circularProgressAnimation.duration = duration 74 | circularProgressAnimation.toValue = 1.0 75 | circularProgressAnimation.fillMode = .forwards 76 | circularProgressAnimation.isRemovedOnCompletion = false 77 | progressLayer.add(circularProgressAnimation, forKey: "progressAnim") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Pico/Mypage/PremiumViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PremiumViewController.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/10/19. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | final class PremiumViewController: UIViewController { 14 | 15 | private let titleLabel: UILabel = { 16 | let label = UILabel() 17 | label.text = "준비중" 18 | label.textColor = .picoFontBlack 19 | label.textAlignment = .center 20 | label.font = .picoProfileNameFont 21 | return label 22 | }() 23 | 24 | private let subTitleLabel: UILabel = { 25 | let label = UILabel() 26 | label.text = "조금만 기다려주세요......" 27 | label.textColor = .picoFontBlack 28 | label.textAlignment = .center 29 | label.font = .picoContentFont 30 | return label 31 | }() 32 | 33 | private let imageView: UIImageView = { 34 | let view = UIImageView() 35 | view.image = UIImage(named: "chu") 36 | view.contentMode = .scaleAspectFit 37 | return view 38 | }() 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | viewConfig() 43 | addSubView() 44 | makeConstraints() 45 | } 46 | 47 | private func viewConfig() { 48 | title = "파워 매칭" 49 | view.configBackgroundColor() 50 | } 51 | 52 | private func addSubView() { 53 | view.addSubview([titleLabel, subTitleLabel, imageView]) 54 | } 55 | 56 | private func makeConstraints() { 57 | imageView.snp.makeConstraints { make in 58 | make.centerX.equalToSuperview() 59 | make.centerY.equalToSuperview().offset(-100) 60 | make.height.equalTo(180) 61 | make.width.equalTo(150) 62 | } 63 | titleLabel.snp.makeConstraints { make in 64 | make.top.equalTo(imageView.snp.bottom).offset(20) 65 | make.centerX.equalToSuperview() 66 | } 67 | 68 | subTitleLabel.snp.makeConstraints { make in 69 | make.top.equalTo(titleLabel.snp.bottom).offset(10) 70 | make.centerX.equalToSuperview() 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Pico/Mypage/ProfileEdit/Cell/ProfileEditCollectionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileEditCollectionCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/09/27. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | import Kingfisher 11 | 12 | final class ProfileEditCollectionCell: UICollectionViewCell { 13 | 14 | private let imageView: UIImageView = { 15 | let imageView = UIImageView() 16 | imageView.image = .add 17 | imageView.contentMode = .scaleAspectFill 18 | return imageView 19 | }() 20 | 21 | let deleteButton: UIButton = { 22 | let button = UIButton(configuration: .plain()) 23 | let imageConfig = UIImage.SymbolConfiguration(pointSize: 17, weight: .light) 24 | let image = UIImage(systemName: "xmark.circle.fill", withConfiguration: imageConfig) 25 | button.setImage(image, for: .normal) 26 | button.tintColor = .white.withAlphaComponent(0.8) 27 | return button 28 | }() 29 | 30 | override init(frame: CGRect) { 31 | super.init(frame: frame) 32 | addSubView() 33 | makeConstraints() 34 | } 35 | 36 | @available(*, unavailable) 37 | required init?(coder: NSCoder) { 38 | fatalError("init(coder:) has not been implemented") 39 | } 40 | 41 | override func prepareForReuse() { 42 | imageView.image = UIImage(systemName: "plus") 43 | } 44 | 45 | func configure(imageName: String) { 46 | guard let url = URL(string: imageName) else { return } 47 | imageView.kf.indicatorType = .custom(indicator: CustomIndicator(cycleSize: .small)) 48 | imageView.kf.setImage(with: url) 49 | imageView.contentMode = .scaleAspectFill 50 | } 51 | 52 | func configure(image: UIImage) { 53 | imageView.image = image 54 | } 55 | 56 | func cellConfigure() { 57 | contentView.backgroundColor = .picoLightGray 58 | contentView.layer.masksToBounds = true 59 | contentView.layer.cornerRadius = 10 60 | } 61 | 62 | private func addSubView() { 63 | [imageView, deleteButton].forEach { 64 | contentView.addSubview($0) 65 | } 66 | } 67 | 68 | private func makeConstraints() { 69 | imageView.snp.makeConstraints { make in 70 | make.edges.equalToSuperview() 71 | } 72 | 73 | deleteButton.snp.makeConstraints { make in 74 | make.top.equalToSuperview() 75 | make.trailing.equalToSuperview().offset(7) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Pico/Mypage/ProfileEdit/Cell/ProfileEditEmptyCollectionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileEditEmptyCollectionCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/10/13. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | import Kingfisher 11 | 12 | final class ProfileEditEmptyCollectionCell: UICollectionViewCell { 13 | 14 | private let imageView: UIImageView = { 15 | let imageView = UIImageView() 16 | let image = UIImage(systemName: "plus") 17 | imageView.image = image 18 | imageView.tintColor = .lightGray 19 | imageView.contentMode = .scaleAspectFit 20 | return imageView 21 | }() 22 | 23 | override init(frame: CGRect) { 24 | super.init(frame: frame) 25 | addSubView() 26 | makeConstraints() 27 | } 28 | 29 | @available(*, unavailable) 30 | required init?(coder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | override func prepareForReuse() { 35 | imageView.image = UIImage(systemName: "plus") 36 | } 37 | 38 | func cellConfigure() { 39 | contentView.backgroundColor = .picoLightGray 40 | contentView.layer.masksToBounds = true 41 | contentView.layer.cornerRadius = 10 42 | } 43 | 44 | private func addSubView() { 45 | [imageView].forEach { 46 | contentView.addSubview($0) 47 | } 48 | } 49 | 50 | private func makeConstraints() { 51 | imageView.snp.makeConstraints { make in 52 | make.center.equalToSuperview() 53 | make.width.height.equalTo(30) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Pico/Mypage/ProfileEdit/Cell/ProfileEditIntroTabelCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileEditIntroTabelCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/09/27. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ProfileEditIntroTabelCell: UITableViewCell { 11 | 12 | private let titleLabel: UILabel = { 13 | let label = UILabel() 14 | label.textColor = .picoFontBlack 15 | label.font = UIFont.picoSubTitleFont 16 | label.text = "한 줄 소개" 17 | return label 18 | }() 19 | 20 | private let textfield = CommonTextField() 21 | 22 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 23 | super.init(style: style, reuseIdentifier: reuseIdentifier) 24 | addSubView() 25 | makeConstraints() 26 | } 27 | 28 | @available(*, unavailable) 29 | required init?(coder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | func configure(intro: String) { 34 | 35 | } 36 | 37 | private func addSubView() { 38 | [titleLabel, textfield].forEach { 39 | contentView.addSubview($0) 40 | } 41 | } 42 | 43 | private func makeConstraints() { 44 | titleLabel.snp.makeConstraints { make in 45 | make.top.equalToSuperview().offset(10) 46 | make.leading.trailing.equalToSuperview().inset(15) 47 | 48 | } 49 | 50 | textfield.snp.makeConstraints { make in 51 | make.leading.trailing.equalToSuperview().inset(15) 52 | make.bottom.equalToSuperview().offset(-15) 53 | make.height.equalTo(50) 54 | } 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Pico/Mypage/ProfileEdit/Cell/ProfileEditLoactionTabelCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileEditLoactionTabelCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/09/27. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | final class ProfileEditLoactionTabelCell: UITableViewCell { 12 | 13 | private let titleLabel: UILabel = { 14 | let label = UILabel() 15 | label.textColor = .picoFontBlack 16 | label.font = UIFont.picoSubTitleFont 17 | label.text = "내 위치" 18 | return label 19 | }() 20 | 21 | private lazy var locationChangeButton: UIButton = { 22 | let button = UIButton() 23 | let image = UIImage(named: "locationPointImage")?.resized(toSize: CGSize(width: 20, height: 20)) 24 | var configuration = UIButton.Configuration.plain() 25 | configuration.subtitle = "" 26 | configuration.subtitleLineBreakMode = .byTruncatingTail 27 | configuration.image = image 28 | configuration.imagePlacement = .trailing 29 | configuration.imagePadding = 7 30 | configuration.titleAlignment = .trailing 31 | button.configuration = configuration 32 | button.addTarget(self, action: #selector(tappedButton), for: .touchUpInside) 33 | button.accessibilityLabel = "현재위치 변경" 34 | return button 35 | }() 36 | 37 | private var profileEditViewModel: ProfileEditViewModel? 38 | private let locationManager = LocationService() 39 | 40 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 41 | super.init(style: style, reuseIdentifier: reuseIdentifier) 42 | addSubView() 43 | makeConstraints() 44 | } 45 | 46 | @available(*, unavailable) 47 | required init?(coder: NSCoder) { 48 | fatalError("init(coder:) has not been implemented") 49 | } 50 | 51 | func configure(location: String, viewModel: ProfileEditViewModel) { 52 | locationChangeButton.configuration?.subtitle = location 53 | profileEditViewModel = viewModel 54 | } 55 | 56 | private func locationConfigure() { 57 | locationManager.configLocation() 58 | } 59 | 60 | @objc private func tappedButton() { 61 | locationConfigure() 62 | profileEditViewModel?.modalType = .location 63 | let space = locationManager.locationManager.location?.coordinate 64 | let lat = space?.latitude 65 | let long = space?.longitude 66 | 67 | locationManager.getAddress(latitude: lat, longitude: long) { [weak self] location in 68 | guard let self else { return } 69 | if let location = location { 70 | profileEditViewModel?.updateData(data: location) 71 | } else { 72 | self.locationManager.configLocation() 73 | } 74 | } 75 | } 76 | 77 | private func addSubView() { 78 | [titleLabel, locationChangeButton].forEach { 79 | contentView.addSubview($0) 80 | } 81 | } 82 | 83 | private func makeConstraints() { 84 | titleLabel.snp.makeConstraints { make in 85 | make.leading.equalToSuperview().offset(15) 86 | make.centerY.equalToSuperview() 87 | make.trailing.equalTo(locationChangeButton.snp.leading).offset(-120) 88 | } 89 | 90 | locationChangeButton.snp.makeConstraints { make in 91 | make.trailing.equalToSuperview().offset(-10) 92 | make.centerY.equalToSuperview() 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Pico/Mypage/ProfileEdit/Cell/ProfileEditModalCollectionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileEditModalCollectionCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/10/13. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | final class ProfileEditModalCollectionCell: UICollectionViewCell { 12 | 13 | private let contentLabel: UILabel = { 14 | let label = UILabel() 15 | label.font = .picoContentFont 16 | label.textColor = .picoFontGray 17 | label.text = "하이하이" 18 | label.textAlignment = .center 19 | return label 20 | }() 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | addSubView() 25 | makeConstraints() 26 | cellConfigure() 27 | } 28 | 29 | @available(*, unavailable) 30 | required init?(coder aDecoder: NSCoder) { 31 | super.init(coder: aDecoder) 32 | } 33 | 34 | override var isSelected: Bool { 35 | didSet { 36 | if isSelected { 37 | contentView.backgroundColor = .picoBetaBlue 38 | contentLabel.textColor = .black 39 | } else { 40 | contentView.backgroundColor = .clear 41 | contentLabel.textColor = .picoFontGray 42 | } 43 | } 44 | } 45 | 46 | func configure(content: String) { 47 | contentLabel.text = content 48 | } 49 | 50 | private func cellConfigure() { 51 | contentView.layer.masksToBounds = false 52 | contentView.layer.borderColor = UIColor.picoFontGray.cgColor 53 | contentView.layer.borderWidth = 1 54 | contentView.layer.cornerRadius = 15 55 | } 56 | 57 | private func addSubView() { 58 | contentView.addSubview([contentLabel]) 59 | } 60 | 61 | private func makeConstraints() { 62 | contentLabel.snp.makeConstraints { make in 63 | make.edges.equalToSuperview() 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Pico/Mypage/ProfileEdit/Cell/ProfileEditModalMbtiCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileEditModalMbtiCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/10/16. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | final class ProfileEditModalMbtiCell: UICollectionViewCell { 12 | 13 | private let contentLabel: UILabel = { 14 | let label = UILabel() 15 | label.font = .picoContentFont 16 | label.textColor = .picoFontGray 17 | label.textAlignment = .center 18 | return label 19 | }() 20 | 21 | override init(frame: CGRect) { 22 | super.init(frame: frame) 23 | addSubView() 24 | makeConstraints() 25 | cellConfigure() 26 | } 27 | 28 | @available(*, unavailable) 29 | required init?(coder aDecoder: NSCoder) { 30 | super.init(coder: aDecoder) 31 | } 32 | 33 | override var isSelected: Bool { 34 | didSet { 35 | if isSelected { 36 | contentView.backgroundColor = .picoBetaBlue 37 | contentLabel.textColor = .black 38 | } else { 39 | contentView.backgroundColor = .clear 40 | contentLabel.textColor = .picoFontGray 41 | } 42 | } 43 | } 44 | 45 | private func cellConfigure() { 46 | contentView.layer.masksToBounds = false 47 | contentView.layer.borderColor = UIColor.picoFontGray.cgColor 48 | contentView.layer.borderWidth = 1 49 | contentView.layer.cornerRadius = 15 50 | } 51 | 52 | func configure(content: String) { 53 | contentLabel.text = content 54 | } 55 | 56 | private func addSubView() { 57 | contentView.addSubview([contentLabel]) 58 | } 59 | 60 | private func makeConstraints() { 61 | contentLabel.snp.makeConstraints { make in 62 | make.edges.equalToSuperview() 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Pico/Mypage/ProfileEdit/Cell/ProfileEditNicknameTabelCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileEditNicknameTabelCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol ProfileEditNicknameDelegate: AnyObject { 11 | func presentEditView() 12 | } 13 | 14 | final class ProfileEditNicknameTabelCell: UITableViewCell { 15 | 16 | private let titleLabel: UILabel = { 17 | let label = UILabel() 18 | label.textColor = .picoFontBlack 19 | label.font = UIFont.picoSubTitleFont 20 | label.text = "닉네임 변경" 21 | return label 22 | }() 23 | 24 | private lazy var nicknameChangeButton: UIButton = { 25 | let button = UIButton() 26 | let image = UIImage(named: "chu")?.resized(toSize: CGSize(width: 30, height: 30)) 27 | var configuration = UIButton.Configuration.plain() 28 | configuration.image = image 29 | configuration.imagePlacement = .leading 30 | configuration.imagePadding = 3 31 | configuration.subtitle = "50" 32 | button.configuration = configuration 33 | button.layer.masksToBounds = false 34 | button.layer.cornerRadius = 15 35 | button.layer.borderWidth = 1 36 | button.layer.borderColor = UIColor.picoBlue.cgColor 37 | button.addTarget(self, action: #selector(tappedButton), for: .touchUpInside) 38 | button.accessibilityLabel = "50츄내고 이름변경" 39 | return button 40 | }() 41 | 42 | weak var profileEditNicknameDelegate: ProfileEditNicknameDelegate? 43 | 44 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 45 | super.init(style: style, reuseIdentifier: reuseIdentifier) 46 | addSubView() 47 | makeConstraints() 48 | } 49 | 50 | @available(*, unavailable) 51 | required init?(coder: NSCoder) { 52 | fatalError("init(coder:) has not been implemented") 53 | } 54 | 55 | @objc private func tappedButton() { 56 | profileEditNicknameDelegate?.presentEditView() 57 | } 58 | 59 | private func addSubView() { 60 | [titleLabel, nicknameChangeButton].forEach { 61 | contentView.addSubview($0) 62 | } 63 | } 64 | 65 | private func makeConstraints() { 66 | titleLabel.snp.makeConstraints { make in 67 | make.leading.equalToSuperview().offset(15) 68 | make.centerY.equalToSuperview() 69 | } 70 | 71 | nicknameChangeButton.snp.makeConstraints { make in 72 | make.trailing.equalToSuperview().offset(-20) 73 | make.centerY.equalToSuperview() 74 | make.width.equalTo(100) 75 | make.height.equalTo(30) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Pico/Mypage/ProfileEdit/Cell/ProfileEditTextModalCollectionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileEditTextModalCollectionCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/10/13. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | final class ProfileEditTextModalCollectionCell: UICollectionViewCell { 12 | 13 | private let contentLabel: UILabel = { 14 | let label = UILabel() 15 | label.font = .picoContentFont 16 | label.textColor = .picoFontGray 17 | label.textAlignment = .center 18 | return label 19 | }() 20 | 21 | let deleteButton: UIButton = { 22 | let button = UIButton(configuration: .plain()) 23 | let imageConfig = UIImage.SymbolConfiguration(pointSize: 13, weight: .light) 24 | let image = UIImage(systemName: "x.circle", withConfiguration: imageConfig) 25 | button.setImage(image, for: .normal) 26 | button.tintColor = .gray.withAlphaComponent(0.9) 27 | return button 28 | }() 29 | 30 | override init(frame: CGRect) { 31 | super.init(frame: frame) 32 | addSubView() 33 | makeConstraints() 34 | cellConfigure() 35 | } 36 | 37 | @available(*, unavailable) 38 | required init?(coder aDecoder: NSCoder) { 39 | super.init(coder: aDecoder) 40 | } 41 | 42 | func configure(content: String) { 43 | contentLabel.text = content 44 | } 45 | 46 | private func addSubView() { 47 | contentView.addSubview([contentLabel, deleteButton]) 48 | } 49 | 50 | private func cellConfigure() { 51 | contentView.layer.masksToBounds = false 52 | contentView.layer.borderColor = UIColor.picoFontGray.cgColor 53 | contentView.layer.borderWidth = 1 54 | contentView.layer.cornerRadius = 15 55 | } 56 | 57 | private func makeConstraints() { 58 | contentLabel.snp.makeConstraints { make in 59 | make.centerY.equalToSuperview() 60 | make.leading.equalToSuperview().offset(8) 61 | } 62 | 63 | deleteButton.snp.makeConstraints { make in 64 | make.centerY.equalToSuperview() 65 | make.trailing 66 | .equalToSuperview().offset(6) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Pico/Mypage/ProfileEdit/Cell/ProfileEditTextTabelCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileEditTextTabelCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/09/27. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | final class ProfileEditTextTabelCell: UITableViewCell { 12 | 13 | private let titleLabel: UILabel = { 14 | let label = UILabel() 15 | label.textColor = .picoFontBlack 16 | label.font = .picoSubTitleFont 17 | return label 18 | }() 19 | 20 | private let contentLabel: UILabel = { 21 | let label = UILabel() 22 | label.font = .picoDescriptionFont 23 | label.textColor = .picoBlue 24 | label.text = "추가" 25 | return label 26 | }() 27 | 28 | private let nextImageView: UIImageView = { 29 | let imageView = UIImageView() 30 | imageView.image = UIImage(systemName: "chevron.right") 31 | imageView.tintColor = .lightGray 32 | imageView.contentMode = .scaleAspectFit 33 | return imageView 34 | }() 35 | 36 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 37 | super.init(style: style, reuseIdentifier: reuseIdentifier) 38 | addSubView() 39 | makeConstraints() 40 | } 41 | 42 | @available(*, unavailable) 43 | required init?(coder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | 47 | override func prepareForReuse() { 48 | super.prepareForReuse() 49 | contentLabel.text = "추가" 50 | contentLabel.font = .picoDescriptionFont 51 | contentLabel.textColor = .picoBlue 52 | contentLabel.textAlignment = .right 53 | titleLabel.text = "" 54 | titleLabel.textAlignment = .left 55 | nextImageView.image = UIImage(systemName: "chevron.right") 56 | } 57 | 58 | func configure(titleLabel: String, contentLabel: String?) { 59 | self.titleLabel.text = titleLabel 60 | self.titleLabel.textAlignment = .left 61 | guard let contentLabel else { return } 62 | if !contentLabel.isEmpty { 63 | self.contentLabel.text = contentLabel 64 | self.contentLabel.textColor = .picoFontBlack 65 | self.contentLabel.textAlignment = .right 66 | } 67 | } 68 | 69 | private func addSubView() { 70 | [titleLabel, contentLabel, nextImageView].forEach { 71 | contentView.addSubview($0) 72 | } 73 | } 74 | 75 | private func makeConstraints() { 76 | titleLabel.snp.makeConstraints { make in 77 | make.leading.equalToSuperview().offset(15) 78 | make.centerY.equalToSuperview() 79 | } 80 | 81 | contentLabel.snp.makeConstraints { make in 82 | make.trailing.equalTo(nextImageView.snp.leading).offset(-10) 83 | make.centerY.equalToSuperview() 84 | make.width.equalTo(150) 85 | } 86 | 87 | nextImageView.snp.makeConstraints { make in 88 | make.trailing.equalToSuperview().offset(-15) 89 | make.centerY.equalToSuperview() 90 | make.width.height.equalTo(20) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Pico/Mypage/ProfileEdit/CenterAlignedCollectionViewFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CenterAlignedCollectionViewFlowLayout.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/10/13. 6 | // 7 | 8 | import UIKit 9 | 10 | final class CenterAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { 11 | 12 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 13 | guard let superAttributes = super.layoutAttributesForElements(in: rect) else { return nil } 14 | guard let attributes = NSArray(array: superAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes] else { return nil } 15 | 16 | let leftPadding: CGFloat = 8 17 | let interItemSpacing = minimumInteritemSpacing 18 | 19 | var leftMargin: CGFloat = leftPadding 20 | var maxY: CGFloat = -1.0 21 | var rowSizes: [[CGFloat]] = [] 22 | var currentRow: Int = 0 23 | attributes.forEach { layoutAttribute in 24 | if layoutAttribute.frame.origin.y >= maxY { 25 | leftMargin = leftPadding 26 | if rowSizes.isEmpty { 27 | rowSizes = [[leftMargin, 0]] 28 | } else { 29 | rowSizes.append([leftMargin, 0]) 30 | currentRow += 1 31 | } 32 | } 33 | 34 | layoutAttribute.frame.origin.x = leftMargin 35 | leftMargin += layoutAttribute.frame.width + interItemSpacing 36 | maxY = max(layoutAttribute.frame.maxY, maxY) 37 | rowSizes[currentRow][1] = leftMargin - interItemSpacing 38 | } 39 | 40 | leftMargin = leftPadding 41 | maxY = -1.0 42 | currentRow = 0 43 | 44 | attributes.forEach { layoutAttribute in 45 | if layoutAttribute.frame.origin.y >= maxY { 46 | leftMargin = leftPadding 47 | let rowWidth = rowSizes[currentRow][1] - rowSizes[currentRow][0] // last.x - first.x 48 | let appendedMargin = (collectionView!.frame.width - leftPadding - rowWidth - leftPadding) / 2 49 | leftMargin += appendedMargin 50 | currentRow += 1 51 | } 52 | 53 | layoutAttribute.frame.origin.x = leftMargin 54 | leftMargin += layoutAttribute.frame.width + interItemSpacing 55 | maxY = max(layoutAttribute.frame.maxY, maxY) 56 | } 57 | return attributes 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Pico/Mypage/ProfileEdit/ProfileEditTableHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileEditTableHeaderView.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/09/27. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ProfileEditTableHeaderView: UITableViewHeaderFooterView { 11 | 12 | private let borderView: UIView = { 13 | let view = UIView() 14 | view.backgroundColor = .lightGray 15 | return view 16 | }() 17 | 18 | private let headerLabel: UILabel = { 19 | let label = UILabel() 20 | label.font = .picoDescriptionFont 21 | label.textColor = .picoFontGray 22 | label.text = "추가 정보" 23 | return label 24 | }() 25 | 26 | override init(reuseIdentifier: String?) { 27 | super.init(reuseIdentifier: reuseIdentifier) 28 | addSubView() 29 | makeConstraints() 30 | } 31 | 32 | @available(*, unavailable) 33 | required init?(coder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | func configure(headerLabel: String) { 38 | self.headerLabel.text = headerLabel 39 | } 40 | 41 | private func addSubView() { 42 | [borderView, headerLabel].forEach { 43 | contentView.addSubview($0) 44 | } 45 | } 46 | 47 | private func makeConstraints() { 48 | borderView.snp.makeConstraints { make in 49 | make.top.equalToSuperview().offset(-15) 50 | make.leading.trailing.equalToSuperview() 51 | make.height.equalTo(0.5) 52 | } 53 | 54 | headerLabel.snp.makeConstraints { make in 55 | make.bottom.equalToSuperview() 56 | make.leading.equalToSuperview().offset(15) 57 | make.trailing.equalToSuperview().offset(-15) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Pico/Mypage/ProfileEdit/ViewModel/ProfileEditModalViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileEditModalViewModel.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/10/13. 6 | // 7 | 8 | import RxSwift 9 | import RxCocoa 10 | 11 | final class ProfileEditNicknameModalViewModel { 12 | 13 | private let currentUser: CurrentUser = UserDefaultsManager.shared.getUserData() 14 | private var currentChuCount = 0 15 | 16 | struct Input { 17 | /// 사용한 츄 18 | let consumeChuCount: Observable 19 | } 20 | 21 | struct Output { 22 | let resultPurchase: Observable 23 | } 24 | 25 | func transform(input: Input) -> Output { 26 | let responsePurchase = input.consumeChuCount 27 | .withUnretained(self) 28 | .flatMap { viewModel, _ in 29 | Loading.showLoading() 30 | viewModel.currentChuCount = 31 | UserDefaultsManager.shared.getChuCount() - 50 32 | return FirestoreService.shared.updateDocumentRx(collectionId: .users, documentId: viewModel.currentUser.userId, field: "chuCount", data: viewModel.currentChuCount) 33 | .flatMap { _ -> Observable in 34 | let payment: Payment.PaymentInfo = Payment.PaymentInfo(price: 0, purchaseChuCount: -50, paymentType: .changeNickname) 35 | return FirestoreService.shared.saveDocumentRx(collectionId: .payment, documentId: viewModel.currentUser.userId, fieldId: "paymentInfos", data: payment) 36 | } 37 | } 38 | .withUnretained(self) 39 | .map { viewModel, _ in 40 | UserDefaultsManager.shared.updateChuCount(viewModel.currentChuCount) 41 | return Loading.hideLoading() 42 | } 43 | return Output( 44 | resultPurchase: responsePurchase 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Pico/Mypage/ProfileEdit/ViewModel/SectionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionModel.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/10/11. 6 | // 7 | 8 | import Foundation 9 | import RxDataSources 10 | 11 | struct SectionModel { 12 | var items: [Item] 13 | } 14 | 15 | extension SectionModel: SectionModelType { 16 | init(original: SectionModel, items: [Item]) { 17 | self = original 18 | self.items = items 19 | } 20 | } 21 | 22 | enum Item { 23 | case profileEditImageTableCell(images: [String]) 24 | case profileEditNicknameTabelCell 25 | case profileEditLoactionTabelCell(location: String) 26 | case profileEditTextTabelCell(title: String, content: String?) 27 | } 28 | -------------------------------------------------------------------------------- /Pico/Mypage/Setting/Cell/SettingPrivateTableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingPrivateTableCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | 10 | final class SettingPrivateTableCell: UITableViewCell { 11 | 12 | private let contentLabel: UILabel = { 13 | let label = UILabel() 14 | label.textColor = .picoAlphaWhite 15 | label.font = UIFont.picoSubTitleFont 16 | label.text = "아는 사람 만나지 않기" 17 | return label 18 | }() 19 | 20 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 21 | super.init(style: style, reuseIdentifier: reuseIdentifier) 22 | addSubView() 23 | makeConstraints() 24 | configView() 25 | } 26 | 27 | @available(*, unavailable) 28 | required init?(coder: NSCoder) { 29 | fatalError("init(coder:) has not been implemented") 30 | } 31 | 32 | override func setSelected(_ selected: Bool, animated: Bool) { 33 | super.setSelected(selected, animated: animated) 34 | 35 | // Configure the view for the selected state 36 | } 37 | 38 | private func configView() { 39 | contentView.backgroundColor = .picoBlue 40 | } 41 | 42 | func configure(contentLabel: String) { 43 | self.contentLabel.text = contentLabel 44 | } 45 | 46 | private func addSubView() { 47 | [contentLabel].forEach { 48 | contentView.addSubview($0) 49 | } 50 | } 51 | 52 | private func makeConstraints() { 53 | contentLabel.snp.makeConstraints { make in 54 | make.center.equalToSuperview() 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Pico/Mypage/Setting/Cell/SettingTableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreTableCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | 10 | final class SettingTableCell: UITableViewCell { 11 | 12 | private let contentLabel: UILabel = { 13 | let label = UILabel() 14 | label.textColor = .picoFontBlack 15 | label.font = UIFont.picoSubTitleFont 16 | label.text = "약관 내용" 17 | return label 18 | }() 19 | 20 | private let nextImageView: UIImageView = { 21 | let imageView = UIImageView() 22 | imageView.image = UIImage(systemName: "chevron.right") 23 | imageView.tintColor = .lightGray 24 | imageView.contentMode = .scaleAspectFit 25 | return imageView 26 | }() 27 | 28 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 29 | super.init(style: style, reuseIdentifier: reuseIdentifier) 30 | addSubView() 31 | makeConstraints() 32 | } 33 | 34 | @available(*, unavailable) 35 | required init?(coder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | override func setSelected(_ selected: Bool, animated: Bool) { 40 | super.setSelected(selected, animated: animated) 41 | 42 | // Configure the view for the selected state 43 | } 44 | 45 | func configure(contentLabel: String, isHiddenNextImage: Bool = false) { 46 | self.contentLabel.text = contentLabel 47 | if isHiddenNextImage == true { 48 | nextImageView.isHidden = true 49 | } 50 | } 51 | 52 | private func addSubView() { 53 | [contentLabel, nextImageView].forEach { 54 | contentView.addSubview($0) 55 | } 56 | } 57 | 58 | private func makeConstraints() { 59 | contentLabel.snp.makeConstraints { make in 60 | make.centerY.equalToSuperview() 61 | make.leading.equalToSuperview().offset(15) 62 | make.trailing.equalToSuperview().offset(-15) 63 | } 64 | nextImageView.snp.makeConstraints { make in 65 | make.trailing.equalToSuperview().offset(-15) 66 | make.centerY.equalToSuperview() 67 | make.width.height.equalTo(20) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Pico/Mypage/Setting/SettingDetail/SettingLicenseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingLicenseViewController.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/10/19. 6 | // 7 | 8 | import SwiftUI 9 | import SnapKit 10 | 11 | final class SettingLicenseViewController: UIViewController { 12 | 13 | private let settingLicenseView = SettingLicenseView() 14 | private lazy var hostingController = UIHostingController(rootView: settingLicenseView) 15 | 16 | private lazy var closeButton: UIButton = { 17 | let button = UIButton(type: .custom) 18 | if let symbolImage = UIImage(systemName: "xmark.circle")?.withRenderingMode(.alwaysTemplate) { 19 | button.setImage(symbolImage, for: .normal) 20 | } 21 | button.tintColor = .picoFontGray 22 | let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: 25, weight: .regular) 23 | button.setPreferredSymbolConfiguration(symbolConfiguration, forImageIn: .normal) 24 | button.addTarget(self, action: #selector(tappedCloseButton), for: .touchUpInside) 25 | return button 26 | }() 27 | 28 | private let titleLabel: UILabel = { 29 | let label = UILabel() 30 | label.text = "오픈소스 라이센스" 31 | label.font = UIFont.boldSystemFont(ofSize: 25) 32 | label.textColor = .picoFontBlack 33 | return label 34 | }() 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | viewConfig() 39 | addSubView() 40 | makeConstraints() 41 | } 42 | 43 | private func viewConfig() { 44 | view.configBackgroundColor() 45 | } 46 | 47 | private func addSubView() { 48 | addChild(hostingController) 49 | view.addSubview([titleLabel, closeButton, hostingController.view]) 50 | hostingController.didMove(toParent: self) 51 | } 52 | 53 | private func makeConstraints() { 54 | let safeArea = view.safeAreaLayoutGuide 55 | 56 | titleLabel.snp.makeConstraints { make in 57 | make.top.equalTo(safeArea).offset(30) 58 | make.centerX.equalTo(safeArea) 59 | } 60 | 61 | closeButton.snp.makeConstraints { make in 62 | make.top.equalTo(titleLabel) 63 | make.trailing.equalTo(safeArea).offset(-15) 64 | } 65 | 66 | hostingController.view.snp.makeConstraints { make in 67 | make.top.equalTo(titleLabel.snp.bottom).offset(15) 68 | make.leading.trailing.bottom.equalToSuperview() 69 | } 70 | } 71 | @objc private func tappedCloseButton() { 72 | dismiss(animated: true, completion: nil) 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /Pico/Mypage/Setting/SettingDetail/SettingSecessionViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingSecessionViewModel.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/10/19. 6 | // 7 | 8 | import RxSwift 9 | import RxCocoa 10 | 11 | final class SettingSecessionViewModel: ViewModelType { 12 | 13 | let userId = UserDefaultsManager.shared.getUserData().userId 14 | 15 | struct Input { 16 | let isUnsubscribe: Observable 17 | } 18 | 19 | struct Output { 20 | let resultIsUnsubscribe: Observable 21 | } 22 | 23 | func transform(input: Input) -> Output { 24 | let responseUnsubscribe = input.isUnsubscribe 25 | .withUnretained(self) 26 | .flatMap { model, _ in 27 | FirestoreService.shared 28 | .loadDocumentRx(collectionId: .users, documentId: model.userId, dataType: User.self) 29 | .flatMap { data -> Observable in 30 | return Observable.combineLatest( 31 | model.saveData(data: data), 32 | model.deleteData() 33 | ).map { _, _ in 34 | return Void() 35 | } 36 | } 37 | } 38 | 39 | return Output(resultIsUnsubscribe: responseUnsubscribe) 40 | } 41 | 42 | private func saveData(data: Codable) -> Observable { 43 | return FirestoreService.shared.saveDocumentRx(collectionId: .unsubscribe, documentId: userId, data: data) 44 | .asObservable() 45 | } 46 | 47 | private func deleteData() -> Observable { 48 | return FirestoreService.shared.deleteDocumentRx(collectionId: .users, documentId: userId) 49 | .asObservable() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Pico/Mypage/Setting/SettingTableHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingTableHeaderView.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | 10 | class SettingTableHeaderView: UITableViewHeaderFooterView { 11 | 12 | static let identifier = "SettingTableHeaderView" 13 | private let headerLabel: UILabel = { 14 | let label = UILabel() 15 | label.font = .picoDescriptionFont 16 | label.textColor = .picoFontGray 17 | return label 18 | }() 19 | 20 | override init(reuseIdentifier: String?) { 21 | super.init(reuseIdentifier: reuseIdentifier) 22 | addSubView() 23 | makeConstraints() 24 | } 25 | 26 | @available(*, unavailable) 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | func configure(headerLabel: String) { 32 | self.headerLabel.text = headerLabel 33 | } 34 | 35 | private func addSubView() { 36 | [headerLabel].forEach { 37 | contentView.addSubview($0) 38 | } 39 | } 40 | 41 | private func makeConstraints() { 42 | headerLabel.snp.makeConstraints { make in 43 | make.centerY.equalToSuperview() 44 | make.leading.equalToSuperview().offset(15) 45 | make.trailing.equalToSuperview().offset(-15) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Pico/Mypage/Store/StoreModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreModel.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 10/17/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct StoreModel { 11 | let count: Int 12 | let price: Int 13 | let discount: Int? 14 | } 15 | -------------------------------------------------------------------------------- /Pico/Mypage/Store/StoreTableBannerCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreTableBannerCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | import Lottie 11 | 12 | final class StoreTableBannerCell: UITableViewCell { 13 | 14 | private let boxTitleImage: UIImageView = { 15 | let imageView = UIImageView() 16 | imageView.contentMode = .scaleAspectFit 17 | imageView.image = UIImage(named: "randomBoxImage") 18 | return imageView 19 | }() 20 | 21 | private let boxContentLabel: UILabel = { 22 | let label = UILabel() 23 | label.textAlignment = .center 24 | label.font = UIFont.picoDescriptionFont 25 | label.text = "꽝은 절대 없다!\n랜덤박스를 열어 부족한 츄를 획득해보세요!" 26 | label.numberOfLines = 0 27 | return label 28 | }() 29 | 30 | let animationView = LottieAnimationView(name: "randomBox") 31 | 32 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 33 | super.init(style: style, reuseIdentifier: reuseIdentifier) 34 | addSubView() 35 | makeConstraints() 36 | } 37 | 38 | @available(*, unavailable) 39 | required init?(coder: NSCoder) { 40 | fatalError("init(coder:) has not been implemented") 41 | } 42 | 43 | private func configStyle() { 44 | self.layer.borderWidth = 1.0 45 | self.layer.cornerRadius = 10 46 | self.layer.borderColor = UIColor.picoBlue.cgColor 47 | } 48 | 49 | private func addSubView() { 50 | addSubview([boxTitleImage, boxContentLabel]) 51 | } 52 | 53 | private func makeConstraints() { 54 | 55 | boxTitleImage.snp.makeConstraints { make in 56 | make.top.equalToSuperview().offset(5) 57 | make.leading.trailing.equalToSuperview().inset(50) 58 | make.height.equalTo(50) 59 | } 60 | boxContentLabel.snp.makeConstraints { make in 61 | make.top.equalTo(boxTitleImage.snp.bottom).offset(5) 62 | make.leading.trailing.equalToSuperview().inset(20) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Pico/Mypage/Store/StoreTableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreTableCell.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | final class StoreTableCell: UITableViewCell { 12 | 13 | private let tableImageView: UIImageView = { 14 | let imageView = UIImageView() 15 | imageView.image = UIImage(named: "chu") 16 | return imageView 17 | }() 18 | private let countLabel: UILabel = { 19 | let label = UILabel() 20 | label.textColor = .picoFontBlack 21 | label.font = UIFont.picoTitleFont 22 | return label 23 | }() 24 | private let priceLabel: UILabel = { 25 | let label = UILabel() 26 | label.textColor = .picoFontBlack 27 | label.font = UIFont.picoSubTitleFont 28 | return label 29 | }() 30 | private let discountLabel: UILabel = { 31 | let label = UILabel() 32 | label.textColor = .red 33 | label.font = UIFont.picoDescriptionFont 34 | return label 35 | }() 36 | 37 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 38 | super.init(style: style, reuseIdentifier: reuseIdentifier) 39 | addSubView() 40 | makeConstraints() 41 | } 42 | 43 | @available(*, unavailable) 44 | required init?(coder: NSCoder) { 45 | fatalError("init(coder:) has not been implemented") 46 | } 47 | 48 | override func setSelected(_ selected: Bool, animated: Bool) { 49 | super.setSelected(selected, animated: animated) 50 | 51 | // Configure the view for the selected state 52 | } 53 | 54 | func configure(_ storeModel: StoreModel) { 55 | countLabel.text = "X \(storeModel.count.formattedSeparator())" 56 | priceLabel.text = "\(storeModel.price.formattedSeparator()) 원" 57 | guard let discount = storeModel.discount else { return } 58 | discountLabel.text = "- \(discount.formattedSeparator())%" 59 | } 60 | 61 | private func addSubView() { 62 | [tableImageView, countLabel, priceLabel, discountLabel].forEach { 63 | contentView.addSubview($0) 64 | } 65 | } 66 | 67 | private func makeConstraints() { 68 | tableImageView.snp.makeConstraints { make in 69 | make.centerY.equalToSuperview() 70 | make.leading.equalToSuperview().offset(10) 71 | make.height.width.equalTo(80) 72 | } 73 | countLabel.snp.makeConstraints { make in 74 | make.centerY.equalToSuperview() 75 | make.leading.equalTo(tableImageView.snp.trailing) 76 | make.trailing.equalToSuperview().offset(-10) 77 | } 78 | priceLabel.snp.makeConstraints { make in 79 | make.trailing.equalToSuperview().offset(-10) 80 | make.bottom.equalToSuperview().offset(-15) 81 | } 82 | discountLabel.snp.makeConstraints { make in 83 | make.trailing.equalTo(priceLabel.snp.trailing) 84 | make.bottom.equalTo(priceLabel.snp.top).offset(-10) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Pico/Mypage/Store/StoreViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreViewModel.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 10/17/23. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | 11 | final class StoreViewModel: ViewModelType { 12 | private let currentUser: CurrentUser = UserDefaultsManager.shared.getUserData() 13 | let storeModels: [StoreModel] = [ 14 | StoreModel(count: 10, price: 1100, discount: nil), 15 | StoreModel(count: 50, price: 5500, discount: nil), 16 | StoreModel(count: 100, price: 11000, discount: nil), 17 | StoreModel(count: 500, price: 50000, discount: 10), 18 | StoreModel(count: 1000, price: 88000, discount: 20) 19 | ] 20 | 21 | private var currentChuCount = 0 22 | 23 | struct Input { 24 | /// 구매한 츄 25 | let purchaseChuCount: Observable 26 | /// 사용한 츄 27 | let consumeChuCount: Observable 28 | } 29 | 30 | struct Output { 31 | let resultPurchase: Observable 32 | } 33 | 34 | func transform(input: Input) -> Output { 35 | let responsePurchase = input.purchaseChuCount 36 | .withUnretained(self) 37 | .flatMap { viewModel, storeModel in 38 | viewModel.currentChuCount = storeModel.count + UserDefaultsManager.shared.getChuCount() 39 | return FirestoreService.shared.updateDocumentRx(collectionId: .users, documentId: viewModel.currentUser.userId, field: "chuCount", data: viewModel.currentChuCount) 40 | .flatMap { _ -> Observable in 41 | let payment: Payment.PaymentInfo = Payment.PaymentInfo(price: storeModel.price, purchaseChuCount: storeModel.count, paymentType: .purchase) 42 | return FirestoreService.shared.saveDocumentRx(collectionId: .payment, documentId: viewModel.currentUser.userId, fieldId: "paymentInfos", data: payment) 43 | } 44 | } 45 | .withUnretained(self) 46 | .map { viewModel, _ in 47 | return UserDefaultsManager.shared.updateChuCount(viewModel.currentChuCount) 48 | } 49 | return Output( 50 | resultPurchase: responsePurchase 51 | ) 52 | } 53 | 54 | private func savePayment() { 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Pico/Mypage/ViewModel/CircularProgressBarViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircularProgressBarViewModel.swift 3 | // Pico 4 | // 5 | // Created by 김민기 on 2023/10/05. 6 | // 7 | 8 | import RxSwift 9 | import RxCocoa 10 | 11 | class CircularProgressBarViewModel { 12 | 13 | let profilePerfection = BehaviorRelay(value: 0.0) 14 | 15 | init() { 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Pico/Pico.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /Pico/Service/KakaoAuthService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KakaoAuthService.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2/20/24. 6 | // 7 | 8 | import Foundation 9 | import KakaoSDKCommon 10 | import KakaoSDKTemplate 11 | import KakaoSDKShare 12 | 13 | enum KakaoLinkType { 14 | case app(url: URL) 15 | case web(url: URL) 16 | case err 17 | } 18 | 19 | final class KakaoAuthService { 20 | static let shared: KakaoAuthService = KakaoAuthService() 21 | 22 | private var randomNumber = "" 23 | 24 | func sendVerificationCode(phoneNumber: String, completion: @escaping (KakaoLinkType) -> ()) { 25 | guard phoneNumber != Bundle.main.testPhoneNumber else { 26 | randomNumber = Bundle.main.testAuthNum 27 | return 28 | } 29 | 30 | DispatchQueue.global().async { 31 | self.kakaoAuth { kakaoLinkType in 32 | completion(kakaoLinkType) 33 | } 34 | } 35 | } 36 | 37 | func checkRandomNumber(number: String) -> Bool { 38 | return randomNumber == number ? true : false 39 | } 40 | 41 | private func configRandomNumber() -> String { 42 | let str = (0..<6).map { _ in "0123456789".randomElement()! } 43 | 44 | randomNumber = String(str) 45 | return String(str) 46 | } 47 | 48 | private func kakaoAuth(completion: @escaping (KakaoLinkType) -> ()) { 49 | let link = Link(mobileWebUrl: URL(string: "https://developers.kakao.com")) 50 | let appLink = Link(androidExecutionParams: ["key1": "value1", "key2": "value2"], 51 | iosExecutionParams: ["key1": "value1", "key2": "value2"]) 52 | 53 | let appButton = Button(title: "앱으로 보기", link: appLink) 54 | 55 | guard let imageUrl = URL(string: Defaults.logoImageURLString) else { return } 56 | let content = Content(title: """ 57 | 인증 코드를 발급해드립니다. 58 | 인증번호는 "\(configRandomNumber())" 입니다. 59 | 해당 인증코드를 입력해주세요. 60 | 타인에게 절대 알려주지 마세요. 61 | """, 62 | imageUrl: imageUrl, 63 | imageHeight: 50, 64 | link: link) 65 | 66 | let template = FeedTemplate(content: content, buttons: [appButton]) 67 | 68 | if let templateJsonData = (try? SdkJSONEncoder.custom.encode(template)) { 69 | if let templateJsonObject = SdkUtils.toJsonObject(templateJsonData) { 70 | if ShareApi.isKakaoTalkSharingAvailable() { 71 | ShareApi.shared.shareDefault(templateObject: templateJsonObject) {(linkResult, error) in 72 | if let error = error { 73 | print("error : \(error)") 74 | completion(.err) 75 | } 76 | else { 77 | print("defaultLink(templateObject:templateJsonObject) success.") 78 | guard let linkResult = linkResult else { return } 79 | completion(.app(url: linkResult.url)) 80 | } 81 | } 82 | 83 | } else { 84 | print("카카오톡 미설치") 85 | if let url = ShareApi.shared.makeDefaultUrl(templateObject: templateJsonObject) { 86 | completion(.web(url: url)) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Pico/Service/KeyboardService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardService.swift 3 | // Pico 4 | // 5 | // Created by LJh on 10/16/23. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | final class KeyboardService { 12 | private var button: UIButton? 13 | 14 | func registerKeyboard(with button: UIButton) { 15 | self.button = button 16 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardUp), name: UIResponder.keyboardWillShowNotification, object: nil) 17 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardDown), name: UIResponder.keyboardWillHideNotification, object: nil) 18 | } 19 | 20 | func unregisterKeyboard() { 21 | NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) 22 | NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) 23 | 24 | button = nil 25 | } 26 | 27 | @objc private func keyboardUp(_ notification: NSNotification) { 28 | if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, 29 | let button = button { 30 | let keyboardRectangle = keyboardFrame.cgRectValue 31 | 32 | UIView.animate( 33 | withDuration: 0.5, 34 | animations: { 35 | button.transform = CGAffineTransform(translationX: 0, y: -keyboardRectangle.height + 25) 36 | } 37 | ) 38 | } 39 | } 40 | 41 | @objc private func keyboardDown(_ notification: NSNotification) { 42 | if let button = button { 43 | button.transform = .identity 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Pico/Service/PictureService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PictureService.swift 3 | // Pico 4 | // 5 | // Created by LJh on 10/16/23. 6 | // 7 | import UIKit 8 | import Photos 9 | 10 | final class PictureService { 11 | func unauthorized(in viewController: UIViewController) { 12 | DispatchQueue.main.async { 13 | viewController.showCustomAlert(alertType: .canCancel, titleText: "사진 라이브러리 권한 필요", messageText: "사진을 선택하려면 사진 라이브러리 권한이 필요합니다. 설정에서 권한을 변경할 수 있습니다.", confirmButtonText: "설정으로 이동", comfrimAction: { 14 | if let settingsURL = URL(string: UIApplication.openSettingsURLString) { 15 | UIApplication.shared.open(settingsURL) 16 | } 17 | }) 18 | } 19 | } 20 | 21 | func requestPhotoLibraryAccess(in viewController: UIViewController) { 22 | PHPhotoLibrary.requestAuthorization { [weak self] status in 23 | guard let self = self else { return } 24 | switch status { 25 | case .authorized: 26 | break 27 | case .denied, .restricted, .notDetermined, .limited: 28 | self.unauthorized(in: viewController) 29 | @unknown default: 30 | self.unauthorized(in: viewController) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Pico/Service/StorageService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageService.swift 3 | // Pico 4 | // 5 | // Created by LJh on 10/6/23. 6 | // 7 | 8 | import UIKit 9 | import RxSwift 10 | import Foundation 11 | import FirebaseStorage 12 | import FirebaseFirestore 13 | import FirebaseFirestoreSwift 14 | 15 | final class StorageService { 16 | static let shared: StorageService = StorageService() 17 | private let storageRef = Storage.storage() 18 | func uploadImages(images: [UIImage], userId: String) -> Observable<[String]> { 19 | return Observable.create { observer in 20 | Task.init { 21 | var urlStrings: [String] = [] 22 | 23 | for (index, image) in images.enumerated() { 24 | guard let imageData = image.jpegData(compressionQuality: 0.5) else { continue } 25 | let imageRef = self.storageRef.reference().child("userImage/\(userId)/image\(index)") 26 | 27 | do { 28 | _ = try await imageRef.putDataAsync(imageData) 29 | let url = try await imageRef.downloadURL() 30 | urlStrings.append(url.absoluteString) 31 | } catch { 32 | observer.onError(error) 33 | return 34 | } 35 | } 36 | 37 | observer.onNext(urlStrings) 38 | observer.onCompleted() 39 | } 40 | 41 | return Disposables.create() 42 | } 43 | } 44 | func getUrlStrings(images: [UIImage], userId: String) -> Observable<[String]> { 45 | let urlStrings = uploadImages(images: images, userId: userId) 46 | 47 | return urlStrings 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Pico/Service/VersionService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionService.swift 3 | // Pico 4 | // 5 | // Created by LJh on 12/16/23. 6 | // 7 | 8 | import Foundation 9 | 10 | final class VersionService { 11 | static let shared: VersionService = VersionService() 12 | 13 | var isOldVersion: Bool = false 14 | 15 | private let appleID = "6473959557" 16 | private let bundleID = "com.ojeomsun.pico.dev" 17 | lazy var appStoreOpenUrlString = "itms-apps://itunes.apple.com/app/apple-store/\(appleID)" 18 | 19 | func loadAppStoreVersion(completion: @escaping (String?) -> Void) { 20 | let appStoreUrl = "http://itunes.apple.com/kr/lookup?bundleId=\(bundleID)" 21 | 22 | let task = URLSession.shared.dataTask(with: URL(string: appStoreUrl)!) { data, _, error in 23 | guard let data = data, error == nil else { 24 | completion(nil) 25 | return 26 | } 27 | 28 | do { 29 | if let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any], 30 | let results = json["results"] as? [[String: Any]], 31 | let appStoreVersion = results[0]["version"] as? String { 32 | completion(appStoreVersion) 33 | } else { 34 | completion(nil) 35 | } 36 | } catch { 37 | completion(nil) 38 | } 39 | } 40 | task.resume() 41 | } 42 | 43 | func nowVersion() -> String { 44 | let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" 45 | 46 | return version 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Pico/Service/VisionService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VisionService.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 12/15/23. 6 | // 7 | 8 | import CoreML 9 | import Vision 10 | import UIKit 11 | 12 | final class VisionService { 13 | private var faceDetectionRequest: VNDetectFaceRectanglesRequest? 14 | 15 | init() { 16 | faceDetectionRequest = VNDetectFaceRectanglesRequest { [weak self] request, error in 17 | guard let self else { return } 18 | _ = handleFaceDetectionResults(request: request, error: error) 19 | } 20 | } 21 | 22 | func handleFaceDetectionResults(request: VNRequest, error: Error?) -> Bool { 23 | guard let results = request.results as? [VNFaceObservation] else { return false } 24 | print("faceCount : \(results.count)") 25 | guard results.isEmpty else { 26 | return true 27 | } 28 | return false 29 | } 30 | 31 | func detectFaces(image: UIImage, completion: @escaping (Bool) -> ()) { 32 | guard let ciImage = CIImage(image: image) else { 33 | completion(false) 34 | return 35 | } 36 | 37 | let request = VNDetectFaceRectanglesRequest { res, err in 38 | completion(self.handleFaceDetectionResults(request: res, error: err)) 39 | } 40 | #if targetEnvironment(simulator) 41 | request.usesCPUOnly = true 42 | #endif 43 | let handler = VNImageRequestHandler(ciImage: ciImage, options: [:]) 44 | 45 | do { 46 | try handler.perform([request]) 47 | } catch { 48 | print("Failed to perform face detection: \(error)") 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Pico/Sign/LocationService/LocationService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationService.swift 3 | // Pico 4 | // 5 | // Created by LJh on 10/5/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class LocationService { 11 | 12 | static var shared = LocationService() 13 | var longitude: Double! 14 | var latitude: Double! 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Pico/Sign/SignIn/ViewModel/SignInViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingInViewModel.swift 3 | // Pico 4 | // 5 | // Created by LJh on 10/10/23. 6 | // 7 | 8 | import Foundation 9 | import FirebaseFirestore 10 | import FirebaseFirestoreSwift 11 | 12 | final class SignInViewModel { 13 | private let dbRef = Firestore.firestore() 14 | var loginUser: User? 15 | var isRightUser = false 16 | 17 | func signIn(userNumber: String, completion: @escaping (User?, String) -> ()) { 18 | let phoneNumberRegex = "^01[0-9]{1}-?[0-9]{3,4}-?[0-9]{4}$" 19 | let phoneNumberPredicate = NSPredicate(format: "SELF MATCHES %@", phoneNumberRegex) 20 | 21 | if !phoneNumberPredicate.evaluate(with: userNumber) { 22 | completion(nil, "유효하지 않은 전화번호 형식입니다.") 23 | return 24 | } 25 | Loading.showLoading() 26 | self.isRightUser = false 27 | 28 | DispatchQueue.global().async { [weak self] in 29 | guard let self else { return } 30 | 31 | dbRef.collection("users").whereField("phoneNumber", isEqualTo: userNumber).getDocuments { [weak self] snapShot, err in 32 | guard let self else { return } 33 | 34 | guard err == nil, let documents = snapShot?.documents else { 35 | print(err ?? "서버오류 비상비상") 36 | isRightUser = false 37 | return 38 | } 39 | 40 | guard let document = documents.first else { 41 | isRightUser = false 42 | completion(nil, "일치하는 번호가 없습니다.") 43 | return 44 | } 45 | 46 | guard let retrievedUser = try? document.data(as: User.self) else { 47 | isRightUser = false 48 | completion(nil, "데이터 형식이 다름.") 49 | return 50 | } 51 | 52 | isRightUser = true 53 | loginUser = retrievedUser 54 | completion(retrievedUser, "인증번호를 입력해주세요!") 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Pico/Sign/SignUp/SignUpCell/SignUpPictureEditCollectionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpPictureEditCollectionCell.swift 3 | // Pico 4 | // 5 | // Created by LJh on 10/15/23. 6 | // 7 | 8 | import UIKit 9 | 10 | final class SignUpPictureEditCollectionCell: UICollectionViewCell { 11 | 12 | private let imageView: UIImageView = { 13 | let imageView = UIImageView() 14 | imageView.image = .add 15 | imageView.contentMode = .scaleAspectFill 16 | return imageView 17 | }() 18 | 19 | private let plusImageView: UIImageView = { 20 | let imageView = UIImageView() 21 | let imageConfig = UIImage.SymbolConfiguration(pointSize: 50, weight: .light) 22 | let image = UIImage(systemName: "plus.circle", withConfiguration: imageConfig) 23 | imageView.image = image 24 | imageView.tintColor = .white.withAlphaComponent(0.8) 25 | return imageView 26 | }() 27 | 28 | override init(frame: CGRect) { 29 | super.init(frame: frame) 30 | addSubView() 31 | makeConstraints() 32 | } 33 | 34 | @available(*, unavailable) 35 | required init?(coder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | override func prepareForReuse() { 40 | imageView.image = UIImage(systemName: "plus") 41 | } 42 | 43 | func configure(imageName: String, isHidden: Bool) { 44 | plusImageView.isHidden = isHidden 45 | guard let url = URL(string: imageName) else { return } 46 | imageView.kf.setImage(with: url) 47 | imageView.contentMode = .scaleAspectFill 48 | } 49 | 50 | func configure(image: UIImage) { 51 | imageView.image = image 52 | } 53 | 54 | func cellConfigure() { 55 | contentView.backgroundColor = .picoLightGray 56 | contentView.layer.masksToBounds = true 57 | contentView.layer.cornerRadius = 10 58 | } 59 | 60 | private func addSubView() { 61 | [imageView, plusImageView].forEach { 62 | contentView.addSubview($0) 63 | } 64 | } 65 | 66 | private func makeConstraints() { 67 | imageView.snp.makeConstraints { make in 68 | make.edges.equalToSuperview() 69 | } 70 | 71 | plusImageView.snp.makeConstraints { make in 72 | make.centerX.equalTo(imageView.snp.centerX) 73 | make.centerY.equalTo(imageView.snp.centerY) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Pico/Sign/SignUp/TermsOfServiceText/TermsOfServiceModalViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TermsOfServiceModalViewController.swift 3 | // Pico 4 | // 5 | // Created by LJh on 10/21/23. 6 | // 7 | 8 | import UIKit 9 | 10 | final class TermsOfServiceModalViewController: UIViewController { 11 | private let tag: Int 12 | private let termsOfServiceTexts: [String] = TermsOfServiceText.termsOfServiceTexts 13 | private let termTitle = TermsOfServiceText.termsTitle 14 | init(tag: Int) { 15 | self.tag = tag 16 | super.init(nibName: nil, bundle: nil) 17 | setupUI() 18 | configButton() 19 | titleLabel.text = termTitle[tag] 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | private lazy var textView: UITextView = { 27 | let textView = UITextView() 28 | textView.isEditable = false 29 | textView.isScrollEnabled = true 30 | textView.text = termsOfServiceTexts[tag] 31 | textView.font = UIFont.systemFont(ofSize: 16) 32 | textView.textColor = .picoFontBlack 33 | textView.backgroundColor = .picoAlphaWhite 34 | textView.showsVerticalScrollIndicator = false 35 | return textView 36 | }() 37 | 38 | private lazy var closeButton: UIButton = { 39 | let button = UIButton(type: .custom) 40 | if let symbolImage = UIImage(systemName: "xmark.circle")?.withRenderingMode(.alwaysTemplate) { 41 | button.setImage(symbolImage, for: .normal) 42 | } 43 | button.tintColor = .picoFontGray 44 | let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: 25, weight: .regular) 45 | button.setPreferredSymbolConfiguration(symbolConfiguration, forImageIn: .normal) 46 | return button 47 | }() 48 | 49 | private let titleLabel: UILabel = { 50 | let label = UILabel() 51 | label.text = "서비스 이용약관" 52 | label.font = UIFont.boldSystemFont(ofSize: 25) 53 | label.textColor = .picoFontBlack 54 | return label 55 | }() 56 | 57 | private func configButton() { 58 | closeButton.addTarget(self, action: #selector(tappedCloseButton), for: .touchUpInside) 59 | } 60 | 61 | @objc private func tappedCloseButton() { 62 | dismiss(animated: true, completion: nil) 63 | } 64 | } 65 | 66 | extension TermsOfServiceModalViewController { 67 | private func setupUI() { 68 | let safeArea = view.safeAreaLayoutGuide 69 | view.configBackgroundColor(color: .systemBackground) 70 | view.addSubview(closeButton) 71 | view.addSubview(textView) 72 | view.addSubview(titleLabel) 73 | 74 | titleLabel.snp.makeConstraints { make in 75 | make.top.equalTo(safeArea).offset(30) 76 | make.centerX.equalTo(safeArea) 77 | } 78 | 79 | closeButton.snp.makeConstraints { make in 80 | make.top.equalTo(titleLabel) 81 | make.trailing.equalTo(safeArea).offset(-SignView.padding) 82 | } 83 | 84 | textView.snp.makeConstraints { make in 85 | make.top.equalTo(titleLabel.snp.bottom).offset(SignView.padding) 86 | make.leading.equalTo(safeArea).offset(SignView.padding) 87 | make.trailing.equalTo(safeArea).offset(-SignView.padding) 88 | make.bottom.equalTo(safeArea).offset(-SignView.padding) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Pico/Sign/SignUp/ViewModel/SignUpViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpViewModel.swift 3 | // Pico 4 | // 5 | // Created by LJh on 10/5/23. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | import RxRelay 11 | import UIKit 12 | import FirebaseFirestore 13 | import FirebaseFirestoreSwift 14 | 15 | final class SignUpViewModel { 16 | private let dbRef = Firestore.firestore() 17 | var imagesSubject: PublishSubject<[UIImage]> = PublishSubject() 18 | var urlStringsSubject: PublishSubject<[String]> = PublishSubject() 19 | var locationSubject: PublishSubject = PublishSubject() 20 | var isSaveSuccess: PublishSubject = PublishSubject() 21 | private let disposeBag = DisposeBag() 22 | 23 | var isRightPhoneNumber: Bool = false 24 | var isRightName: Bool = false 25 | private let id = UUID().uuidString 26 | var userMbti = "" 27 | private lazy var mbti: MBTIType = { 28 | guard let mbtiType = MBTIType(rawValue: userMbti.lowercased()) else { 29 | return .enfj 30 | } 31 | return mbtiType 32 | }() 33 | var phoneNumber: String = "" 34 | var gender: GenderType = .etc 35 | var birth: String = "" 36 | var nickName: String = "" 37 | var location: Location = Location(address: "", latitude: 0, longitude: 0) 38 | var imageArray: [UIImage] = [] 39 | var createdDate: Double = Date().timeIntervalSince1970 40 | var chuCount: Int = 0 41 | var isSubscribe: Bool = false 42 | var progressStatus: Float = 0.0 43 | 44 | private lazy var newUser: User = User(id: id, mbti: mbti, phoneNumber: phoneNumber, gender: gender, birth: birth, nickName: nickName, location: location, imageURLs: [""], createdDate: createdDate, subInfo: nil, reports: nil, blocks: nil, chuCount: chuCount, isSubscribe: isSubscribe, isOnline: false) 45 | 46 | init() { 47 | locationSubject.subscribe { [weak self] location in 48 | guard let self = self else { return } 49 | 50 | Loading.showLoading() 51 | newUser.location = location 52 | saveImage() 53 | } 54 | .disposed(by: disposeBag) 55 | 56 | imagesSubject 57 | .flatMap { images -> Observable<[String]> in 58 | return StorageService.shared.getUrlStrings(images: images, userId: self.id) 59 | } 60 | .subscribe(onNext: urlStringsSubject.onNext(_:)) 61 | .disposed(by: disposeBag) 62 | 63 | urlStringsSubject 64 | .subscribe { [weak self] strings in 65 | guard let self = self else { return } 66 | newUser.imageURLs = strings 67 | saveNewUser() 68 | Loading.hideLoading() 69 | }.disposed(by: disposeBag) 70 | } 71 | 72 | func animateProgressBar(progressView: UIProgressView, endPoint: Float) { 73 | let endStatus = endPoint * 0.143 74 | UIView.animate(withDuration: 3) { 75 | progressView.setProgress(endStatus, animated: true) 76 | } 77 | progressStatus = endStatus 78 | } 79 | 80 | func saveImage() { 81 | imagesSubject.onNext(imageArray) 82 | } 83 | 84 | func saveNewUser() { 85 | if newUser.phoneNumber == Bundle.main.testPhoneNumber { 86 | newUser.id = Bundle.main.testId 87 | } 88 | FirestoreService().saveDocumentRx(collectionId: .users, documentId: newUser.id, data: newUser) 89 | .subscribe(onNext: isSaveSuccess.onNext(_:)) 90 | .disposed(by: disposeBag) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Pico/UserDefaults/UserDefaultsManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsManager.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import Foundation 9 | 10 | final class UserDefaultsManager { 11 | enum Key: String, CaseIterable { 12 | case userId, nickName, mbti, imageURL, birth, phoneNumber 13 | case latitude, longitude 14 | case filterGender, filterMbti, filterDistance, filterAgeMin, filterAgeMax 15 | case chuCount 16 | case dontWatchAgain, minPoint, maxPoint 17 | } 18 | 19 | static let shared: UserDefaultsManager = UserDefaultsManager() 20 | 21 | func removeAll() { 22 | Key.allCases.forEach { 23 | UserDefaults.standard.removeObject(forKey: $0.rawValue) 24 | } 25 | } 26 | 27 | func setUserData(userData: User) { 28 | UserDefaults.standard.setValue(userData.id, forKey: Key.userId.rawValue) 29 | UserDefaults.standard.setValue(userData.nickName, forKey: Key.nickName.rawValue) 30 | UserDefaults.standard.setValue(userData.mbti.rawValue, forKey: Key.mbti.rawValue) 31 | 32 | if let imageURL = userData.imageURLs[safe: 0] { 33 | UserDefaults.standard.setValue(imageURL, forKey: Key.imageURL.rawValue) 34 | } 35 | UserDefaults.standard.setValue(userData.birth, forKey: Key.birth.rawValue) 36 | UserDefaults.standard.setValue(userData.location.latitude, forKey: Key.latitude.rawValue) 37 | UserDefaults.standard.setValue(userData.location.longitude, forKey: Key.longitude.rawValue) 38 | 39 | UserDefaults.standard.setValue(userData.chuCount, forKey: Key.chuCount.rawValue) 40 | UserDefaults.standard.setValue(userData.phoneNumber, forKey: Key.phoneNumber.rawValue) 41 | } 42 | 43 | func isLogin() -> Bool { 44 | UserDefaults.standard.string(forKey: Key.userId.rawValue) == nil ? false : true 45 | } 46 | 47 | func getUserData() -> CurrentUser { 48 | let userId = UserDefaults.standard.string(forKey: Key.userId.rawValue) ?? "없음" 49 | let nickName = UserDefaults.standard.string(forKey: Key.nickName.rawValue) ?? "없음" 50 | let mbti = UserDefaults.standard.string(forKey: Key.mbti.rawValue) ?? "없음" 51 | let imageURL = UserDefaults.standard.string(forKey: Key.imageURL.rawValue) ?? "없음" 52 | let birth = UserDefaults.standard.string(forKey: Key.birth.rawValue) ?? "없음" 53 | let latitude = UserDefaults.standard.double(forKey: Key.latitude.rawValue) 54 | let longitude = UserDefaults.standard.double(forKey: Key.longitude.rawValue) 55 | let phoneNumber = UserDefaults.standard.string(forKey: Key.phoneNumber.rawValue) ?? "" 56 | return CurrentUser(userId: userId, nickName: nickName, mbti: mbti, imageURL: imageURL, birth: birth, latitude: latitude, longitude: longitude, phoneNumber: phoneNumber) 57 | } 58 | 59 | func getChuCount() -> Int { 60 | return UserDefaults.standard.integer(forKey: Key.chuCount.rawValue) 61 | } 62 | 63 | func updateChuCount(_ chuCount: Int) { 64 | UserDefaults.standard.setValue(chuCount, forKey: Key.chuCount.rawValue) 65 | } 66 | 67 | func updateLastWorldCupTime(_ time: Date) { 68 | let key = "lastStartedTime_\(UserDefaultsManager.shared.getUserData().userId)" 69 | UserDefaults.standard.set(time, forKey: key) 70 | } 71 | 72 | func getLastWorldCupTime() -> Date? { 73 | let key = "lastStartedTime_\(UserDefaultsManager.shared.getUserData().userId)" 74 | return UserDefaults.standard.object(forKey: key) as? Date 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Pico/Utils/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 2023/09/26. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | /* 11 | 사용법: 상속해서 사용하시면 됩니다 12 | class MainViewController: BaseViewController { 13 | */ 14 | 15 | class BaseViewController: UIViewController { 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | configUI() 20 | } 21 | 22 | override func viewWillAppear(_ animated: Bool) { 23 | super.viewWillAppear(animated) 24 | configNavigationLogo() 25 | self.tabBarController?.tabBar.isHidden = false 26 | } 27 | 28 | override func viewWillDisappear(_ animated: Bool) { 29 | super.viewWillDisappear(animated) 30 | navigationItem.leftBarButtonItem = nil 31 | navigationItem.hidesBackButton = true 32 | } 33 | 34 | private func configUI() { 35 | view.configBackgroundColor() 36 | view.tappedDismissKeyboard() 37 | configNavigationBgColor() 38 | configNavigationBackButton() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Pico/Utils/ViewModelType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelType.swift 3 | // Pico 4 | // 5 | // Created by 최하늘 on 10/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ViewModelType { 11 | associatedtype Input 12 | associatedtype Output 13 | 14 | func transform(input: Input) -> Output 15 | } 16 | -------------------------------------------------------------------------------- /PicoTests/PicoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PicoTests.swift 3 | // PicoTests 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | @testable import Pico 9 | import XCTest 10 | 11 | final class StringExtensionTests: XCTestCase { 12 | var sut: String? 13 | var sut2: String? 14 | 15 | override func setUpWithError() throws { 16 | sut = "01000000000" 17 | sut2 = "2023-12-25" 18 | } 19 | 20 | override func tearDownWithError() throws { 21 | sut = nil 22 | sut2 = nil 23 | } 24 | 25 | func test_formattedTextFieldText_함수를_사용해서_전화번호_11자리에_대쉬가_추가되는지() throws { 26 | // let expectation = XCTestExpectation() 27 | // given - 어떤 환경에서 28 | var result = "" 29 | 30 | // when - 어떤 액션을 했을 때 31 | result = sut!.formattedTextFieldText() 32 | 33 | // then - 어떤 결과가 나오는지 34 | XCTAssertEqual(result, "010-0000-0000") 35 | // expectation.fulfill() 36 | // wait(for: [expectation], timeout: 3) 37 | } 38 | 39 | func test_timeAgoSinceDate_시간을_현_시간과_비교해서_얼마나_지났는지() throws { 40 | var result = "" 41 | let sut2DateDouble = sut2?.toDate().timeIntervalSince1970 42 | 43 | result = sut2DateDouble!.timeAgoSinceDate() 44 | XCTAssertEqual(result, "2일 전") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /PicoUITests/PicoUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PicoUITests.swift 3 | // PicoUITests 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import XCTest 9 | 10 | final class PicoUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /PicoUITests/PicoUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PicoUITestsLaunchTests.swift 3 | // PicoUITests 4 | // 5 | // Created by 최하늘 on 2023/09/25. 6 | // 7 | 8 | import XCTest 9 | 10 | final class PicoUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /사용자메뉴얼.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APP-iOS2/final-pico/6fe1e0a72743ed200ef3b2c97a5fa8c5ddb5f0ed/사용자메뉴얼.pdf --------------------------------------------------------------------------------