├── .DS_Store ├── .gitignore ├── Practice-JH ├── Podfile ├── Podfile.lock ├── Pods │ └── JJFloatingActionButton │ │ └── Sources │ │ └── JJFloatingActionButton+Animation.swift ├── Practice-JH.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── junholee.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── junholee.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── Practice-JH.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Practice-JH │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── cell_four.imageset │ │ ├── Contents.json │ │ ├── Group 293.png │ │ ├── Group 293@2x.png │ │ └── Group 293@3x.png │ ├── cell_one.imageset │ │ ├── Contents.json │ │ ├── Group 58.png │ │ ├── Group 58@2x.png │ │ └── Group 58@3x.png │ ├── cell_three.imageset │ │ ├── Contents.json │ │ ├── Group 61.png │ │ ├── Group 61@2x.png │ │ └── Group 61@3x.png │ ├── cell_two.imageset │ │ ├── Contents.json │ │ ├── Group 293.png │ │ ├── Group 293@2x.png │ │ └── Group 293@3x.png │ └── chatButton.imageset │ │ ├── Contents.json │ │ ├── chatButton.png │ │ ├── chatButton@2x.png │ │ └── chatButton@3x.png │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── LogoutViewController.swift │ ├── SceneDelegate.swift │ ├── ViewController.swift │ └── carousel │ ├── Carousel.storyboard │ ├── CarouselCVC │ ├── firstCVC.swift │ ├── secondCVC.swift │ └── thirdCVC.swift │ ├── CarouselContainerView.swift │ ├── CarouselLayout.swift │ └── CarouselVC.swift ├── Practice-YSB ├── FloatingButton │ ├── FloatingButton.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── FloatingButton.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── FloatingButton │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── BaseViewController.swift │ │ ├── Info.plist │ │ ├── SceneDelegate.swift │ │ └── ViewController.swift │ ├── Podfile │ └── Podfile.lock ├── Photo │ ├── Photo.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Photo │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── SceneDelegate.swift │ │ └── ViewController.swift ├── StickyHeader │ ├── Podfile │ ├── Podfile.lock │ ├── StickyHeader.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── StickyHeader.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── StickyHeader │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── CustomCVC.swift │ │ ├── CustomHeaderView.swift │ │ ├── Info.plist │ │ ├── SceneDelegate.swift │ │ └── ViewController.swift ├── Stopwatch │ ├── Podfile │ ├── Podfile.lock │ ├── Stopwatch.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Stopwatch.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Stopwatch │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── SceneDelegate.swift │ │ ├── Stopwatch.swift │ │ └── ViewController.swift └── socialLogin │ ├── Podfile │ ├── Podfile.lock │ ├── socialLogin.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ ├── socialLogin.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── socialLogin │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── SceneDelegate.swift │ ├── ViewController.swift │ └── socialLogin.entitlements └── README.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/macos,xcode,cocoapods,swift 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,xcode,cocoapods,swift 4 | 5 | ### CocoaPods ### 6 | ## CocoaPods GitIgnore Template 7 | 8 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 9 | # - Also handy if you have a large number of dependant pods 10 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 11 | Pods/ 12 | 13 | ### macOS ### 14 | # General 15 | .DS_Store 16 | .AppleDouble 17 | .LSOverride 18 | 19 | # Icon must end with two \r 20 | Icon 21 | 22 | 23 | # Thumbnails 24 | ._* 25 | 26 | # Files that might appear in the root of a volume 27 | .DocumentRevisions-V100 28 | .fseventsd 29 | .Spotlight-V100 30 | .TemporaryItems 31 | .Trashes 32 | .VolumeIcon.icns 33 | .com.apple.timemachine.donotpresent 34 | 35 | # Directories potentially created on remote AFP share 36 | .AppleDB 37 | .AppleDesktop 38 | Network Trash Folder 39 | Temporary Items 40 | .apdisk 41 | 42 | ### Swift ### 43 | # Xcode 44 | # 45 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 46 | 47 | ## User settings 48 | xcuserdata/ 49 | 50 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 51 | *.xcscmblueprint 52 | *.xccheckout 53 | 54 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 55 | build/ 56 | DerivedData/ 57 | *.moved-aside 58 | *.pbxuser 59 | !default.pbxuser 60 | *.mode1v3 61 | !default.mode1v3 62 | *.mode2v3 63 | !default.mode2v3 64 | *.perspectivev3 65 | !default.perspectivev3 66 | 67 | ## Obj-C/Swift specific 68 | *.hmap 69 | 70 | ## App packaging 71 | *.ipa 72 | *.dSYM.zip 73 | *.dSYM 74 | 75 | ## Playgrounds 76 | timeline.xctimeline 77 | playground.xcworkspace 78 | 79 | # Swift Package Manager 80 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 81 | # Packages/ 82 | # Package.pins 83 | # Package.resolved 84 | # *.xcodeproj 85 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 86 | # hence it is not needed unless you have added a package configuration file to your project 87 | # .swiftpm 88 | 89 | .build/ 90 | 91 | # CocoaPods 92 | # We recommend against adding the Pods directory to your .gitignore. However 93 | # you should judge for yourself, the pros and cons are mentioned at: 94 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 95 | # Pods/ 96 | # Add this line if you want to avoid checking in source code from the Xcode workspace 97 | # *.xcworkspace 98 | 99 | # Carthage 100 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 101 | # Carthage/Checkouts 102 | 103 | Carthage/Build/ 104 | 105 | # Accio dependency management 106 | Dependencies/ 107 | .accio/ 108 | 109 | # fastlane 110 | # It is recommended to not store the screenshots in the git repo. 111 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 112 | # For more information about the recommended setup visit: 113 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 114 | 115 | fastlane/report.xml 116 | fastlane/Preview.html 117 | fastlane/screenshots/**/*.png 118 | fastlane/test_output 119 | 120 | # Code Injection 121 | # After new code Injection tools there's a generated folder /iOSInjectionProject 122 | # https://github.com/johnno1962/injectionforxcode 123 | 124 | iOSInjectionProject/ 125 | 126 | ### Xcode ### 127 | # Xcode 128 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 129 | 130 | 131 | 132 | 133 | ## Gcc Patch 134 | /*.gcno 135 | 136 | ### Xcode Patch ### 137 | *.xcodeproj/* 138 | !*.xcodeproj/project.pbxproj 139 | !*.xcodeproj/xcshareddata/ 140 | !*.xcworkspace/contents.xcworkspacedata 141 | **/xcshareddata/WorkspaceSettings.xcsettings 142 | 143 | # End of https://www.toptal.com/developers/gitignore/api/macos,xcode,cocoapods,swift -------------------------------------------------------------------------------- /Practice-JH/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'Practice-JH' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for Practice-JH 9 | pod 'KakaoSDKUser' # 카카오 로그인 10 | pod 'KakaoSDKCommon' # 카카오 일반 11 | pod 'JJFloatingActionButton' # 플로팅버튼 12 | pod 'KakaoSDKAuth' # 사용자 관리 13 | pod 'SnapKit', '~> 5.0.0' 14 | 15 | end 16 | -------------------------------------------------------------------------------- /Practice-JH/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (5.4.4) 3 | - JJFloatingActionButton (2.5.0) 4 | - KakaoSDKAuth (2.8.4): 5 | - KakaoSDKCommon (= 2.8.4) 6 | - KakaoSDKCommon (2.8.4): 7 | - KakaoSDKCommon/Common (= 2.8.4) 8 | - KakaoSDKCommon/Network (= 2.8.4) 9 | - KakaoSDKCommon/Common (2.8.4) 10 | - KakaoSDKCommon/Network (2.8.4): 11 | - Alamofire (~> 5.1) 12 | - KakaoSDKCommon/Common (= 2.8.4) 13 | - KakaoSDKUser (2.8.4): 14 | - KakaoSDKAuth (= 2.8.4) 15 | - SnapKit (5.0.1) 16 | 17 | DEPENDENCIES: 18 | - KakaoSDKAuth 19 | - JJFloatingActionButton 20 | - KakaoSDKCommon 21 | - KakaoSDKUser 22 | - SnapKit (~> 5.0.0) 23 | 24 | SPEC REPOS: 25 | trunk: 26 | - Alamofire 27 | - JJFloatingActionButton 28 | - KakaoSDKAuth 29 | - KakaoSDKCommon 30 | - KakaoSDKUser 31 | - SnapKit 32 | 33 | SPEC CHECKSUMS: 34 | Alamofire: f3b09a368f1582ab751b3fff5460276e0d2cf5c9 35 | JJFloatingActionButton: fdc3aefb0f28f04d0f988fa25d5fac97028a9ea7 36 | KakaoSDKAuth: b9b693995f98b1821ccd22b39d383e1dede5ca45 37 | KakaoSDKCommon: 33c9902ee9ce13e9d1cac3549896406229836ff5 38 | KakaoSDKUser: d878977e0ba6b365cfeac3b6319cdf4231713bea 39 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb 40 | 41 | PODFILE CHECKSUM: 95ee9ecf986dd9f5352c067a2cbba904fba53cd9 42 | 43 | COCOAPODS: 1.11.2 44 | -------------------------------------------------------------------------------- /Practice-JH/Pods/JJFloatingActionButton/Sources/JJFloatingActionButton+Animation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JJFloatingActionButton+Animation.swift 3 | // 4 | // Copyright (c) 2017-Present Jochen Pfeiffer 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | @objc public extension JJFloatingActionButton { 28 | /// Open the floating action button and show all action items. 29 | /// 30 | /// - Parameter animated: When true, button will be opened with an animation. Default is `true`. 31 | /// - Parameter completion: Will be handled upon completion. Default is `nil`. 32 | /// 33 | /// - Remark: Hidden items and items that have user interaction disabled are omitted. 34 | /// 35 | /// - SeeAlso: `buttonAnimationConfiguration` 36 | /// - SeeAlso: `itemAnimationConfiguration` 37 | /// 38 | func open(animated: Bool = true, completion: (() -> Void)? = nil) { 39 | guard let superview = superview, buttonState == .closed, !enabledItems.isEmpty, !isSingleActionButton else { 40 | return 41 | } 42 | 43 | buttonState = .opening 44 | delegate?.floatingActionButtonWillOpen?(self) 45 | 46 | storeAnimationState() 47 | 48 | superview.bringSubviewToFront(self) 49 | addOverlayView(to: superview) 50 | addItems(to: superview) 51 | itemContainerView.setNeedsLayout() 52 | itemContainerView.layoutIfNeeded() 53 | 54 | let animationGroup = DispatchGroup() 55 | 56 | showOverlay(animated: animated, group: animationGroup) 57 | openButton(withConfiguration: currentButtonAnimationConfiguration!, 58 | animated: animated, 59 | group: animationGroup) 60 | openItems(animated: animated, group: animationGroup) 61 | 62 | let groupCompletion: () -> Void = { 63 | guard self.buttonState == .opening else { 64 | return 65 | } 66 | self.buttonState = .open 67 | self.delegate?.floatingActionButtonDidOpen?(self) 68 | completion?() 69 | } 70 | if animated { 71 | animationGroup.notify(queue: .main, execute: groupCompletion) 72 | } else { 73 | groupCompletion() 74 | } 75 | } 76 | 77 | /// Close the floating action button and hide all action items. 78 | /// 79 | /// - Parameter animated: When true, button will be close with an animation. Default is `true`. 80 | /// - Parameter completion: Will be handled upon completion. Default is `nil`. 81 | /// 82 | /// - SeeAlso: `buttonAnimationConfiguration` 83 | /// - SeeAlso: `itemAnimationConfiguration` 84 | /// 85 | func close(animated: Bool = true, completion: (() -> Void)? = nil) { 86 | guard buttonState == .open || buttonState == .opening else { 87 | return 88 | } 89 | buttonState = .closing 90 | delegate?.floatingActionButtonWillClose?(self) 91 | overlayView.isEnabled = false 92 | 93 | let animationGroup = DispatchGroup() 94 | 95 | hideOverlay(animated: animated, group: animationGroup) 96 | closeButton(withConfiguration: currentButtonAnimationConfiguration!, 97 | animated: animated, 98 | group: animationGroup) 99 | closeItems(animated: animated, group: animationGroup) 100 | 101 | let groupCompletion: () -> Void = { 102 | self.openItems.forEach { item in 103 | item.removeFromSuperview() 104 | } 105 | self.resetAnimationState() 106 | self.itemContainerView.removeFromSuperview() 107 | self.buttonState = .closed 108 | self.delegate?.floatingActionButtonDidClose?(self) 109 | completion?() 110 | } 111 | if animated { 112 | animationGroup.notify(queue: .main, execute: groupCompletion) 113 | } else { 114 | groupCompletion() 115 | } 116 | } 117 | } 118 | 119 | internal extension JJFloatingActionButton { 120 | func removeRelatedViewsFromSuperview() { 121 | if overlayView.superview != nil { 122 | overlayView.removeFromSuperview() 123 | } 124 | 125 | if itemContainerView.superview != nil { 126 | itemContainerView.removeFromSuperview() 127 | } 128 | } 129 | } 130 | 131 | // MARK: - Animation State 132 | 133 | fileprivate extension JJFloatingActionButton { 134 | func storeAnimationState() { 135 | openItems = enabledItems 136 | currentItemAnimationConfiguration = itemAnimationConfiguration 137 | currentButtonAnimationConfiguration = buttonAnimationConfiguration 138 | } 139 | 140 | func resetAnimationState() { 141 | openItems.removeAll() 142 | currentButtonAnimationConfiguration = nil 143 | currentItemAnimationConfiguration = nil 144 | } 145 | } 146 | 147 | // MARK: - Overlay Animation 148 | 149 | fileprivate extension JJFloatingActionButton { 150 | func addOverlayView(to superview: UIView) { 151 | overlayView.isEnabled = true 152 | superview.insertSubview(overlayView, belowSubview: self) 153 | overlayView.translatesAutoresizingMaskIntoConstraints = false 154 | overlayView.topAnchor.constraint(equalTo: superview.topAnchor).isActive = true 155 | overlayView.leadingAnchor.constraint(equalTo: superview.leadingAnchor).isActive = true 156 | overlayView.trailingAnchor.constraint(equalTo: superview.trailingAnchor).isActive = true 157 | overlayView.bottomAnchor.constraint(equalTo: superview.bottomAnchor).isActive = true 158 | } 159 | 160 | func showOverlay(animated: Bool, group: DispatchGroup) { 161 | let buttonAnimation: () -> Void = { 162 | self.overlayView.alpha = 1 163 | } 164 | UIView.animate(duration: 0.3, 165 | usingSpringWithDamping: 1, 166 | initialSpringVelocity: 0.3, 167 | animations: buttonAnimation, 168 | group: group, 169 | animated: animated) 170 | } 171 | 172 | func hideOverlay(animated: Bool, group: DispatchGroup) { 173 | let animations: () -> Void = { 174 | self.overlayView.alpha = 0 175 | } 176 | let completion: (Bool) -> Void = { _ in 177 | self.overlayView.removeFromSuperview() 178 | } 179 | UIView.animate(duration: 0.3, 180 | usingSpringWithDamping: 1, 181 | initialSpringVelocity: 0.8, 182 | animations: animations, 183 | completion: completion, 184 | group: group, 185 | animated: animated) 186 | } 187 | } 188 | 189 | // MARK: - Button Animation 190 | 191 | fileprivate extension JJFloatingActionButton { 192 | func openButton(withConfiguration configuration: JJButtonAnimationConfiguration, 193 | animated: Bool, 194 | group: DispatchGroup) { 195 | switch configuration.style { 196 | case .rotation: 197 | rotateButton(toAngle: configuration.angle, 198 | settings: configuration.opening, 199 | group: group, 200 | animated: animated, 201 | status: 0) 202 | case .transition: 203 | transition(toImage: configuration.image, 204 | settings: configuration.opening, 205 | animated: animated, 206 | group: group) 207 | } 208 | } 209 | 210 | func closeButton(withConfiguration configuration: JJButtonAnimationConfiguration, 211 | animated: Bool, 212 | group: DispatchGroup) { 213 | switch configuration.style { 214 | case .rotation: 215 | rotateButton(toAngle: 0, 216 | settings: configuration.closing, 217 | group: group, 218 | animated: animated, 219 | status: 1) 220 | case .transition: 221 | transition(toImage: currentButtonImage, 222 | settings: configuration.closing, 223 | animated: animated, 224 | group: group) 225 | } 226 | } 227 | 228 | // MARK: 버튼 색상 전환을 위한 커스텀 229 | func rotateButton(toAngle angle: CGFloat, 230 | settings: JJAnimationSettings, 231 | group: DispatchGroup, 232 | animated: Bool, 233 | status: Int) { 234 | let spark_red_color: UIColor = UIColor(red: 1, green: 0, blue: 0.24, alpha: 1) 235 | 236 | let animation: () -> Void = { 237 | self.circleView.transform = CGAffineTransform(rotationAngle: angle) 238 | switch status { 239 | case 0: 240 | self.circleView.color = .white 241 | self.imageView.tintColor = spark_red_color 242 | default: 243 | self.circleView.color = spark_red_color 244 | self.imageView.tintColor = .white 245 | } 246 | 247 | } 248 | 249 | UIView.animate(duration: settings.duration, 250 | usingSpringWithDamping: settings.dampingRatio, 251 | initialSpringVelocity: settings.initialVelocity, 252 | animations: animation, 253 | group: group, 254 | animated: animated) 255 | } 256 | 257 | func transition(toImage image: UIImage?, 258 | settings: JJAnimationSettings, 259 | animated: Bool, 260 | group: DispatchGroup) { 261 | let transition: () -> Void = { 262 | self.imageView.image = image 263 | } 264 | UIView.transition(with: imageView, 265 | duration: settings.duration, 266 | animations: transition, 267 | group: group, 268 | animated: animated) 269 | } 270 | } 271 | 272 | // MARK: - Items Animation 273 | 274 | fileprivate extension JJFloatingActionButton { 275 | func addItems(to superview: UIView) { 276 | precondition(currentItemAnimationConfiguration != nil) 277 | let configuration = currentItemAnimationConfiguration! 278 | 279 | superview.insertSubview(itemContainerView, belowSubview: self) 280 | 281 | openItems.forEach { item in 282 | item.alpha = 0 283 | item.transform = .identity 284 | itemContainerView.addSubview(item) 285 | 286 | item.translatesAutoresizingMaskIntoConstraints = false 287 | 288 | item.circleView.heightAnchor.constraint(equalTo: circleView.heightAnchor, 289 | multiplier: itemSizeRatio).isActive = true 290 | 291 | item.topAnchor.constraint(greaterThanOrEqualTo: itemContainerView.topAnchor).isActive = true 292 | item.leadingAnchor.constraint(greaterThanOrEqualTo: itemContainerView.leadingAnchor).isActive = true 293 | item.trailingAnchor.constraint(lessThanOrEqualTo: itemContainerView.trailingAnchor).isActive = true 294 | item.bottomAnchor.constraint(lessThanOrEqualTo: itemContainerView.bottomAnchor).isActive = true 295 | } 296 | 297 | configuration.itemLayout.layout(openItems, self) 298 | } 299 | 300 | func openItems(animated: Bool, group: DispatchGroup) { 301 | precondition(currentItemAnimationConfiguration != nil) 302 | let configuration = currentItemAnimationConfiguration! 303 | 304 | let numberOfItems = openItems.count 305 | var delay: TimeInterval = 0.0 306 | var index = 0 307 | for item in openItems { 308 | configuration.closedState.prepare(item, index, numberOfItems, self) 309 | let animation: () -> Void = { 310 | configuration.openState.prepare(item, index, numberOfItems, self) 311 | } 312 | UIView.animate(duration: configuration.opening.duration, 313 | delay: delay, 314 | usingSpringWithDamping: configuration.opening.dampingRatio, 315 | initialSpringVelocity: configuration.opening.initialVelocity, 316 | animations: animation, 317 | group: group, 318 | animated: animated) 319 | 320 | delay += configuration.opening.interItemDelay 321 | index += 1 322 | } 323 | } 324 | 325 | func closeItems(animated: Bool, group: DispatchGroup) { 326 | precondition(currentItemAnimationConfiguration != nil) 327 | let configuration = currentItemAnimationConfiguration! 328 | 329 | let numberOfItems = openItems.count 330 | var delay: TimeInterval = 0.0 331 | var index = numberOfItems - 1 332 | for item in openItems.reversed() { 333 | let animation: () -> Void = { 334 | configuration.closedState.prepare(item, index, numberOfItems, self) 335 | } 336 | UIView.animate(duration: configuration.closing.duration, 337 | delay: delay, 338 | usingSpringWithDamping: configuration.closing.dampingRatio, 339 | initialSpringVelocity: configuration.closing.initialVelocity, 340 | animations: animation, 341 | group: group, 342 | animated: animated) 343 | 344 | delay += configuration.closing.interItemDelay 345 | index -= 1 346 | } 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH.xcodeproj/project.xcworkspace/xcuserdata/junholee.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH.xcodeproj/project.xcworkspace/xcuserdata/junholee.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Practice-JH/Practice-JH.xcodeproj/xcuserdata/junholee.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Practice-JH.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 6 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Practice-JH 4 | // 5 | // Created by Junho Lee on 2022/01/05. 6 | // 7 | 8 | import UIKit 9 | import KakaoSDKCommon 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | KakaoSDK.initSDK(appKey: "d462a0525fdd474a8bd51c58fe4081cb") 19 | 20 | return true 21 | } 22 | 23 | // MARK: UISceneSession Lifecycle 24 | 25 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 26 | // Called when a new scene session is being created. 27 | // Use this method to select a configuration to create the new scene with. 28 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 29 | } 30 | 31 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 32 | // Called when the user discards a scene session. 33 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 34 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 35 | } 36 | 37 | 38 | } 39 | 40 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/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 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_four.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 293.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Group 293@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Group 293@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_four.imageset/Group 293.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/cell_four.imageset/Group 293.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_four.imageset/Group 293@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/cell_four.imageset/Group 293@2x.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_four.imageset/Group 293@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/cell_four.imageset/Group 293@3x.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_one.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 58.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Group 58@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Group 58@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_one.imageset/Group 58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/cell_one.imageset/Group 58.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_one.imageset/Group 58@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/cell_one.imageset/Group 58@2x.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_one.imageset/Group 58@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/cell_one.imageset/Group 58@3x.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_three.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 61.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Group 61@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Group 61@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_three.imageset/Group 61.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/cell_three.imageset/Group 61.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_three.imageset/Group 61@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/cell_three.imageset/Group 61@2x.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_three.imageset/Group 61@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/cell_three.imageset/Group 61@3x.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_two.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 293.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Group 293@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Group 293@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_two.imageset/Group 293.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/cell_two.imageset/Group 293.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_two.imageset/Group 293@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/cell_two.imageset/Group 293@2x.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/cell_two.imageset/Group 293@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/cell_two.imageset/Group 293@3x.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/chatButton.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "chatButton.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "chatButton@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "chatButton@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/chatButton.imageset/chatButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/chatButton.imageset/chatButton.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/chatButton.imageset/chatButton@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/chatButton.imageset/chatButton@2x.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Assets.xcassets/chatButton.imageset/chatButton@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamSparker/Spark-Practice-iOS/35168167b2fb6ea035dbd24b4d1669e60450ecb8/Practice-JH/Practice-JH/Assets.xcassets/chatButton.imageset/chatButton@3x.png -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 28 | 37 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 75 | 82 | 91 | 100 | 107 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | CFBundleURLSchemes 11 | 12 | kakaod462a0525fdd474a8bd51c58fe4081cb 13 | 14 | 15 | 16 | LSApplicationQueriesSchemes 17 | 18 | kakaolink 19 | kakaokompassauth 20 | 21 | UIApplicationSceneManifest 22 | 23 | UIApplicationSupportsMultipleScenes 24 | 25 | UISceneConfigurations 26 | 27 | UIWindowSceneSessionRoleApplication 28 | 29 | 30 | UISceneConfigurationName 31 | Default Configuration 32 | UISceneDelegateClassName 33 | $(PRODUCT_MODULE_NAME).SceneDelegate 34 | UISceneStoryboardFile 35 | Main 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/LogoutViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoutViewController.swift 3 | // Practice-JH 4 | // 5 | // Created by Junho Lee on 2022/01/06. 6 | // 7 | 8 | import UIKit 9 | import KakaoSDKUser 10 | import JJFloatingActionButton 11 | 12 | class LogoutViewController: UIViewController { 13 | var nickname: String? 14 | var email: String? 15 | var userId: Int64? 16 | var birthday: String? 17 | var urlString = "https://archijude.tistory.com/183" 18 | var profile: URL? 19 | 20 | @IBOutlet weak var nicknameLabel: UILabel! 21 | @IBOutlet weak var emailLabel: UILabel! 22 | @IBOutlet weak var userIdLabel: UILabel! 23 | @IBOutlet weak var birthdayLabel: UILabel! 24 | @IBOutlet weak var profileImageView: UIImageView! 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | 29 | setText() 30 | } 31 | @IBAction func logoutClicked(_ sender: Any) { 32 | // ✅ 로그아웃 : 로그아웃은 API 요청의 성공 여부와 관계없이 토큰을 삭제 처리한다는 점에 유의합니다. 33 | UserApi.shared.logout {(error) in 34 | if let error = error { 35 | print(error) 36 | } 37 | else { 38 | print("logout() success.") 39 | 40 | // ✅ 로그아웃 시 메인으로 보냄 41 | self.navigationController?.popViewController(animated: true) 42 | } 43 | } 44 | } 45 | 46 | @IBAction func unlinkClicked(_ sender: Any) { 47 | // ✅ 연결 끊기 : 연결이 끊어지면 기존의 토큰은 더 이상 사용할 수 없으므로, 연결 끊기 API 요청 성공 시 로그아웃 처리가 함께 이뤄져 토큰이 삭제됩니다. 48 | UserApi.shared.unlink {(error) in 49 | if let error = error { 50 | print(error) 51 | } 52 | else { 53 | print("unlink() success.") 54 | 55 | // ✅ 연결끊기 시 메인으로 보냄 56 | self.navigationController?.popViewController(animated: true) 57 | } 58 | } 59 | } 60 | } 61 | 62 | extension LogoutViewController { 63 | func setText(){ 64 | nicknameLabel.text = nickname 65 | emailLabel.text = email 66 | userIdLabel.text = "\(userId)" 67 | birthdayLabel.text = birthday 68 | 69 | let url = profile! 70 | DispatchQueue.global().async { 71 | let data = try? Data(contentsOf: url) 72 | DispatchQueue.main.async { 73 | self.profileImageView.image = UIImage(data: data!) 74 | } 75 | } 76 | 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Practice-JH 4 | // 5 | // Created by Junho Lee on 2022/01/05. 6 | // 7 | 8 | import UIKit 9 | import KakaoSDKAuth 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | guard let _ = (scene as? UIWindowScene) else { return } 21 | } 22 | 23 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { 24 | if let url = URLContexts.first?.url { 25 | if (AuthApi.isKakaoTalkLoginUrl(url)) { 26 | _ = AuthController.handleOpenUrl(url: url) 27 | } 28 | } 29 | } 30 | 31 | func sceneDidDisconnect(_ scene: UIScene) { 32 | // Called as the scene is being released by the system. 33 | // This occurs shortly after the scene enters the background, or when its session is discarded. 34 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 35 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 36 | } 37 | 38 | func sceneDidBecomeActive(_ scene: UIScene) { 39 | // Called when the scene has moved from an inactive state to an active state. 40 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 41 | } 42 | 43 | func sceneWillResignActive(_ scene: UIScene) { 44 | // Called when the scene will move from an active state to an inactive state. 45 | // This may occur due to temporary interruptions (ex. an incoming phone call). 46 | } 47 | 48 | func sceneWillEnterForeground(_ scene: UIScene) { 49 | // Called as the scene transitions from the background to the foreground. 50 | // Use this method to undo the changes made on entering the background. 51 | } 52 | 53 | func sceneDidEnterBackground(_ scene: UIScene) { 54 | // Called as the scene transitions from the foreground to the background. 55 | // Use this method to save data, release shared resources, and store enough scene-specific state information 56 | // to restore the scene back to its current state. 57 | } 58 | 59 | 60 | } 61 | 62 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Practice-JH 4 | // 5 | // Created by Junho Lee on 2022/01/05. 6 | // 7 | 8 | import UIKit 9 | import KakaoSDKUser 10 | import KakaoSDKAuth 11 | import KakaoSDKCommon 12 | import JJFloatingActionButton 13 | 14 | class ViewController: UIViewController { 15 | 16 | @IBOutlet weak var talkLoginButton: UIButton! 17 | @IBOutlet weak var accountLoginButton: UIButton! 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | // 앱 실행시마다 연결 끊기 23 | UserApi.shared.unlink {(error) in 24 | if let error = error { 25 | print(error) 26 | } 27 | else { 28 | print("unlink() success.") 29 | } 30 | } 31 | 32 | setFloatingButton() 33 | } 34 | 35 | override func viewWillAppear(_ animated: Bool) { 36 | super.viewWillAppear(animated) 37 | 38 | // 토큰을 가지고 있는지 검사하여 logoutVC에서 pop되었을 때 상태에 따라 다시 돌아가게 만듬 39 | // if (AuthApi.hasToken()) { 40 | // UserApi.shared.accessTokenInfo { (_, error) in 41 | // if let error = error { 42 | // if let sdkError = error as? SdkError, sdkError.isInvalidTokenError() == true { 43 | // //로그인 필요 44 | // } 45 | // else { 46 | // //기타 에러 47 | // } 48 | // } 49 | // else { 50 | // // 토큰 유효성 체크 성공(필요 시 토큰 갱신됨) 51 | // print("토큰 유효성 체크 성공") 52 | // 53 | // // ✅ 사용자 정보를 가져오고 화면전환을 하는 커스텀 메서드 54 | // self.getUserInfo() 55 | // } 56 | // } 57 | // } 58 | // else { 59 | // //로그인 필요 60 | // } 61 | } 62 | 63 | @IBAction func touchTalkLogin(_ sender: Any) { 64 | loginKakao() 65 | } 66 | 67 | @IBAction func touchAccountLogin(_ sender: Any) { 68 | loginKakaoAccount() 69 | } 70 | @IBAction func touchPresentCarousel(_ sender: Any) { 71 | } 72 | 73 | extension ViewController { 74 | func loginKakao() { 75 | print("loginKakao() called.") 76 | 77 | // ✅ 카카오톡 설치 여부 확인 78 | if (UserApi.isKakaoTalkLoginAvailable()) { 79 | UserApi.shared.loginWithKakaoTalk {(oauthToken, error) in 80 | if let error = error { 81 | print(error) 82 | } 83 | else { 84 | print("loginWithKakaoTalk() success.") 85 | 86 | // ✅ 회원가입 성공 시 oauthToken 저장가능하다 87 | // _ = oauthToken 88 | 89 | // ✅ 사용자정보를 성공적으로 가져오면 화면전환 한다. 90 | self.getUserInfo() 91 | } 92 | } 93 | } 94 | // ✅ 카카오톡 미설치 95 | else { 96 | print("카카오톡 미설치") 97 | } 98 | } 99 | 100 | private func getUserInfo() { 101 | 102 | // ✅ 사용자 정보 가져오기 103 | UserApi.shared.me() {(user, error) in 104 | if let error = error { 105 | print(error) 106 | } 107 | else { 108 | print("me() success.") 109 | 110 | // ✅ 닉네임, 이메일 정보 111 | let nickname = user?.kakaoAccount?.profile?.nickname 112 | let email = user?.kakaoAccount?.email 113 | let userId = user?.id 114 | let birthday = user?.kakaoAccount?.birthday 115 | let profile = user?.kakaoAccount?.profile?.profileImageUrl 116 | 117 | guard let nextVC = self.storyboard?.instantiateViewController(withIdentifier: "LogoutViewController") as? LogoutViewController else { return } 118 | 119 | // ✅ 사용자 정보 넘기기 120 | nextVC.nickname = nickname 121 | nextVC.email = email 122 | nextVC.userId = userId 123 | nextVC.birthday = birthday 124 | nextVC.profile = profile! 125 | 126 | // ✅ 화면전환 127 | self.navigationController?.pushViewController(nextVC, animated: true) 128 | } 129 | } 130 | } 131 | 132 | func loginKakaoAccount() { 133 | print("loginKakaoAccount() called.") 134 | 135 | // ✅ 기본 웹 브라우저를 사용하여 로그인 진행. 136 | UserApi.shared.loginWithKakaoAccount {(oauthToken, error) in 137 | if let error = error { 138 | print(error) 139 | } 140 | else { 141 | print("loginWithKakaoAccount() success.") 142 | 143 | // ✅ 회원가입 성공 시 oauthToken 저장 144 | // _ = oauthToken 145 | 146 | // ✅ 사용자정보를 성공적으로 가져오면 화면전환 한다. 147 | self.getUserInfo() 148 | } 149 | } 150 | } 151 | 152 | func setFloatingButton(){ 153 | let actionButton = JJFloatingActionButton() 154 | let spark_red_color: UIColor = UIColor(red: 1, green: 0, blue: 0.24, alpha: 1) 155 | 156 | actionButton.addItem(title: "방 만들기", image: UIImage(named: "floating_room_icon")?.withRenderingMode(.alwaysTemplate)) { item in 157 | // 클릭 action 부분 158 | } 159 | 160 | actionButton.addItem(title: "코드로 참여", image: UIImage(named: "floating_code_icon")?.withRenderingMode(.alwaysTemplate)) { item in 161 | // 클릭 action 부분 162 | } 163 | 164 | actionButton.configureDefaultItem { item in 165 | item.buttonColor = spark_red_color 166 | item.buttonImageColor = .white 167 | } 168 | 169 | self.view.addSubview(actionButton) 170 | actionButton.buttonColor = spark_red_color 171 | actionButton.translatesAutoresizingMaskIntoConstraints = false 172 | actionButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16).isActive = true 173 | actionButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16).isActive = true 174 | } 175 | } 176 | 177 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/carousel/Carousel.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/carousel/CarouselCVC/firstCVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // firstCVC.swift 3 | // Practice-JH 4 | // 5 | // Created by Junho Lee on 2022/01/12. 6 | // 7 | 8 | import UIKit 9 | 10 | class firstCVC: UICollectionViewCell { 11 | let customView: UIImageView = { 12 | let view = UIImageView() 13 | view.translatesAutoresizingMaskIntoConstraints = false 14 | view.layer.cornerRadius = 2 15 | view.clipsToBounds = true 16 | 17 | return view 18 | }() 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | 23 | self.addSubview(self.customView) 24 | 25 | self.customView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true 26 | self.customView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true 27 | self.customView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1).isActive = true 28 | self.customView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1).isActive = true 29 | } 30 | 31 | required init?(coder aDecoder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/carousel/CarouselCVC/secondCVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // secondCVC.swift 3 | // Practice-JH 4 | // 5 | // Created by Junho Lee on 2022/01/12. 6 | // 7 | 8 | import UIKit 9 | 10 | class secondCVC: UICollectionViewCell { 11 | let customView: UIImageView = { 12 | let view = UIImageView() 13 | view.translatesAutoresizingMaskIntoConstraints = false 14 | view.layer.cornerRadius = 2 15 | view.clipsToBounds = true 16 | 17 | return view 18 | }() 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | 23 | self.addSubview(self.customView) 24 | 25 | self.customView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true 26 | self.customView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true 27 | self.customView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1).isActive = true 28 | self.customView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1).isActive = true 29 | } 30 | 31 | required init?(coder aDecoder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/carousel/CarouselCVC/thirdCVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // thirdCVC.swift 3 | // Practice-JH 4 | // 5 | // Created by Junho Lee on 2022/01/12. 6 | // 7 | 8 | import UIKit 9 | 10 | class thirdCVC: UICollectionViewCell { 11 | let customView: UIImageView = { 12 | let view = UIImageView() 13 | view.translatesAutoresizingMaskIntoConstraints = false 14 | view.layer.cornerRadius = 2 15 | view.clipsToBounds = true 16 | 17 | return view 18 | }() 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | 23 | self.addSubview(self.customView) 24 | 25 | self.customView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true 26 | self.customView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true 27 | self.customView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1).isActive = true 28 | self.customView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1).isActive = true 29 | } 30 | 31 | required init?(coder aDecoder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/carousel/CarouselContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CarouselContainerView.swift 3 | // Practice-JH 4 | // 5 | // Created by Junho Lee on 2022/01/11. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | class CarouselContainerView: UIView { 12 | 13 | var collectionView: UICollectionView = { 14 | let layout = UICollectionViewFlowLayout() 15 | layout.minimumLineSpacing = 10 16 | 17 | layout.scrollDirection = .horizontal 18 | layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) 19 | 20 | let cv = UICollectionView(frame: CGRect(x: 0,y: 0,width: 0,height: 0), collectionViewLayout: layout) 21 | 22 | var cvIdentifier = "CVC" 23 | 24 | return cv 25 | }() 26 | 27 | public var identifier = "View" 28 | 29 | override init(frame: CGRect) { 30 | super.init(frame: .zero) 31 | } 32 | 33 | init(putIdentifier: String) { 34 | super.init(frame: .zero) 35 | self.backgroundColor = .red 36 | self.layer.name = "firstView" 37 | 38 | addSubview(collectionView) 39 | // collectionView.iden 40 | 41 | self.collectionView.snp.makeConstraints { make in 42 | make.edges.equalTo(self).inset(10) 43 | } 44 | identifier = putIdentifier 45 | } 46 | 47 | required init?(coder: NSCoder) { 48 | fatalError("init(coder:) has not been implemented") 49 | } 50 | // 51 | // override init(frame: CGRect) { 52 | // super.init(frame: frame) 53 | // self.backgroundColor = .red 54 | // 55 | // addSubview(collectionView) 56 | // 57 | // self.collectionView.snp.makeConstraints { make in 58 | // make.width.height.equalTo(self) 59 | // } 60 | // } 61 | // 62 | // required init?(coder aDecoder: NSCoder) { 63 | // fatalError("init(coder:) has not been implemented") 64 | // } 65 | } 66 | 67 | class MyButton: UIButton { 68 | public var statusCV: Int? 69 | } 70 | 71 | //extension UIView { 72 | // let cellCase :carouselCase = [.firstView, .secondView, .thirdView] 73 | //} 74 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/carousel/CarouselLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CarouselLayout.swift 3 | // Practice-JH 4 | // 5 | // Created by Junho Lee on 2022/01/07. 6 | // 7 | 8 | import UIKit 9 | 10 | class CarouselLayout: UICollectionViewFlowLayout { 11 | 12 | public var sideItemScale: CGFloat = 0.5 13 | public var sideItemAlpha: CGFloat = 0.5 14 | public var spacing: CGFloat = 10 15 | 16 | // animaition 전환에 사용하는 변수 17 | public var animationStatus: Bool = true 18 | 19 | public var isPagingEnabled: Bool = false 20 | 21 | private var isSetup: Bool = false 22 | 23 | // MARK: prepare는 사용자가 스크롤 시 매번 호출된다고 한다. setupLayout()은 초기에 한 번만 호출되도록 한 것이다. 24 | override public func prepare() { 25 | super.prepare() 26 | if isSetup == false { 27 | setupLayout() 28 | isSetup = true 29 | } 30 | } 31 | 32 | private func setupLayout() { 33 | guard let collectionView = self.collectionView else {return} 34 | 35 | let collectionViewSize = collectionView.bounds.size 36 | 37 | let xInset = (collectionViewSize.width - self.itemSize.width) / 2 38 | let yInset = (collectionViewSize.height - self.itemSize.height) / 2 39 | 40 | self.sectionInset = UIEdgeInsets(top: yInset, left: xInset, bottom: yInset, right: xInset) 41 | 42 | let itemWidth = self.itemSize.width 43 | 44 | let scaledItemOffset = (itemWidth - itemWidth*self.sideItemScale) / 2 45 | self.minimumLineSpacing = spacing - scaledItemOffset 46 | 47 | self.scrollDirection = .horizontal 48 | } 49 | 50 | public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 51 | return true 52 | } 53 | 54 | /// 모든 셀과 뷰에 대한 속성을 UICollectionViewLayoutAttributes의 배열로 반환해준다고 한다. 55 | /// 이 속성들을 사용하기 위해 map 함수를 통해 리턴해주었다. 56 | public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 57 | guard let superAttributes = super.layoutAttributesForElements(in: rect), 58 | let attributes = NSArray(array: superAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes] 59 | else { return nil } 60 | 61 | /// transformLayoutAttributes() 함수를 이용해 기존 attributes 속성들을 원하는 대로 변환하고 62 | /// 이를 attributes에 다시 매핑하여 UICollectionViewLayoutAttributes로 반환한다 63 | return attributes.map({ self.transformLayoutAttributes(attributes: $0) }) 64 | } 65 | 66 | private func transformLayoutAttributes(attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { 67 | 68 | guard let collectionView = self.collectionView else {return attributes} 69 | 70 | // 순서대로 컬렉션뷰 크기의 절반, 컬렉션뷰의 컨텐트의 x좌표:예를 들어 한 셀이 지나면 셀의 크기만큼 더해진다, 현재 컨텐트의 오프셋과 각 셀의 중심 좌표의 차이다 : 이는 컬렉션뷰의 오리진 x좌표와 셀의 중심 사이의 거리라고 볼 수 있다. 71 | // 즉, center는 컬렉션뷰 좌표계를 기준으로 각 셀의 중심의 x표이다. 72 | let collectionCenter = collectionView.frame.size.width / 2 73 | let contentOffset = collectionView.contentOffset.x 74 | let center = attributes.center.x - contentOffset 75 | 76 | // 멀리 있는 경우, ratio가 0이 되며 sideItem으로 판별하여 alpha와 scale이 된다. 77 | // maxDistance보다 가까운 경우, ratio가 1에 가까워지면서 alpha와 scale 값이 1로 온전해진다. 78 | // distance의 경우, 컬렉션뷰 중심과 각 셀의 중심 사이의 거리이다. 컬렉션뷰 중심으로 셀이 가까이 올수록 거리가 줄어든다. maxdistance를 넣은 이유는 ratio가 음수가 되는 것을 방지하기 위해서이다. 79 | let maxDistance = self.itemSize.width + self.minimumLineSpacing 80 | let distance = min(abs(collectionCenter - center), maxDistance) 81 | 82 | let ratio = (maxDistance - distance)/maxDistance 83 | 84 | // 아래에 있는 1을 바꿔서 중심으로 올수록 흐려지거나, 작게 만들 수 있다. 85 | let alpha = ratio * (1 - self.sideItemAlpha) + self.sideItemAlpha 86 | let scale = ratio * (1 - self.sideItemScale) + self.sideItemScale 87 | 88 | attributes.alpha = alpha 89 | 90 | let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size) 91 | let dist = attributes.frame.midX - visibleRect.midX 92 | var transform = CATransform3DScale(CATransform3DIdentity, scale, scale, 1) 93 | 94 | // fakescale은 중심에서 -1, 멀어졌을 때 1 / realscale은 중심에서 0, 멀어졌을 때 -1 95 | // CA로테이션 함수에서 1이 한 바퀴 회전이다. 360도임. 96 | // CATransform은 한 번에 하나씩만 사용 가능하다. 97 | switch animationStatus{ 98 | // 회전시키는 경우 99 | case true: 100 | let fakescale = (492/520-scale)*(520/28) 101 | let realscale = (-1-fakescale)*0.5 102 | transform = CATransform3DMakeRotation(realscale*8, 0, 1, 0) 103 | // 크기를 조절하는 경우 104 | default: 105 | transform = CATransform3DTranslate(transform, 0, 0, -abs(dist/1000)) 106 | } 107 | attributes.transform3D = transform 108 | 109 | return attributes 110 | } 111 | 112 | // MARK: 페이징 가능하게 해주는 코드 113 | // 이 함수는 현재 스크롤의 방향과 속력을 고려하여 스크롤을 멈출 ContentOffset을 지정해준다. 따라서, 멈추길 원하는 목적하는 CGPoint를 함수 내에서 반환해주면 된다. 114 | // 우리는 각 셀의 center와, 컬렉션뷰의 center를 일치시키고자 한다. 115 | override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { 116 | 117 | // 여기서 collectionView = self.collectionVIew의 의미를 모르겠어요. 컬렉션뷰는 항상 있는건데 왜 else로 빠지는걸까요? 118 | guard let collectionView = self.collectionView else { 119 | let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) 120 | print(latestOffset) 121 | return latestOffset 122 | } 123 | 124 | // print(targetRect)를 통해 targertRect가 하나만 생성된다는 것을 확인했다. 컨텐츠가 멈추기 원하는 목표지점에 rect를 생성해주는 코드이다. 우리는, 이 rect에 포함되는 셀들 중에 컬렉션뷰의 중심에 가장 가까이 있는 셀의 중심에 멈춰야 한다. 125 | let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.frame.width, height: collectionView.frame.height) 126 | 127 | // layoutAttrbutesForeElemets는 parameter로 취하는 targetRect의 범위 안에 있는 셀들의 UICollectionViewLayoutAttributes를 반환해준다. 따라서, Rect의 위치에 따라 2개 셀의 레이아웃 애트리뷰트들이 반환되거나, 3개 셀의 레이아웃 애트리뷰트들이 반환된다. 128 | // targetRect의 width를 2000으로 늘린다면? -> 2000의 범위 안에 있는 셀들의 속성이 반환된다. 129 | guard let rectAttributes = super.layoutAttributesForElements(in: targetRect) else { return .zero } 130 | 131 | // finite number란, 무한보다 작은 셀 수 있는 숫자이다. 여기에 greatest가 붙어서, 가장 큰 양의 숫자를 반환한다. 시스템 상에서 가장 큰 소수의 값이다. _MAX를 생각하면 된다. 132 | var offsetAdjustment = CGFloat.greatestFiniteMagnitude 133 | 134 | // horizontalCenter는 컨텐츠오프셋의 x좌표에 컬렉션뷰의 절반을 더해서 새로운 목표지점이 될 x좌표를 만들어준다. 135 | let horizontalCenter = proposedContentOffset.x + collectionView.frame.width / 2 136 | 137 | // 여러 개 셀의 레이아웃 속성들을 비교하여, 138 | // magnitude는 소수의 절대값을 말한다. 즉, (itemHorizontalCenter - horizontalCenter).magnitude는 변위를 거리로 바꿔준다. 139 | // if 문의 역할은, 두 개의 셀 중에서 컬렉션뷰 중심까지의 거리가 더 가까운 셀의 거리를 offsetAdjustment에 저장해주는 것이다. if문 안의 대입값에는 magnitude가 없는데, 이는 -값과 +값(변위)을 모두 사용해야 하기 때문이다. 140 | for layoutAttributes in rectAttributes { 141 | let itemHorizontalCenter = layoutAttributes.center.x 142 | if (itemHorizontalCenter - horizontalCenter).magnitude < offsetAdjustment.magnitude { 143 | offsetAdjustment = itemHorizontalCenter - horizontalCenter 144 | } 145 | } 146 | 147 | // 이렇게 원래 스크롤 시 멈추려던 좌표에서, 조정될 변위를 더해줘서 셀의 중심과 컬렉션뷰의 중심을 일치시켜주면 페이징과 같이 기능하게 된다. 148 | return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Practice-JH/Practice-JH/carousel/CarouselVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CarouselVC.swift 3 | // Practice-JH 4 | // 5 | // Created by Junho Lee on 2022/01/07. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | enum carouselCase{ 12 | case firstView 13 | case secondView 14 | case thirdView 15 | } 16 | 17 | class CarouselVC: UIViewController { 18 | 19 | // @IBOutlet weak var carouselCV: UICollectionView! 20 | private var isClicked: Bool = false 21 | private let cellCaseList: [carouselCase] = [.firstView, .secondView, .thirdView] 22 | 23 | let firstViewButton = MyButton() 24 | let secondViewButton = MyButton() 25 | let thirdViewButton = MyButton() 26 | var selectedItem = 3 27 | 28 | let imageNames: [String] = ["cell_one","cell_two","cell_three" 29 | ,"cell_four","cell_one","cell_two", 30 | "cell_three","cell_four","cell_one", 31 | "cell_two","cell_three","cell_four", 32 | "cell_one","cell_two","cell_three", 33 | "cell_four","cell_one","cell_two", 34 | "cell_three","cell_four"] 35 | 36 | let firstView = CarouselContainerView(putIdentifier: "firstView") 37 | let secondView = CarouselContainerView(putIdentifier: "secondView") 38 | let thirdView = CarouselContainerView(putIdentifier: "thirdView") 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | 43 | // addCollectionView(isClicked) 44 | setDelegate() 45 | // carouselCV.backgroundColor = .clear 46 | self.view.backgroundColor = .white 47 | 48 | addSubviewss(firstViewButton, secondViewButton, thirdViewButton) 49 | 50 | //버튼 관련 51 | setButtons() 52 | addTargets(firstViewButton, secondViewButton, thirdViewButton) 53 | 54 | //캐러셀 컨테이너 관련 55 | addContainerViews(firstView, secondView, thirdView) 56 | print("안녕하세ㅛㅇ") 57 | print(self.firstView.collectionView.frame.width) 58 | setContainerViews() 59 | setCarousels() 60 | } 61 | 62 | override func viewWillAppear(_ animated: Bool) { 63 | super.viewWillAppear(animated) 64 | 65 | } 66 | 67 | func setCarousels() { 68 | let layout = CarouselLayout() 69 | 70 | let centerItemWidthScale: CGFloat = 327/375 71 | let centerItemHeightScale: CGFloat = 1 72 | 73 | layout.itemSize = CGSize(width: 500*centerItemWidthScale, height: 300*centerItemHeightScale) 74 | 75 | 76 | layout.sideItemScale = 464/520 77 | layout.spacing = 12 78 | layout.sideItemAlpha = 0.4 79 | 80 | 81 | let layout1 = CarouselLayout() 82 | 83 | layout1.itemSize = CGSize(width: 500*centerItemWidthScale, height: 300*centerItemHeightScale) 84 | 85 | layout1.sideItemScale = 464/520 86 | layout1.spacing = 12 87 | layout1.sideItemAlpha = 0.4 88 | 89 | let layout2 = CarouselLayout() 90 | 91 | layout2.itemSize = CGSize(width: 500*centerItemWidthScale, height: 300*centerItemHeightScale) 92 | 93 | layout2.sideItemScale = 464/520 94 | layout2.spacing = 12 95 | layout2.sideItemAlpha = 0.4 96 | 97 | firstView.collectionView.collectionViewLayout = layout 98 | firstView.collectionView.register(thirdCVC.self, forCellWithReuseIdentifier: "thirdCVC") 99 | 100 | secondView.collectionView.collectionViewLayout = layout1 101 | 102 | secondView.collectionView.register(thirdCVC.self, forCellWithReuseIdentifier: "thirdCVC") 103 | // 104 | thirdView.collectionView.collectionViewLayout = layout2 105 | 106 | thirdView.collectionView.register(thirdCVC.self, forCellWithReuseIdentifier: "thirdCVC") 107 | 108 | firstView.collectionView.reloadData() 109 | secondView.collectionView.reloadData() 110 | thirdView.collectionView.reloadData() 111 | } 112 | 113 | func setButtons() { 114 | firstViewButton.statusCV = 0 115 | firstViewButton.backgroundColor = .clear 116 | firstViewButton.setTitle("캐러셀 1", for: .normal) 117 | firstViewButton.setTitleColor(.black, for: .normal) 118 | firstViewButton.setTitleColor(.gray, for: .highlighted) 119 | secondViewButton.statusCV = 1 120 | secondViewButton.backgroundColor = .clear 121 | secondViewButton.setTitle("캐러셀 2", for: .normal) 122 | secondViewButton.setTitleColor(.black, for: .normal) 123 | secondViewButton.setTitleColor(.gray, for: .highlighted) 124 | thirdViewButton.statusCV = 2 125 | thirdViewButton.backgroundColor = .clear 126 | thirdViewButton.setTitle("캐러셀 3", for: .normal) 127 | thirdViewButton.setTitleColor(.black, for: .normal) 128 | thirdViewButton.setTitleColor(.gray, for: .highlighted) 129 | 130 | secondViewButton.snp.makeConstraints { make in 131 | make.top.equalToSuperview().offset(150) 132 | make.centerX.equalToSuperview() 133 | } 134 | 135 | firstViewButton.snp.makeConstraints { make in 136 | make.centerY.equalTo(secondViewButton.snp.centerY) 137 | make.trailing.equalTo(secondViewButton.snp.leading).offset(-40) 138 | } 139 | 140 | thirdViewButton.snp.makeConstraints { make in 141 | make.centerY.equalTo(secondViewButton.snp.centerY) 142 | make.leading.equalTo(secondViewButton.snp.trailing).offset(40) 143 | } 144 | } 145 | 146 | func addContainerViews(_ views: UIView...) { 147 | for view in views { 148 | self.view.addSubview(view) 149 | view.snp.makeConstraints { make in 150 | make.leading.trailing.equalToSuperview() 151 | make.top.equalToSuperview().offset(300) 152 | make.bottom.equalToSuperview().offset(-95) 153 | } 154 | } 155 | } 156 | 157 | func setContainerViews() { 158 | secondView.backgroundColor = .blue 159 | thirdView.backgroundColor = .purple 160 | } 161 | 162 | func addSubviewss(_ views: UIView...) { 163 | for view in views { 164 | self.view.addSubview(view) 165 | } 166 | } 167 | 168 | func addTargets(_ buttons: MyButton...) { 169 | for button in buttons { 170 | button.addTarget(self, action: #selector(changeCollectionView), for: .touchUpInside) 171 | } 172 | } 173 | 174 | func setDelegate() { 175 | self.firstView.collectionView.delegate = self 176 | self.firstView.collectionView.dataSource = self 177 | self.secondView.collectionView.delegate = self 178 | self.secondView.collectionView.dataSource = self 179 | self.thirdView.collectionView.delegate = self 180 | self.thirdView.collectionView.dataSource = self 181 | } 182 | 183 | // func addCollectionView(_ status: Bool){ 184 | // 185 | // let layout = CarouselLayout() 186 | // 187 | // // TODO: 컬렉션뷰 자체의 비율을 고정하는 작업 하기 188 | // // 현재 페이지의 크기 비율을 지정, 피그마 참고했습니다. 189 | // let centerItemWidthScale: CGFloat = 327/375 190 | // let centerItemHeightScale: CGFloat = 1 191 | // 192 | // layout.itemSize = CGSize(width: carouselCV.frame.size.width*centerItemWidthScale, height: carouselCV.frame.size.height*centerItemHeightScale) 193 | // 194 | // layout.sideItemScale = 464/520 195 | // layout.spacing = 12 196 | // layout.sideItemAlpha = 0.4 197 | // layout.animationStatus = status 198 | // carouselCV.collectionViewLayout = layout 199 | // 200 | // self.carouselCV?.register(carouselCVC.self, forCellWithReuseIdentifier: "carouselCVC") 201 | // 202 | // self.carouselCV?.reloadData() 203 | // } 204 | 205 | // 버튼을 터치함에 따라서 animationStatus을 변경해준다. 206 | @IBAction func touchChangeAnimation(_ sender: Any) { 207 | if !isClicked { 208 | isClicked = true 209 | // addCollectionView(isClicked) 210 | } else { 211 | isClicked = false 212 | // addCollectionView(isClicked) 213 | } 214 | } 215 | 216 | @objc func changeCollectionView(sender: MyButton) { 217 | let status: Int = (sender.statusCV)! 218 | switch status { 219 | case 0: 220 | firstView.isHidden = false 221 | print(self.firstView.frame.width) 222 | secondView.isHidden = true 223 | thirdView.isHidden = true 224 | selectedItem = 1 225 | print(firstView.layer.name) 226 | // firstView.position.z 227 | case 1: 228 | firstView.isHidden = true 229 | secondView.isHidden = false 230 | thirdView.isHidden = true 231 | selectedItem = 2 232 | secondView.collectionView.reloadData() 233 | firstView.collectionView.reloadData() 234 | thirdView.collectionView.reloadData() 235 | // secondVIew.position.z 236 | default: 237 | firstView.isHidden = true 238 | secondView.isHidden = true 239 | thirdView.isHidden = false 240 | selectedItem = 3 241 | // thirdView.position.z 242 | } 243 | } 244 | } 245 | 246 | extension CarouselVC: UICollectionViewDelegate, UICollectionViewDataSource { 247 | func numberOfSections(in collectionView: UICollectionView) -> Int { 248 | return 1 249 | } 250 | 251 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 252 | return 20 253 | } 254 | 255 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 256 | // 257 | // let selected = collectionView.superview?.layer.name 258 | //// let superView = collectionView.superview.layer.name 259 | // 260 | // if selected == "firstView" { 261 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "thirdCVC", for: indexPath) as! thirdCVC 262 | 263 | cell.customView.image = UIImage(named: imageNames[indexPath.row]) 264 | return cell 265 | // } 266 | // if selected == "firstView" { 267 | // let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "firstCVC", for: indexPath) as! firstCVC 268 | // 269 | // cell.customView.image = UIImage(named: imageNames[indexPath.row]) 270 | // return cell 271 | // } else 272 | // { 273 | // let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "firstCVC", for: indexPath) as! firstCVC 274 | // 275 | // cell.customView.image = UIImage(named: imageNames[indexPath.row]) 276 | // return cell 277 | // } 278 | 279 | // print("2왔당") 280 | // let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "secondCVC", for: indexPath) as! secondCVC 281 | // 282 | // cell.customView.image = UIImage(named: imageNames[indexPath.row]) 283 | // return cell\ 284 | // print("3왔당") 285 | // let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "thirdCVC", for: indexPath) as! thirdCVC 286 | // 287 | // cell.customView.image = UIImage(named: imageNames[indexPath.row]) 288 | // return cell 289 | 290 | 291 | // switch superView { 292 | // case .firstView: 293 | // let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "firstCVC", for: indexPath) as! firstCVC 294 | // 295 | // cell.customView.image = UIImage(named: imageNames[indexPath.row]) 296 | // return cell 297 | // case .secondView: 298 | // let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "secondCVC", for: indexPath) as! secondCVC 299 | // 300 | // cell.customView.image = UIImage(named: imageNames[indexPath.row]) 301 | // return cell 302 | // case .thirdView: 303 | // let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "thirdCVC", for: indexPath) as! secondCVC 304 | // 305 | // cell.customView.image = UIImage(named: imageNames[indexPath.row]) 306 | // return cell 307 | // default: 308 | // let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "thirdCVC", for: indexPath) as! secondCVC 309 | // 310 | // cell.customView.image = UIImage(named: imageNames[indexPath.row]) 311 | // return cell 312 | // } 313 | // let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "firstCVC", for: indexPath) as! firstCVC 314 | // 315 | // cell.customView.image = UIImage(named: imageNames[indexPath.row]) 316 | // return cell 317 | 318 | 319 | 320 | // let cellcase = cellCaseList[indexPath.row] 321 | // 322 | // switch(cellcase){ 323 | // case .first: 324 | // let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "carouselCVC", for: indexPath) as! carouselCVC 325 | // 326 | // cell.customView.image = UIImage(named: imageNames[indexPath.row]) 327 | // return cell 328 | // case .second: 329 | // let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "secondCVC", for: indexPath) as! secondCVC 330 | // 331 | // cell.customView.image = UIImage(named: imageNames[indexPath.row]) 332 | // return cell 333 | // default: 334 | // let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "secondCVC", for: indexPath) as! secondCVC 335 | // 336 | // cell.customView.image = UIImage(named: imageNames[indexPath.row]) 337 | // return cell 338 | // } 339 | 340 | } 341 | } 342 | // 343 | //extension CarouselVC: UICollectionViewDelegateFlowLayout { 344 | // func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 345 | // let centerItemWidthScale: CGFloat = 327/375 346 | // let centerItemHeightScale: CGFloat = 1 347 | // 348 | // let itemSize = CGSize(width: firstView.frame.width*centerItemWidthScale, height: firstView.frame.height*centerItemHeightScale) 349 | // 350 | // return itemSize 351 | // } 352 | //} 353 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/FloatingButton.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/FloatingButton.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/FloatingButton.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/FloatingButton.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/FloatingButton/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // FloatingButton 4 | // 5 | // Created by 양수빈 on 2022/01/07. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/FloatingButton/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 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/FloatingButton/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/FloatingButton/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/FloatingButton/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/FloatingButton/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/FloatingButton/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.swift 3 | // FloatingButton 4 | // 5 | // Created by 양수빈 on 2022/01/07. 6 | // 7 | 8 | import UIKit 9 | 10 | class BaseViewController: UIViewController { 11 | 12 | // MARK: - View Life Cycles 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | setLayout() 17 | setUI() 18 | } 19 | 20 | // MARK: - Override Method 21 | func setLayout() { 22 | // Override Layout 23 | } 24 | 25 | func setUI() { 26 | view.backgroundColor = .black 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/FloatingButton/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/FloatingButton/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // FloatingButton 4 | // 5 | // Created by 양수빈 on 2022/01/07. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/FloatingButton/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // FloatingButton 4 | // 5 | // Created by 양수빈 on 2022/01/07. 6 | // 7 | 8 | import UIKit 9 | 10 | import JJFloatingActionButton 11 | import SnapKit 12 | 13 | class ViewController: BaseViewController { 14 | 15 | // MARK: - Properties 16 | /// 뷰에 올릴 프로퍼티 선언 17 | let titleLabel = UILabel() 18 | let subTitleLabel = UILabel() 19 | let actionButton = JJFloatingActionButton() 20 | let tap = UITapGestureRecognizer() 21 | 22 | // MARK: - View Life Cycles 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | /// BaseViewController를 상속받기 때문에, BaseViewController에서 viewDidLoad()에 넣어둔 함수는 다시 안넣어도 적용O 28 | setButtonUI() 29 | setAddTarget() 30 | } 31 | 32 | // MARK: - Method 33 | 34 | override func setUI() { 35 | super.setUI() 36 | 37 | /// 프로퍼티 특성 설정 38 | titleLabel.text = "Floating Button Test" 39 | titleLabel.textColor = .white 40 | 41 | subTitleLabel.text = "화이팅" 42 | subTitleLabel.textColor = .gray 43 | 44 | /// gesture 지정할 요소 외의 모든 다른 부분의 touch도 받아들이도록 false 설정 45 | tap.cancelsTouchesInView = false 46 | view.addGestureRecognizer(tap) 47 | } 48 | 49 | override func setLayout() { 50 | /// 프로퍼티를 View에 넣어줌 (storyboard에서 viewcontroller에 올리는 과정) 51 | view.addSubview(titleLabel) 52 | view.addSubview(actionButton) 53 | view.addSubview(subTitleLabel) 54 | 55 | /// textLabel의 레이아웃 설정 56 | titleLabel.snp.makeConstraints { make in 57 | /// superView를 기준으로 center에 배치 58 | make.center.equalToSuperview() 59 | } 60 | 61 | subTitleLabel.snp.makeConstraints { make in 62 | /// subTitleLabel의 top을 titleLabel의 bottom으로부터 20만큼 띄우겠다 63 | make.top.equalTo(titleLabel.snp.bottom).offset(20) 64 | make.centerX.equalTo(titleLabel.snp.centerX) 65 | } 66 | 67 | actionButton.snp.makeConstraints { make in 68 | /// actionButton의 trailing과 bottom을 superView의 trailing과 bottom으로부터 각각 30씩 안쪽으로 constraint를 주겠다 69 | /// 두개를 아래처럼 따로 써도 되고, 같은 값이라면 합칠 수도 O 70 | 71 | // make.trailing.equalToSuperview().inset(30) 72 | // make.bottom.equalToSuperview().inset(30) 73 | 74 | make.trailing.bottom.equalToSuperview().inset(30) 75 | } 76 | } 77 | 78 | func setButtonUI() { 79 | /// 버튼 컬러 80 | actionButton.buttonColor = .purple 81 | /// 버튼 이미지 82 | actionButton.buttonImage = UIImage(systemName: "moon") 83 | /// 버튼 이미지 컬러 84 | actionButton.buttonImageColor = .white 85 | /// 버튼 이미지 크기 86 | actionButton.buttonImageSize = CGSize(width: 24, height: 24) 87 | /// 버튼 탭했을 때 순간 컬러 88 | actionButton.highlightedButtonColor = .orange 89 | /// item 버튼 크기 90 | actionButton.itemSizeRatio = CGFloat(1) 91 | /// item 버튼 추가 92 | actionButton.addItem(title: "첫번째", image: UIImage(systemName: "house")) { _ in 93 | print("첫번째 눌림") 94 | } 95 | 96 | actionButton.addItem(title: "두번째", image: UIImage(systemName: "house.fill")) { _ in 97 | print("두번째 눌림") 98 | } 99 | 100 | /// item 버튼 등장하는 방식 101 | actionButton.itemAnimationConfiguration = .popUp() /// default 102 | /// 버튼 shadow 컬러 103 | actionButton.layer.shadowColor = UIColor.white.cgColor 104 | /// 버튼 shadow 방향 105 | actionButton.layer.shadowOffset = CGSize(width: 0, height: 0) 106 | /// 버튼 shadow 투명도 107 | actionButton.layer.shadowOpacity = Float(0.4) 108 | /// 버튼 shadow 퍼지는 정도 109 | actionButton.layer.shadowRadius = CGFloat(10) 110 | } 111 | 112 | func setAddTarget() { 113 | actionButton.addTarget(self, action: #selector(setButtonColor), for: .touchUpInside) 114 | tap.addTarget(self, action: #selector(setButtonColor)) 115 | } 116 | 117 | /// actionButton을 클릭했을 때, buttonState.rawValue 값에 따라 버튼 컬러 재설정 시도 118 | @objc 119 | func setButtonColor() { 120 | /// buttonState == 2 -> 플로팅 버튼 안펼쳐진 상태 121 | if actionButton.buttonState.rawValue == 2 { 122 | actionButton.buttonColor = .red 123 | } else { 124 | actionButton.buttonColor = .purple 125 | } 126 | } 127 | } 128 | 129 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'FloatingButton' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for FloatingButton 9 | pod 'SnapKit', '~> 5.0.0' 10 | pod 'JJFloatingActionButton' 11 | 12 | end 13 | -------------------------------------------------------------------------------- /Practice-YSB/FloatingButton/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - JJFloatingActionButton (2.5.0) 3 | - SnapKit (5.0.1) 4 | 5 | DEPENDENCIES: 6 | - JJFloatingActionButton 7 | - SnapKit (~> 5.0.0) 8 | 9 | SPEC REPOS: 10 | trunk: 11 | - JJFloatingActionButton 12 | - SnapKit 13 | 14 | SPEC CHECKSUMS: 15 | JJFloatingActionButton: fdc3aefb0f28f04d0f988fa25d5fac97028a9ea7 16 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb 17 | 18 | PODFILE CHECKSUM: 2664582dfab0b1714414828d663ec2ebd9706ba3 19 | 20 | COCOAPODS: 1.11.2 21 | -------------------------------------------------------------------------------- /Practice-YSB/Photo/Photo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2B05709227897C2400D50E6D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B05709127897C2400D50E6D /* AppDelegate.swift */; }; 11 | 2B05709427897C2400D50E6D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B05709327897C2400D50E6D /* SceneDelegate.swift */; }; 12 | 2B05709627897C2400D50E6D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B05709527897C2400D50E6D /* ViewController.swift */; }; 13 | 2B05709927897C2400D50E6D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2B05709727897C2400D50E6D /* Main.storyboard */; }; 14 | 2B05709B27897C2500D50E6D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2B05709A27897C2500D50E6D /* Assets.xcassets */; }; 15 | 2B05709E27897C2500D50E6D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2B05709C27897C2500D50E6D /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 2B05708E27897C2400D50E6D /* Photo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Photo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 2B05709127897C2400D50E6D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 21 | 2B05709327897C2400D50E6D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 22 | 2B05709527897C2400D50E6D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23 | 2B05709827897C2400D50E6D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 24 | 2B05709A27897C2500D50E6D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | 2B05709D27897C2500D50E6D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 26 | 2B05709F27897C2500D50E6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 2B05708B27897C2400D50E6D /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 2B05708527897C2400D50E6D = { 41 | isa = PBXGroup; 42 | children = ( 43 | 2B05709027897C2400D50E6D /* Photo */, 44 | 2B05708F27897C2400D50E6D /* Products */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | 2B05708F27897C2400D50E6D /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 2B05708E27897C2400D50E6D /* Photo.app */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | 2B05709027897C2400D50E6D /* Photo */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 2B05709127897C2400D50E6D /* AppDelegate.swift */, 60 | 2B05709327897C2400D50E6D /* SceneDelegate.swift */, 61 | 2B05709527897C2400D50E6D /* ViewController.swift */, 62 | 2B05709727897C2400D50E6D /* Main.storyboard */, 63 | 2B05709A27897C2500D50E6D /* Assets.xcassets */, 64 | 2B05709C27897C2500D50E6D /* LaunchScreen.storyboard */, 65 | 2B05709F27897C2500D50E6D /* Info.plist */, 66 | ); 67 | path = Photo; 68 | sourceTree = ""; 69 | }; 70 | /* End PBXGroup section */ 71 | 72 | /* Begin PBXNativeTarget section */ 73 | 2B05708D27897C2400D50E6D /* Photo */ = { 74 | isa = PBXNativeTarget; 75 | buildConfigurationList = 2B0570A227897C2500D50E6D /* Build configuration list for PBXNativeTarget "Photo" */; 76 | buildPhases = ( 77 | 2B05708A27897C2400D50E6D /* Sources */, 78 | 2B05708B27897C2400D50E6D /* Frameworks */, 79 | 2B05708C27897C2400D50E6D /* Resources */, 80 | ); 81 | buildRules = ( 82 | ); 83 | dependencies = ( 84 | ); 85 | name = Photo; 86 | productName = Photo; 87 | productReference = 2B05708E27897C2400D50E6D /* Photo.app */; 88 | productType = "com.apple.product-type.application"; 89 | }; 90 | /* End PBXNativeTarget section */ 91 | 92 | /* Begin PBXProject section */ 93 | 2B05708627897C2400D50E6D /* Project object */ = { 94 | isa = PBXProject; 95 | attributes = { 96 | BuildIndependentTargetsInParallel = 1; 97 | LastSwiftUpdateCheck = 1300; 98 | LastUpgradeCheck = 1300; 99 | TargetAttributes = { 100 | 2B05708D27897C2400D50E6D = { 101 | CreatedOnToolsVersion = 13.0; 102 | }; 103 | }; 104 | }; 105 | buildConfigurationList = 2B05708927897C2400D50E6D /* Build configuration list for PBXProject "Photo" */; 106 | compatibilityVersion = "Xcode 13.0"; 107 | developmentRegion = en; 108 | hasScannedForEncodings = 0; 109 | knownRegions = ( 110 | en, 111 | Base, 112 | ); 113 | mainGroup = 2B05708527897C2400D50E6D; 114 | productRefGroup = 2B05708F27897C2400D50E6D /* Products */; 115 | projectDirPath = ""; 116 | projectRoot = ""; 117 | targets = ( 118 | 2B05708D27897C2400D50E6D /* Photo */, 119 | ); 120 | }; 121 | /* End PBXProject section */ 122 | 123 | /* Begin PBXResourcesBuildPhase section */ 124 | 2B05708C27897C2400D50E6D /* Resources */ = { 125 | isa = PBXResourcesBuildPhase; 126 | buildActionMask = 2147483647; 127 | files = ( 128 | 2B05709E27897C2500D50E6D /* LaunchScreen.storyboard in Resources */, 129 | 2B05709B27897C2500D50E6D /* Assets.xcassets in Resources */, 130 | 2B05709927897C2400D50E6D /* Main.storyboard in Resources */, 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | /* End PBXResourcesBuildPhase section */ 135 | 136 | /* Begin PBXSourcesBuildPhase section */ 137 | 2B05708A27897C2400D50E6D /* Sources */ = { 138 | isa = PBXSourcesBuildPhase; 139 | buildActionMask = 2147483647; 140 | files = ( 141 | 2B05709627897C2400D50E6D /* ViewController.swift in Sources */, 142 | 2B05709227897C2400D50E6D /* AppDelegate.swift in Sources */, 143 | 2B05709427897C2400D50E6D /* SceneDelegate.swift in Sources */, 144 | ); 145 | runOnlyForDeploymentPostprocessing = 0; 146 | }; 147 | /* End PBXSourcesBuildPhase section */ 148 | 149 | /* Begin PBXVariantGroup section */ 150 | 2B05709727897C2400D50E6D /* Main.storyboard */ = { 151 | isa = PBXVariantGroup; 152 | children = ( 153 | 2B05709827897C2400D50E6D /* Base */, 154 | ); 155 | name = Main.storyboard; 156 | sourceTree = ""; 157 | }; 158 | 2B05709C27897C2500D50E6D /* LaunchScreen.storyboard */ = { 159 | isa = PBXVariantGroup; 160 | children = ( 161 | 2B05709D27897C2500D50E6D /* Base */, 162 | ); 163 | name = LaunchScreen.storyboard; 164 | sourceTree = ""; 165 | }; 166 | /* End PBXVariantGroup section */ 167 | 168 | /* Begin XCBuildConfiguration section */ 169 | 2B0570A027897C2500D50E6D /* Debug */ = { 170 | isa = XCBuildConfiguration; 171 | buildSettings = { 172 | ALWAYS_SEARCH_USER_PATHS = NO; 173 | CLANG_ANALYZER_NONNULL = YES; 174 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 175 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 176 | CLANG_CXX_LIBRARY = "libc++"; 177 | CLANG_ENABLE_MODULES = YES; 178 | CLANG_ENABLE_OBJC_ARC = YES; 179 | CLANG_ENABLE_OBJC_WEAK = YES; 180 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 181 | CLANG_WARN_BOOL_CONVERSION = YES; 182 | CLANG_WARN_COMMA = YES; 183 | CLANG_WARN_CONSTANT_CONVERSION = YES; 184 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 185 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 186 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 187 | CLANG_WARN_EMPTY_BODY = YES; 188 | CLANG_WARN_ENUM_CONVERSION = YES; 189 | CLANG_WARN_INFINITE_RECURSION = YES; 190 | CLANG_WARN_INT_CONVERSION = YES; 191 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 192 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 193 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 194 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 195 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 196 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 197 | CLANG_WARN_STRICT_PROTOTYPES = YES; 198 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 199 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 200 | CLANG_WARN_UNREACHABLE_CODE = YES; 201 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 202 | COPY_PHASE_STRIP = NO; 203 | DEBUG_INFORMATION_FORMAT = dwarf; 204 | ENABLE_STRICT_OBJC_MSGSEND = YES; 205 | ENABLE_TESTABILITY = YES; 206 | GCC_C_LANGUAGE_STANDARD = gnu11; 207 | GCC_DYNAMIC_NO_PIC = NO; 208 | GCC_NO_COMMON_BLOCKS = YES; 209 | GCC_OPTIMIZATION_LEVEL = 0; 210 | GCC_PREPROCESSOR_DEFINITIONS = ( 211 | "DEBUG=1", 212 | "$(inherited)", 213 | ); 214 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 215 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 216 | GCC_WARN_UNDECLARED_SELECTOR = YES; 217 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 218 | GCC_WARN_UNUSED_FUNCTION = YES; 219 | GCC_WARN_UNUSED_VARIABLE = YES; 220 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 221 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 222 | MTL_FAST_MATH = YES; 223 | ONLY_ACTIVE_ARCH = YES; 224 | SDKROOT = iphoneos; 225 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 226 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 227 | }; 228 | name = Debug; 229 | }; 230 | 2B0570A127897C2500D50E6D /* Release */ = { 231 | isa = XCBuildConfiguration; 232 | buildSettings = { 233 | ALWAYS_SEARCH_USER_PATHS = NO; 234 | CLANG_ANALYZER_NONNULL = YES; 235 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 236 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 237 | CLANG_CXX_LIBRARY = "libc++"; 238 | CLANG_ENABLE_MODULES = YES; 239 | CLANG_ENABLE_OBJC_ARC = YES; 240 | CLANG_ENABLE_OBJC_WEAK = YES; 241 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 242 | CLANG_WARN_BOOL_CONVERSION = YES; 243 | CLANG_WARN_COMMA = YES; 244 | CLANG_WARN_CONSTANT_CONVERSION = YES; 245 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 246 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 247 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 248 | CLANG_WARN_EMPTY_BODY = YES; 249 | CLANG_WARN_ENUM_CONVERSION = YES; 250 | CLANG_WARN_INFINITE_RECURSION = YES; 251 | CLANG_WARN_INT_CONVERSION = YES; 252 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 253 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 254 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 255 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 256 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 261 | CLANG_WARN_UNREACHABLE_CODE = YES; 262 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 263 | COPY_PHASE_STRIP = NO; 264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 265 | ENABLE_NS_ASSERTIONS = NO; 266 | ENABLE_STRICT_OBJC_MSGSEND = YES; 267 | GCC_C_LANGUAGE_STANDARD = gnu11; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 276 | MTL_ENABLE_DEBUG_INFO = NO; 277 | MTL_FAST_MATH = YES; 278 | SDKROOT = iphoneos; 279 | SWIFT_COMPILATION_MODE = wholemodule; 280 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 281 | VALIDATE_PRODUCT = YES; 282 | }; 283 | name = Release; 284 | }; 285 | 2B0570A327897C2500D50E6D /* Debug */ = { 286 | isa = XCBuildConfiguration; 287 | buildSettings = { 288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 289 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 290 | CODE_SIGN_STYLE = Automatic; 291 | CURRENT_PROJECT_VERSION = 1; 292 | DEVELOPMENT_TEAM = 4QG3GC35LA; 293 | GENERATE_INFOPLIST_FILE = YES; 294 | INFOPLIST_FILE = Photo/Info.plist; 295 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 296 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 297 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 298 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 299 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 300 | LD_RUNPATH_SEARCH_PATHS = ( 301 | "$(inherited)", 302 | "@executable_path/Frameworks", 303 | ); 304 | MARKETING_VERSION = 1.0; 305 | PRODUCT_BUNDLE_IDENTIFIER = subin.Photo; 306 | PRODUCT_NAME = "$(TARGET_NAME)"; 307 | SWIFT_EMIT_LOC_STRINGS = YES; 308 | SWIFT_VERSION = 5.0; 309 | TARGETED_DEVICE_FAMILY = "1,2"; 310 | }; 311 | name = Debug; 312 | }; 313 | 2B0570A427897C2500D50E6D /* Release */ = { 314 | isa = XCBuildConfiguration; 315 | buildSettings = { 316 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 317 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 318 | CODE_SIGN_STYLE = Automatic; 319 | CURRENT_PROJECT_VERSION = 1; 320 | DEVELOPMENT_TEAM = 4QG3GC35LA; 321 | GENERATE_INFOPLIST_FILE = YES; 322 | INFOPLIST_FILE = Photo/Info.plist; 323 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 324 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 325 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 326 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 327 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 328 | LD_RUNPATH_SEARCH_PATHS = ( 329 | "$(inherited)", 330 | "@executable_path/Frameworks", 331 | ); 332 | MARKETING_VERSION = 1.0; 333 | PRODUCT_BUNDLE_IDENTIFIER = subin.Photo; 334 | PRODUCT_NAME = "$(TARGET_NAME)"; 335 | SWIFT_EMIT_LOC_STRINGS = YES; 336 | SWIFT_VERSION = 5.0; 337 | TARGETED_DEVICE_FAMILY = "1,2"; 338 | }; 339 | name = Release; 340 | }; 341 | /* End XCBuildConfiguration section */ 342 | 343 | /* Begin XCConfigurationList section */ 344 | 2B05708927897C2400D50E6D /* Build configuration list for PBXProject "Photo" */ = { 345 | isa = XCConfigurationList; 346 | buildConfigurations = ( 347 | 2B0570A027897C2500D50E6D /* Debug */, 348 | 2B0570A127897C2500D50E6D /* Release */, 349 | ); 350 | defaultConfigurationIsVisible = 0; 351 | defaultConfigurationName = Release; 352 | }; 353 | 2B0570A227897C2500D50E6D /* Build configuration list for PBXNativeTarget "Photo" */ = { 354 | isa = XCConfigurationList; 355 | buildConfigurations = ( 356 | 2B0570A327897C2500D50E6D /* Debug */, 357 | 2B0570A427897C2500D50E6D /* Release */, 358 | ); 359 | defaultConfigurationIsVisible = 0; 360 | defaultConfigurationName = Release; 361 | }; 362 | /* End XCConfigurationList section */ 363 | }; 364 | rootObject = 2B05708627897C2400D50E6D /* Project object */; 365 | } 366 | -------------------------------------------------------------------------------- /Practice-YSB/Photo/Photo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Practice-YSB/Photo/Photo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Practice-YSB/Photo/Photo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Photo 4 | // 5 | // Created by 양수빈 on 2022/01/08. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Practice-YSB/Photo/Photo/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 | -------------------------------------------------------------------------------- /Practice-YSB/Photo/Photo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Practice-YSB/Photo/Photo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Practice-YSB/Photo/Photo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Practice-YSB/Photo/Photo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /Practice-YSB/Photo/Photo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSCameraUsageDescription 6 | 카메라를 열어도 될까요? 7 | NSPhotoLibraryUsageDescription 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 | UISceneStoryboardFile 23 | Main 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Practice-YSB/Photo/Photo/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Photo 4 | // 5 | // Created by 양수빈 on 2022/01/08. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Practice-YSB/Photo/Photo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Photo 4 | // 5 | // Created by 양수빈 on 2022/01/08. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | // MARK: - @IBOutlet 13 | 14 | @IBOutlet weak var imageView: UIImageView! 15 | 16 | // MARK: - Properties 17 | 18 | let picker = UIImagePickerController() 19 | 20 | // MARK: - View Life Cycles 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | picker.delegate = self 26 | } 27 | 28 | // MARK: - @IBAction 29 | 30 | @IBAction func getPhotoImage(_ sender: Any) { 31 | /// alterController 생성 32 | let alter = UIAlertController(title: "사진가져오기", message: "진짜로 사진을 가져오시겠어요?", preferredStyle: .actionSheet) 33 | 34 | /// alter에 들어갈 액션 생성 35 | let library = UIAlertAction(title: "앨범에서 가져오기", style: .default) { action in 36 | self.openLibrary() 37 | } 38 | 39 | let camera = UIAlertAction(title: "카메라 열기", style: .default) { action in 40 | self.openCamera() 41 | } 42 | 43 | let cancel = UIAlertAction(title: "취소", style: .cancel, handler: nil) 44 | 45 | /// alter에 액션을 넣어줌 46 | alter.addAction(library) 47 | alter.addAction(camera) 48 | alter.addAction(cancel) 49 | 50 | /// button tap했을 때 alter present 51 | present(alter, animated: true, completion: nil) 52 | } 53 | 54 | func openLibrary() { 55 | /// UIImagePickerController에서 어떤 식으로 image를 pick해올지 -> 앨범에서 픽해오겠다 56 | picker.sourceType = .photoLibrary 57 | present(picker, animated: false, completion: nil) 58 | } 59 | 60 | func openCamera() { 61 | /// 카메라 촬영 타입이 가능하다면 62 | if UIImagePickerController.isSourceTypeAvailable(.camera) { 63 | /// UIImagePickerController에서 어떤 식으로 image를 pick해올지 -> 카메라 촬영헤서 픽해오겠다 64 | picker.sourceType = .camera 65 | present(picker, animated: false, completion: nil) 66 | } else { 67 | print("카메라 안됩니다.") 68 | } 69 | } 70 | } 71 | 72 | extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { 73 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { 74 | // print("------------------") 75 | // print(info) 76 | // print("------------------") 77 | /// UIImage 타입인 originalImage를 빼옴 78 | if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { 79 | imageView.image = image 80 | imageView.contentMode = .scaleAspectFill /// 1:1 로 맞춰둔 imageView에 꽉 채우겠다. 81 | } 82 | dismiss(animated: true, completion: nil) 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'StickyHeader' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for StickyHeader 9 | pod 'SnapKit' 10 | 11 | end 12 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SnapKit (5.0.1) 3 | 4 | DEPENDENCIES: 5 | - SnapKit 6 | 7 | SPEC REPOS: 8 | trunk: 9 | - SnapKit 10 | 11 | SPEC CHECKSUMS: 12 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb 13 | 14 | PODFILE CHECKSUM: 984ec16e67572a32e8bb28ae5cb764e733d0a406 15 | 16 | COCOAPODS: 1.11.2 17 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // StickyHeader 4 | // 5 | // Created by 양수빈 on 2022/01/10. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader/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 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader/CustomCVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomCVC.swift 3 | // StickyHeader 4 | // 5 | // Created by 양수빈 on 2022/01/10. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | class CustomCVC: UICollectionViewCell { 13 | static let identifier = "CustomCVC" 14 | 15 | // MARK: - Properties 16 | let titleLabel = UILabel() 17 | let userNameLabel = UILabel() 18 | 19 | // MARK: - View Life Cycles 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | 23 | setUI() 24 | setLayout() 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | override func prepareForReuse() { 32 | super.prepareForReuse() 33 | 34 | titleLabel.text = "---" 35 | userNameLabel.text = "사용자이름" 36 | } 37 | 38 | // MARK: - Methods 39 | func setUI() { 40 | titleLabel.text = "---" 41 | userNameLabel.text = "사용자이름" 42 | 43 | titleLabel.font = .systemFont(ofSize: 20) 44 | userNameLabel.font = .systemFont(ofSize: 14) 45 | userNameLabel.textColor = .gray 46 | } 47 | 48 | func setLayout() { 49 | addSubview(titleLabel) 50 | addSubview(userNameLabel) 51 | 52 | titleLabel.snp.makeConstraints { make in 53 | make.leading.equalToSuperview().offset(16) 54 | make.top.equalToSuperview().offset(16) 55 | } 56 | 57 | userNameLabel.snp.makeConstraints { make in 58 | make.leading.equalToSuperview().offset(16) 59 | make.top.equalTo(titleLabel.snp.bottom).offset(10) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader/CustomHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomHeaderView.swift 3 | // StickyHeader 4 | // 5 | // Created by 양수빈 on 2022/01/10. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | class CustomHeaderView: UICollectionReusableView { 13 | static let identifier = "CustomHeaderView" 14 | 15 | // MARK: - Properties 16 | let dateLabel = UILabel() 17 | 18 | // MARK: - View Life Cycles 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | 22 | setUI() 23 | setLayout() 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | fatalError("init(coder:) has not been implemented") 28 | } 29 | 30 | override func prepareForReuse() { 31 | super.prepareForReuse() 32 | 33 | dateLabel.text = "" 34 | } 35 | 36 | // MARK: - Methods 37 | func setUI() { 38 | dateLabel.text = "Header" 39 | dateLabel.font = .systemFont(ofSize: 30) 40 | } 41 | 42 | func setLayout() { 43 | addSubview(dateLabel) 44 | 45 | dateLabel.snp.makeConstraints { make in 46 | make.leading.equalToSuperview().offset(16) 47 | make.centerY.equalToSuperview() 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // StickyHeader 4 | // 5 | // Created by 양수빈 on 2022/01/10. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Practice-YSB/StickyHeader/StickyHeader/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // StickyHeader 4 | // 5 | // Created by 양수빈 on 2022/01/10. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | class ViewController: UIViewController { 13 | 14 | // MARK: - Dummy Data 15 | var dummyDataList: Array = [ ["date": "2022-11-11", "userName": "양수빈", "sparkTitle": "간식먹기", "sparkCount": "1"], 16 | ["date": "2022-11-11", "userName": "감자", "sparkTitle": "간식먹기222", "sparkCount": "2"], 17 | ["date": "2022-11-11", "userName": "김", "sparkTitle": "아아아간식먹기", "sparkCount": "3"], 18 | ["date": "2022-11-10", "userName": "옹", "sparkTitle": "아아 마셔", "sparkCount": "4"], 19 | ["date": "2022-11-10", "userName": "우와", "sparkTitle": "커피 홀짝", "sparkCount": "5"], 20 | ["date": "2022-11-9", "userName": "우와", "sparkTitle": "홀짝 커피", "sparkCount": "6"], 21 | ["date": "2022-11-9", "userName": "뭐지", "sparkTitle": "하하하하", "sparkCount": "7"], 22 | ["date": "2022-11-9", "userName": "하하", "sparkTitle": "쉽지않네", "sparkCount": "8"], 23 | ["date": "2022-11-9", "userName": "호호", "sparkTitle": "덤이데잍어", "sparkCount": "9"], 24 | ["date": "2022-11-7", "userName": "s", "sparkTitle": "덤이데sdfdfdfdf잍어", "sparkCount": "10"], 25 | ["date": "2022-11-7", "userName": "호sgggg호", "sparkTitle": "덤이sddd데잍어", "sparkCount": "11"] ] 26 | 27 | var newDummyDataList: Array = [ ["date": "2022-11-7", "userName": "양수빈", "sparkTitle": "간식먹기", "sparkCount": "12"], 28 | ["date": "2022-11-7", "userName": "감자", "sparkTitle": "간식먹기222", "sparkCount": "13"], 29 | ["date": "2022-11-4", "userName": "김", "sparkTitle": "아아아간식먹기", "sparkCount": "14"], 30 | ["date": "2022-11-4", "userName": "옹", "sparkTitle": "아아 마셔", "sparkCount": "15"], 31 | ["date": "2022-11-3", "userName": "우와", "sparkTitle": "커피 홀짝", "sparkCount": "16"], 32 | ["date": "2022-11-3", "userName": "우와", "sparkTitle": "홀짝 커피", "sparkCount": "17"], 33 | ["date": "2022-11-3", "userName": "뭐지", "sparkTitle": "하하하하", "sparkCount": "18"], 34 | ["date": "2022-11-3", "userName": "하하", "sparkTitle": "쉽지않네", "sparkCount": "19"], 35 | ["date": "2022-11-3", "userName": "호호", "sparkTitle": "덤이데잍어", "sparkCount": "20"], 36 | ["date": "2022-11-2", "userName": "s", "sparkTitle": "덤이데sdfdfdfdf잍어", "sparkCount": "21"], 37 | ["date": "2022-11-2", "userName": "호sgggg호", "sparkTitle": "덤이sddd데잍어", "sparkCount": "22"] ] 38 | 39 | // MARK: - Properties 40 | let collectionViewFlowlayout = UICollectionViewFlowLayout() 41 | lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewFlowlayout) 42 | var index = 0 43 | var dateList: [String] = [] 44 | var firstList: [Any] = [] 45 | var secondList: [Any] = [] 46 | var thirdList: [Any] = [] 47 | var fourthList: [Any] = [] 48 | var fifthList: [Any] = [] 49 | var sixthList: [Any] = [] 50 | var seventhList: [Any] = [] 51 | 52 | // MARK: - View Life Cycles 53 | override func viewDidLoad() { 54 | super.viewDidLoad() 55 | 56 | setLayout() 57 | setCollcetionView() 58 | setData(datalist: dummyDataList) 59 | } 60 | 61 | // MARK: - Methods 62 | func setLayout() { 63 | view.addSubview(collectionView) 64 | 65 | collectionView.snp.makeConstraints { make in 66 | make.edges.equalTo(view.safeAreaLayoutGuide) 67 | } 68 | } 69 | 70 | func setData(datalist: Array>) { 71 | var indexPath = 0 72 | var sectionCount = 0 /// section을 돌기 위한 변수 73 | 74 | // print(dummyDataList[indexPath]["date"]) 75 | // print(type(of: dummyDataList[indexPath]["date"])) 76 | // print(dummyDataList[indexPath]) 77 | 78 | // print(datalist) 79 | // print(type(of: datalist)) 80 | // print(dummyDataList) 81 | // print(type(of: dummyDataList)) 82 | while indexPath < datalist.count { 83 | if dateList.isEmpty { 84 | dateList.append(datalist[indexPath]["date"] as! String) 85 | indexPath += 1 86 | } else { 87 | let day: String = datalist[indexPath]["date"] ?? "" 88 | 89 | if !(dateList.contains(day)) { 90 | dateList.append(day) 91 | } 92 | 93 | indexPath += 1 94 | } 95 | } 96 | 97 | // TODO: - section별 리스트 생성 98 | while sectionCount < dateList.count { 99 | var index = 0 100 | while index < datalist.count && !datalist.isEmpty && sectionCount != dateList.count { 101 | 102 | if dateList[sectionCount] == datalist[index]["date"] { 103 | switch sectionCount { 104 | case 0: 105 | firstList.append(datalist[index]) 106 | case 1: 107 | secondList.append(datalist[index]) 108 | case 2: 109 | thirdList.append(datalist[index]) 110 | case 3: 111 | fourthList.append(datalist[index]) 112 | case 4: 113 | fifthList.append(datalist[index]) 114 | case 5: 115 | sixthList.append(datalist[index]) 116 | case 6: 117 | seventhList.append(datalist[index]) 118 | default: 119 | seventhList.append(datalist[index]) 120 | } 121 | 122 | } 123 | index += 1 124 | } 125 | sectionCount += 1 126 | } 127 | 128 | // print("1: ", firstList) 129 | // print("2: ", secondList) 130 | // print("2: ", thirdList) 131 | // print("4: ", fourthList) 132 | // print("5: ", fifthList) 133 | // print("6: ", sixthList) 134 | // print("7: ", seventhList) 135 | // print("-------------------------------------------------------") 136 | } 137 | 138 | func setCollcetionView() { 139 | collectionView.backgroundColor = .lightGray 140 | collectionViewFlowlayout.scrollDirection = .vertical 141 | collectionView.showsVerticalScrollIndicator = false 142 | 143 | /// cell register 144 | collectionView.register(CustomCVC.self, forCellWithReuseIdentifier: CustomCVC.identifier) 145 | collectionView.register(CustomHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: CustomHeaderView.identifier) 146 | 147 | /// collectionView를 상단바 부분부터 시작하도록 하는 코드 148 | // collectionView.contentInset.top = -UIApplication.shared.statusBarFrame.height 149 | // collectionView.contentInsetAdjustmentBehavior = .never 150 | 151 | /// delegate, datasource 152 | collectionView.delegate = self 153 | collectionView.dataSource = self 154 | 155 | /// sticky header을 가능하도록 하는 코드 156 | collectionViewFlowlayout.sectionHeadersPinToVisibleBounds = true 157 | } 158 | } 159 | 160 | // MARK: - extension 161 | extension ViewController: UICollectionViewDelegate { 162 | func numberOfSections(in collectionView: UICollectionView) -> Int { 163 | return dateList.count 164 | } 165 | } 166 | 167 | extension ViewController: UICollectionViewDataSource { 168 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 169 | var itemCount = 0 170 | var indexPath = 0 171 | 172 | while indexPath < dummyDataList.count { 173 | if dateList[section] == dummyDataList[indexPath]["date"] { 174 | itemCount += 1 175 | indexPath += 1 176 | } else { 177 | indexPath += 1 178 | } 179 | } 180 | 181 | return itemCount 182 | } 183 | 184 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 185 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCVC.identifier, for: indexPath) as? CustomCVC else { return UICollectionViewCell() } 186 | 187 | var alist: [String:String] = [:] 188 | 189 | // TODO: - section별 데이터 넣기 190 | switch indexPath.section { 191 | case 0: 192 | alist = firstList[indexPath.item] as! [String : String] 193 | case 1: 194 | alist = secondList[indexPath.item] as! [String : String] 195 | case 2: 196 | alist = thirdList[indexPath.item] as! [String : String] 197 | case 3: 198 | alist = fourthList[indexPath.item] as! [String : String] 199 | case 4: 200 | alist = fifthList[indexPath.item] as! [String : String] 201 | case 5: 202 | alist = sixthList[indexPath.item] as! [String : String] 203 | case 6: 204 | alist = seventhList[indexPath.item] as! [String : String] 205 | default: 206 | alist = firstList[indexPath.item] as! [String : String] 207 | } 208 | 209 | cell.titleLabel.text = "\(String(describing: alist["sparkCount"]!))" 210 | cell.userNameLabel.text = "\(String(describing: alist["userName"]!))" 211 | cell.backgroundColor = .white 212 | 213 | return cell 214 | } 215 | 216 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 217 | guard let cell = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "CustomHeaderView", for: indexPath) as? CustomHeaderView else { return UICollectionReusableView() } 218 | cell.dateLabel.text = "\(dateList[indexPath.section])" 219 | cell.backgroundColor = .systemIndigo 220 | 221 | return cell 222 | } 223 | 224 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 225 | let width = UIScreen.main.bounds.width 226 | 227 | return CGSize(width: width, height: 60) 228 | } 229 | 230 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 231 | if (scrollView.contentOffset.y + 1) >= (scrollView.contentSize.height - scrollView.frame.size.height) { 232 | if index < 1 { 233 | dummyDataList.append(contentsOf: newDummyDataList) 234 | setData(datalist: newDummyDataList) 235 | collectionView.reloadData() 236 | index += 1 237 | } else { 238 | print("끝입니다") 239 | } 240 | } 241 | } 242 | } 243 | 244 | extension ViewController: UICollectionViewDelegateFlowLayout { 245 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 246 | 247 | let width = UIScreen.main.bounds.width 248 | 249 | return CGSize(width: width, height: 120) 250 | } 251 | 252 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 253 | 254 | return 20 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'Stopwatch' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for Stopwatch 9 | pod 'SnapKit' 10 | 11 | end 12 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SnapKit (5.0.1) 3 | 4 | DEPENDENCIES: 5 | - SnapKit 6 | 7 | SPEC REPOS: 8 | trunk: 9 | - SnapKit 10 | 11 | SPEC CHECKSUMS: 12 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb 13 | 14 | PODFILE CHECKSUM: 70ac4033f551479a325066b8fa35bd6a8b59e8e2 15 | 16 | COCOAPODS: 1.11.2 17 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Stopwatch.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Stopwatch.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Stopwatch.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Stopwatch.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Stopwatch/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Stopwatch 4 | // 5 | // Created by 양수빈 on 2022/01/07. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Stopwatch/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 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Stopwatch/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Stopwatch/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Stopwatch/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Stopwatch/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Stopwatch/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Stopwatch/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Stopwatch 4 | // 5 | // Created by 양수빈 on 2022/01/07. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Stopwatch/Stopwatch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stopwatch.swift 3 | // Stopwatch 4 | // 5 | // Created by 양수빈 on 2022/01/07. 6 | // 7 | 8 | import Foundation 9 | 10 | class Stopwatch: NSObject { 11 | var counter: Double 12 | var timer: Timer 13 | 14 | override init() { 15 | counter = 0.0 16 | timer = Timer() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Practice-YSB/Stopwatch/Stopwatch/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Stopwatch 4 | // 5 | // Created by 양수빈 on 2022/01/07. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | class ViewController: UIViewController { 13 | 14 | let stopwatch: Stopwatch = Stopwatch() 15 | var timerLabel = UILabel() 16 | var resetButton = UIButton() 17 | var startButton = UIButton() 18 | var stopTimeLabel = UILabel() /// 멈췄을 때 시간 표시하는 라벨 19 | var isPlay: Bool = false 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | setLayout() 25 | setUI() 26 | setAddTarget() 27 | } 28 | 29 | func setLayout() { 30 | view.addSubview(timerLabel) 31 | view.addSubview(resetButton) 32 | view.addSubview(startButton) 33 | view.addSubview(stopTimeLabel) 34 | 35 | timerLabel.snp.makeConstraints { make in 36 | make.centerX.equalToSuperview() 37 | make.top.equalToSuperview().offset(300) 38 | } 39 | 40 | startButton.snp.makeConstraints { make in 41 | make.top.equalTo(timerLabel.snp.bottom).offset(40) 42 | make.leading.equalToSuperview().offset(80) 43 | make.width.height.equalTo(100) 44 | } 45 | 46 | resetButton.snp.makeConstraints { make in 47 | make.top.equalTo(timerLabel.snp.bottom).offset(40) 48 | make.trailing.equalToSuperview().inset(80) 49 | make.width.height.equalTo(100) 50 | } 51 | 52 | stopTimeLabel.snp.makeConstraints { make in 53 | make.centerX.equalToSuperview() 54 | make.top.equalTo(resetButton.snp.bottom).offset(40) 55 | } 56 | } 57 | 58 | func setUI() { 59 | timerLabel.text = "00:00:00" 60 | timerLabel.font = .systemFont(ofSize: 40) 61 | 62 | stopTimeLabel.text = "-" 63 | 64 | startButton.setTitle("start", for: .normal) 65 | resetButton.setTitle("reset", for: .normal) 66 | 67 | startButton.backgroundColor = .blue 68 | resetButton.backgroundColor = .red 69 | 70 | startButton.layer.cornerRadius = 50 71 | resetButton.layer.cornerRadius = 50 72 | } 73 | 74 | func setAddTarget() { 75 | startButton.addTarget(self, action: #selector(startPauseTimer(_:)), for: .touchUpInside) 76 | resetButton.addTarget(self, action: #selector(resetTimer(_:)), for: .touchUpInside) 77 | } 78 | 79 | /// start 버튼을 눌렀을 때 isPlay의 상태에 따라 버튼, 라벨 상태 변경 80 | @objc 81 | func startPauseTimer(_ sender: AnyObject) { 82 | /// 실행중 X, 스톱워치 시작 + 버튼 변경 83 | /// 실행중 O, 스톱워치 84 | if !isPlay { 85 | unowned let weakSelf = self 86 | 87 | /// 0.035초마다 updateMainTimer 함수 호출하는 타이머 88 | stopwatch.timer = Timer.scheduledTimer(timeInterval: 0.035, target: weakSelf, selector: #selector(updateMainTimer), userInfo: nil, repeats: true) 89 | 90 | /// RunLoop에서 timer 객체를 추가해서 관리 91 | RunLoop.current.add(stopwatch.timer, forMode: .common) 92 | 93 | /// 실행X -> 실행O 94 | isPlay = true 95 | changeButton(startButton, title: "Stop", titleColor: .white) 96 | } else { 97 | /// 버튼 눌렀을 때의 timerLabel의 값을 stopTimeLabel에 넣음 98 | stopTimeLabel.text = timerLabel.text 99 | 100 | /// RunLoop 객체로부터 timer를 제거하기 위한 함수 (반복 타이머 중지) 101 | stopwatch.timer.invalidate() 102 | 103 | /// 실행O -> 실행X 104 | isPlay = false 105 | changeButton(startButton, title: "Start", titleColor: .white) 106 | } 107 | } 108 | 109 | @objc 110 | func resetTimer(_ sender: AnyObject) { 111 | /// 실행중 X (스톱워치가 멈춘 상태라면), reset 112 | /// 실행중 O (스톱워치가 돌아가는 상태라면), print 113 | if !isPlay { 114 | resetMainTimer() 115 | } else { 116 | print("먼저 멈추지?") 117 | } 118 | } 119 | 120 | @objc 121 | func updateMainTimer() { 122 | updateTimer(stopwatch, label: timerLabel) 123 | } 124 | 125 | func changeButton(_ button: UIButton, title: String, titleColor: UIColor) { 126 | button.setTitle(title, for: UIControl.State()) 127 | button.setTitleColor(titleColor, for: UIControl.State()) 128 | } 129 | 130 | func resetMainTimer() { 131 | resetTimer(stopwatch, label: timerLabel) 132 | stopTimeLabel.text = "-" 133 | } 134 | 135 | /// stopwatch를 멈추고, label을 reset하는 함수 136 | func resetTimer(_ stopwatch: Stopwatch, label: UILabel) { 137 | stopwatch.timer.invalidate() 138 | stopwatch.counter = 0.0 139 | label.text = "00:00:00" 140 | } 141 | 142 | /// timer를 증가시키면서 label의 값에 반영시키는 함수 143 | func updateTimer(_ stopwatch: Stopwatch, label: UILabel) { 144 | stopwatch.counter = stopwatch.counter + 0.035 145 | 146 | var minutes: String = "\((Int)(stopwatch.counter / 60))" 147 | if (Int)(stopwatch.counter / 60) < 10 { 148 | minutes = "0\((Int)(stopwatch.counter / 60))" 149 | } 150 | 151 | var seconds: String = String(format: "%.2f", (stopwatch.counter.truncatingRemainder(dividingBy: 60))) 152 | if stopwatch.counter.truncatingRemainder(dividingBy: 60) < 10 { 153 | seconds = "0" + seconds 154 | } 155 | 156 | label.text = minutes + ":" + seconds 157 | } 158 | } 159 | 160 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'socialLogin' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for socialLogin 9 | pod 'SnapKit', '~> 5.0.0' 10 | 11 | end 12 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SnapKit (5.0.1) 3 | 4 | DEPENDENCIES: 5 | - SnapKit (~> 5.0.0) 6 | 7 | SPEC REPOS: 8 | trunk: 9 | - SnapKit 10 | 11 | SPEC CHECKSUMS: 12 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb 13 | 14 | PODFILE CHECKSUM: fb4ca78265042e20c9a4bf8dfa049e55b9cb7f34 15 | 16 | COCOAPODS: 1.11.2 17 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/socialLogin.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/socialLogin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/socialLogin.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/socialLogin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/socialLogin/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // socialLogin 4 | // 5 | // Created by 양수빈 on 2022/01/05. 6 | // 7 | 8 | import UIKit 9 | import AuthenticationServices 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | let appleIDProvider = ASAuthorizationAppleIDProvider() 19 | appleIDProvider.getCredentialState(forUserID: UserDefaults.standard.string(forKey: "setUserIdentifier") ?? "") { credentialState, error in 20 | switch credentialState { 21 | case .authorized: 22 | // Apple ID Credential is valid 23 | // 로그인된 상태 -> 홈뷰 24 | print("해당 ID는 연동되어 있습니다.") 25 | case .revoked: 26 | // Apple ID Credential revoked, handle unlink 27 | // 로그아웃된 상태 -> 로그인뷰 28 | print("해당 ID는 연동되어 있지 않습니다.") 29 | case .notFound: 30 | // Credential not found, show login UI 31 | // 잘못된 userIdentifier -> 로그인뷰 32 | print("해당 ID를 찾을 수 없습니다.") 33 | default: 34 | break 35 | } 36 | } 37 | 38 | return true 39 | } 40 | 41 | // MARK: UISceneSession Lifecycle 42 | 43 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 44 | // Called when a new scene session is being created. 45 | // Use this method to select a configuration to create the new scene with. 46 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 47 | } 48 | 49 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 50 | // Called when the user discards a scene session. 51 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 52 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 53 | } 54 | 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/socialLogin/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 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/socialLogin/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/socialLogin/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/socialLogin/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/socialLogin/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/socialLogin/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/socialLogin/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // socialLogin 4 | // 5 | // Created by 양수빈 on 2022/01/05. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/socialLogin/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // socialLogin 4 | // 5 | // Created by 양수빈 on 2022/01/05. 6 | // 7 | 8 | import UIKit 9 | 10 | import AuthenticationServices 11 | import SnapKit 12 | 13 | class ViewController: UIViewController { 14 | 15 | let appleLoginButton = ASAuthorizationAppleIDButton(type: .continue, style: .black) 16 | var nameLabel = UILabel() 17 | var emailLabel = UILabel() 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | setLayout() 23 | setUI() 24 | } 25 | 26 | func setLayout() { 27 | view.addSubview(appleLoginButton) 28 | view.addSubview(nameLabel) 29 | view.addSubview(emailLabel) 30 | 31 | appleLoginButton.snp.makeConstraints { make in 32 | make.center.equalToSuperview() 33 | } 34 | 35 | nameLabel.snp.makeConstraints { make in 36 | make.centerX.equalToSuperview() 37 | make.top.equalTo(appleLoginButton.snp.bottom).offset(30) 38 | } 39 | 40 | emailLabel.snp.makeConstraints { make in 41 | make.centerX.equalToSuperview() 42 | make.top.equalTo(nameLabel.snp.bottom).offset(20) 43 | } 44 | } 45 | 46 | func setUI() { 47 | nameLabel.text = "name" 48 | emailLabel.text = "email" 49 | 50 | appleLoginButton.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside) 51 | } 52 | 53 | @objc 54 | func handleAuthorizationAppleIDButtonPress() { 55 | let appleIDProvider = ASAuthorizationAppleIDProvider() 56 | let request = appleIDProvider.createRequest() 57 | request.requestedScopes = [.fullName, .email] 58 | 59 | let authorizationController = ASAuthorizationController(authorizationRequests: [request]) 60 | authorizationController.delegate = self 61 | authorizationController.presentationContextProvider = self 62 | authorizationController.performRequests() 63 | } 64 | } 65 | 66 | extension ViewController: ASAuthorizationControllerDelegate { 67 | func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { 68 | switch authorization.credential { 69 | case let appleIDCredential as ASAuthorizationAppleIDCredential: 70 | 71 | let userIdentifier = appleIDCredential.user 72 | 73 | if let fullName = appleIDCredential.fullName, 74 | let email = appleIDCredential.email { 75 | 76 | let username: String = "\(fullName.familyName ?? "")" + "\(fullName.givenName ?? "")" 77 | 78 | print("fullName: \(fullName)") 79 | print("userName: \(username), email: \(email)") 80 | 81 | UserDefaults.standard.set(username, forKey: "setFullName") 82 | UserDefaults.standard.set(email, forKey: "setEmail") 83 | } 84 | 85 | if let fullName = appleIDCredential.fullName, 86 | let email = appleIDCredential.email, 87 | let authorizationCode = appleIDCredential.authorizationCode, 88 | let identityToken = appleIDCredential.identityToken, 89 | let authString = String(data: authorizationCode, encoding: .utf8) { 90 | 91 | print("authorizationCode: \(authorizationCode)") 92 | print("identityToken: \(identityToken)") 93 | print("authString: \(authString)") 94 | } 95 | 96 | print("useridentifier: \(userIdentifier)") 97 | UserDefaults.standard.set(userIdentifier, forKey: "setUserIdentifier") 98 | 99 | nameLabel.text = UserDefaults.standard.string(forKey: "setFullName") 100 | emailLabel.text = UserDefaults.standard.string(forKey: "setEmail") 101 | 102 | case let passwordCredential as ASPasswordCredential: 103 | 104 | let username = passwordCredential.user 105 | let password = passwordCredential.password 106 | 107 | print("username: \(username), password: \(password)") 108 | 109 | default: 110 | break 111 | } 112 | } 113 | 114 | func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { 115 | print("login error wow") 116 | } 117 | } 118 | 119 | extension ViewController: ASAuthorizationControllerPresentationContextProviding { 120 | func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { 121 | return self.view.window! 122 | } 123 | } 124 | 125 | -------------------------------------------------------------------------------- /Practice-YSB/socialLogin/socialLogin/socialLogin.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.applesignin 6 | 7 | Default 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spark-Practice-iOS 2 | 3 | Spark🎇 4 | 5 | | # | 작업 내용 | 작업한 사람 | 📎 | 6 | |:----------:|:-------------:|:------:|:------:| 7 | | 1 | 카카오 소셜로그인 구현 | [L-j-h-c](https://github.com/L-j-h-c) | [📎](https://github.com/TeamSparker/Spark-Practice-iOS/pull/9) | 8 | | 2 | 애플 소셜로그인 구현 | [yangsubinn](https://github.com/yangsubinn) | [📎](https://github.com/TeamSparker/Spark-Practice-iOS/pull/12) | 9 | | 3 | 플로팅버튼 구현 | [L-j-h-c](https://github.com/L-j-h-c) | | 10 | | 4 | 플로팅버튼 구현 | [yangsubinn](https://github.com/yangsubinn) | [📎](https://github.com/TeamSparker/Spark-Practice-iOS/pull/18) | 11 | | 5 | Carousel 구현 및 애니메이션 연습 | [L-j-h-c](https://github.com/L-j-h-c) | | 12 | | 6 | 스톱워치 | [yangsubinn](https://github.com/yangsubinn) | [📎](https://github.com/TeamSparker/Spark-Practice-iOS/pull/15) | 13 | | 7 | StickyHeader + 무한스크롤 | [yangsubinn](https://github.com/yangsubinn) | [📎](https://github.com/TeamSparker/Spark-Practice-iOS/pull/23) | 14 | --------------------------------------------------------------------------------