├── .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
--------------------------------------------------------------------------------