From f7ab24c05ec1919dedb70041f6bf255df2c90b8a Mon Sep 17 00:00:00 2001 From: xongjaemin Date: Thu, 5 May 2022 03:33:57 +0900 Subject: [PATCH 1/9] dev : news app basic --- .../UserInterfaceState.xcuserstate | Bin 36835 -> 37028 bytes .../UserInterfaceState.xcuserstate | Bin 30319 -> 35997 bytes .../Base.lproj/Main.storyboard | 10 +- .../CatStaGram.xcodeproj/project.pbxproj | 721 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 70399 bytes .../xcdebugger/Breakpoints_v2.xcbkptlist | 6 + .../xcschemes/xcschememanagement.plist | 14 + .../CatStaGram/CatStaGram/AppDelegate.swift | 36 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 +++ .../Assets.xcassets/Colors/Contents.json | 6 + .../Contents.json | 38 + .../facebookColor.colorset/Contents.json | 38 + .../CatStaGram/Assets.xcassets/Contents.json | 6 + .../ic_catstagram_logo.imageset/Contents.json | 21 + .../ic_catstagram_logo.png | Bin 0 -> 11616 bytes .../Contents.json" | 6 + .../ic_login_facebook.imageset/Contents.json" | 21 + .../ic_login_facebook.png" | Bin 0 -> 6350 bytes .../ic_login_hidden.imageset/Contents.json" | 21 + .../ic_login_hidden.png" | Bin 0 -> 11970 bytes .../ic_login_show.imageset/Contents.json" | 21 + .../ic_login_show.imageset/ic_login_show.png" | Bin 0 -> 14608 bytes .../\353\246\264\354\212\244/Contents.json" | 6 + .../ic_reels_comment.imageset/Contents.json" | 21 + .../ic_reels_comment.png" | Bin 0 -> 12247 bytes .../ic_reels_heart.imageset/Contents.json" | 21 + .../ic_reels_heart.png" | Bin 0 -> 17486 bytes .../Contents.json" | 21 + .../ic_reels_heart_full.png" | Bin 0 -> 11641 bytes .../ic_reels_more.imageset/Contents.json" | 21 + .../ic_reels_more.imageset/ic_reels_more.png" | Bin 0 -> 4354 bytes .../ic_reels_send.imageset/Contents.json" | 21 + .../ic_reels_send.imageset/ic_reels_send.png" | Bin 0 -> 18159 bytes .../\353\247\210\354\235\264/Contents.json" | 6 + .../ic_my_hamburger.imageset/Contents.json" | 21 + .../ic_my_hamburger.png" | Bin 0 -> 2793 bytes .../ic_my_invite.imageset/Contents.json" | 21 + .../ic_my_invite.imageset/ic_my_invite.png" | Bin 0 -> 13230 bytes .../ic_my_upload.imageset/Contents.json" | 21 + .../ic_my_upload.imageset/ic_my_upload.png" | Bin 0 -> 9855 bytes .../\355\231\210/Contents.json" | 6 + .../ic_bookmark_black.imageset/Contents.json" | 21 + .../ic_bookmark_black.png" | Bin 0 -> 5921 bytes .../ic_bookmark_white.imageset/Contents.json" | 21 + .../ic_bookmark_white.png" | Bin 0 -> 5408 bytes .../Contents.json" | 21 + .../ic_homd_search_light.png" | Bin 0 -> 14511 bytes .../ic_home_comment.imageset/Contents.json" | 21 + .../ic_home_comment.png" | Bin 0 -> 10913 bytes .../ic_home_heart.imageset/Contents.json" | 21 + .../ic_home_heart.imageset/ic_home_heart.png" | Bin 0 -> 17743 bytes .../Contents.json" | 21 + .../ic_home_heart_full.png" | Bin 0 -> 11641 bytes .../Contents.json" | 21 + .../ic_home_home_black.png" | Bin 0 -> 5094 bytes .../Contents.json" | 21 + .../ic_home_home_white.png" | Bin 0 -> 9869 bytes .../ic_home_more.imageset/Contents.json" | 21 + .../ic_home_more.imageset/ic_home_more.png" | Bin 0 -> 3425 bytes .../ic_home_reels.imageset/Contents.json" | 21 + .../ic_home_reels.imageset/ic_home_reels.png" | Bin 0 -> 12969 bytes .../Contents.json" | 21 + .../ic_home_search_bold.png" | Bin 0 -> 12367 bytes .../ic_home_send.imageset/Contents.json" | 21 + .../ic_home_send.imageset/ic_home_send.png" | Bin 0 -> 12495 bytes .../Contents.json" | 21 + .../ic_home_shop_black.png" | Bin 0 -> 7889 bytes .../Contents.json" | 21 + .../ic_home_shop_white.png" | Bin 0 -> 7004 bytes .../ic_home_upload.imageset/Contents.json" | 21 + .../ic_home_upload.png" | Bin 0 -> 9855 bytes .../Base.lproj/LaunchScreen.storyboard | 25 + .../CatStaGram/Base.lproj/Main.storyboard | 536 +++++++++++++ .../Home/Cell/FeedTableViewCell.swift | 60 ++ .../Home/Cell/FeedTableViewCell.xib | 183 +++++ .../Home/Cell/StoryCollectionViewCell.swift | 26 + .../Home/Cell/StoryCollectionViewCell.xib | 100 +++ .../Home/Cell/StoryTableViewCell.swift | 42 + .../Home/Cell/StoryTableViewCell.xib | 44 ++ .../CatStaGram/Home/HomeViewController.swift | 81 ++ .../CatStaGram/CatStaGram/Info.plist | 25 + .../CatStaGram/LoginViewController.swift | 82 ++ .../PostCell/PostCollectionViewCell.swift | 22 + .../Cell/PostCell/PostCollectionViewCell.xib | 38 + .../ProfileCollectionViewCell.swift | 45 ++ .../ProfileCell/ProfileCollectionViewCell.xib | 156 ++++ .../Profile/ProfileViewController.swift | 98 +++ .../CatStaGram/RegisterViewController.swift | 140 ++++ .../CatStaGram/CatStaGram/SceneDelegate.swift | 52 ++ .../UIViewController+Extension.swift | 21 + .../CatStaGram/UIViewExtension.swift | 20 + .../CatStaGram/CatStaGram/UserInfo.swift | 15 + .../CatStaGramTests/CatStaGramTests.swift | 36 + .../CatStaGramUITests/CatStaGramUITests.swift | 42 + .../CatStaGramUITestsLaunchTests.swift | 32 + .../NewsApp/NewsApp.xcodeproj/project.pbxproj | 621 +++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 40344 bytes .../xcschemes/xcschememanagement.plist | 14 + jaem/week6/NewsApp/NewsApp/AppDelegate.swift | 36 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 +++ .../NewsApp/Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../NewsApp/Base.lproj/Main.storyboard | 144 ++++ jaem/week6/NewsApp/NewsApp/DetailVC.swift | 36 + jaem/week6/NewsApp/NewsApp/Info.plist | 25 + .../NewsApp/NewsApp/NewsTableViewCell.swift | 28 + .../NewsApp/NewsApp/NewsTableViewCell.xib | 65 ++ .../week6/NewsApp/NewsApp/SceneDelegate.swift | 52 ++ .../NewsApp/NewsApp/ViewController.swift | 124 +++ .../NewsApp/NewsAppTests/NewsAppTests.swift | 36 + .../NewsAppUITests/NewsAppUITests.swift | 42 + .../NewsAppUITestsLaunchTests.swift | 32 + 118 files changed, 4913 insertions(+), 8 deletions(-) create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.pbxproj create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/AppDelegate.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Colors/Contents.json create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Colors/disabledButtonColor.colorset/Contents.json create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Colors/facebookColor.colorset/Contents.json create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Contents.json create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/ic_catstagram_logo.imageset/Contents.json create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/ic_catstagram_logo.imageset/ic_catstagram_logo.png create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_facebook.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_facebook.imageset/ic_login_facebook.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_hidden.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_hidden.imageset/ic_login_hidden.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_show.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_show.imageset/ic_login_show.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_comment.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_comment.imageset/ic_reels_comment.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_heart.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_heart.imageset/ic_reels_heart.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_heart_full.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_heart_full.imageset/ic_reels_heart_full.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_more.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_more.imageset/ic_reels_more.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_send.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_send.imageset/ic_reels_send.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_hamburger.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_hamburger.imageset/ic_my_hamburger.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_invite.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_invite.imageset/ic_my_invite.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_upload.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_upload.imageset/ic_my_upload.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_black.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_black.imageset/ic_bookmark_black.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_white.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_white.imageset/ic_bookmark_white.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_homd_search_light.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_homd_search_light.imageset/ic_homd_search_light.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_comment.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_comment.imageset/ic_home_comment.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart.imageset/ic_home_heart.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart_full.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart_full.imageset/ic_home_heart_full.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_black.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_black.imageset/ic_home_home_black.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_white.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_white.imageset/ic_home_home_white.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_more.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_more.imageset/ic_home_more.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_reels.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_reels.imageset/ic_home_reels.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_search_bold.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_search_bold.imageset/ic_home_search_bold.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_send.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_send.imageset/ic_home_send.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_black.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_black.imageset/ic_home_shop_black.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_white.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_white.imageset/ic_home_shop_white.png" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_upload.imageset/Contents.json" create mode 100644 "jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_upload.imageset/ic_home_upload.png" create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Base.lproj/LaunchScreen.storyboard create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Base.lproj/Main.storyboard create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/FeedTableViewCell.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/FeedTableViewCell.xib create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryCollectionViewCell.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryCollectionViewCell.xib create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryTableViewCell.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryTableViewCell.xib create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/HomeViewController.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Info.plist create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/LoginViewController.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/PostCell/PostCollectionViewCell.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/PostCell/PostCollectionViewCell.xib create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/ProfileCell/ProfileCollectionViewCell.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/ProfileCell/ProfileCollectionViewCell.xib create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/ProfileViewController.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/RegisterViewController.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/SceneDelegate.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/UIViewController+Extension.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/UIViewExtension.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGram/UserInfo.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGramTests/CatStaGramTests.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGramUITests/CatStaGramUITests.swift create mode 100644 jaem/week5/CatStaGram/CatStaGram/CatStaGramUITests/CatStaGramUITestsLaunchTests.swift create mode 100644 jaem/week6/NewsApp/NewsApp.xcodeproj/project.pbxproj create mode 100644 jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 jaem/week6/NewsApp/NewsApp.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 jaem/week6/NewsApp/NewsApp/AppDelegate.swift create mode 100644 jaem/week6/NewsApp/NewsApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 jaem/week6/NewsApp/NewsApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 jaem/week6/NewsApp/NewsApp/Assets.xcassets/Contents.json create mode 100644 jaem/week6/NewsApp/NewsApp/Base.lproj/LaunchScreen.storyboard create mode 100644 jaem/week6/NewsApp/NewsApp/Base.lproj/Main.storyboard create mode 100644 jaem/week6/NewsApp/NewsApp/DetailVC.swift create mode 100644 jaem/week6/NewsApp/NewsApp/Info.plist create mode 100644 jaem/week6/NewsApp/NewsApp/NewsTableViewCell.swift create mode 100644 jaem/week6/NewsApp/NewsApp/NewsTableViewCell.xib create mode 100644 jaem/week6/NewsApp/NewsApp/SceneDelegate.swift create mode 100644 jaem/week6/NewsApp/NewsApp/ViewController.swift create mode 100644 jaem/week6/NewsApp/NewsAppTests/NewsAppTests.swift create mode 100644 jaem/week6/NewsApp/NewsAppUITests/NewsAppUITests.swift create mode 100644 jaem/week6/NewsApp/NewsAppUITests/NewsAppUITestsLaunchTests.swift diff --git a/jaem/week2/CarrotmarketClone/CarrotmarketClone.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week2/CarrotmarketClone/CarrotmarketClone.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate index 7424d26e62b9fc347e4f94413ec2cde33db4ae84..698fc0a277b98f6af3e62ad7a835425c256bd042 100644 GIT binary patch delta 17523 zcmcI~2Y3@l*Y3{t6~%Jz1zYY-vMtGS@0N`%8*GDdlVwZDy@O4aCJ6};LR(7c*q9P} zASBe#dm!}C2_XbXs38PG0^E^}34FUd*nvyQ7&o=e(!9vvXq+-r0^%)d2Cd zG7pe9?ZZ6g0CogBiXET!!v$c?ZwIM{R(urxF+K_Z1fPsg!KdQW@agzmd>%d@@4z?U zTkx&;ZhSv}06&5s#lOK%;3x63_&NMCeg(gd-^72yf5so;kMZ9Ki~s~q5Clo|Ci)P4 z2{WP};ZArEBEpmKBD@J7B7_Jh6hsseOT-b0L=uroD2e_CB7?{xvWXlbm&hZA5Cudb zp(aWR4N*o6CF+Uc#Aw1uKw=y*ftW~4CZ-TPF@so3tRvPFpA#F1jl>tkCgMxtD`GRT zh1fytBlZ(ViEoHA#987T@f~rV_@1~<{6O3#?h*Hi2gF0-H{vhi1@V%2MfM_llZHNI zU($>elIElZX-Qg<)}#&TN_vq2q?nYDQc^)ikTGN|nMkIR{mERikkpVmvXZPN>&PZ@ z68Q-^nVdpSC8v?oNuHcR&Ln4%v&ng62f2joBv+EF$c^L|x(=N0t?Lqs~0dyc8L`Tq(bQB#;$I!8K934+5(CKu4I)g5u z)pRMXq08uUT1)HbI=Yc=qKDDL=@IludJH|5hV**|dK~>B{So~!J&FE=o<>inKc(l> z3+Uza3i>m8CB2sZoZdihrnl4I&?o4V^jZ2GeUZLIU!%XLpVNQRf6*`Km-H(JV*rCQ z1Vb_u!!bg}oUvu>7&oRLa zW_=8-KPzQrYy_LYCbC)VU^bs^U>n&cwwY~Vhq1%iR`ZnR zJDdHKUCg$#+t}^w4)$wyC%cQ?&F*3MvisQm>|yo=WuK471+&p49n z&Gq3dIa|(-3*Z8|ATF586LS(SgbU@uxNuIvMRPG+EEmTmap_!tE{n_N25@;?A*bd_ zxhk%jtKn+7My{1><3@0hdyjjc`+ytAP2@i2rf@U4S=?-H9k-tQoX@6RbH3q@amTrD zxf9$;?i6>P`+>W`{m9+qe&TL%kGNmCCjv@93m5?_-~<9eUxB&6LSQ3s61WLG1Y$v$ zKq^oOA_OslSV5v7NuU&@2{QRZbS&7*vrLPn8|H)g8hac2prfx*U=$i1_$f>$pT)-V z64nW@{G;CX{0i36l1?jYsHtpj5sS|NoFFNh5%Uil!wuJjRxBsz)w@q$GoiVKrIodf zt)0Dtqm#3Xt6M*J4-ZcdZyygo|A4@tV71Uzm6I~8rY5JnNvEqDTv?~BZ%t{^)ar^= zId5GRs&aB#M%3ssHOG?(<;Pz|ds z(>3w&oK2+T+k@p+*630yYigRi$42ebn%|m=DkrzHrA9Z{bi2Q91b>8cipfVeHDzhq zHuNQ}Ia5b#sHJT$h%_=W(vNgH$Tthw-ok>NNa#R(}P#u;fNVYFwBq|t7j&_NOs8YY#=!xc&C{WAs* z8k}FMDbrO}RoB(?MFLThTIe8l6T3;m!o+S0vFYpUi*UKrO)V59hK0)#<>4uT(&Uij zz%Y4=G*FQskp;>_74lGdxHu&&IfOR~>i7o&dpaUADmpHa@7v4KCMMR%8T)j^Cmm?;KRax2m+Fl~NtN}zz}8Bi#03(ANbySj-%Az(klY7J|vKG^_=ijLpYZVB4_c*cI#%Fas{Y3j~35kOPW91sD(J z1H*E#7Hk3Azz(nr>;e10L2wux0Y||xa2%Wfr@(1&7JLUTfQ#TVxC;Kn0q%-N;pKQG zz8z)wJ1D0=L%CdlGPxOHPFN8(gdO34GI|s-0Hx|YVlXk5_?VbV%p*Ec3jUJVNgN;! z5l2wseT)(-n?d@aWT{7KvKl4IdXys9kQ>O|fgdb;d2mt;X#rU_9^p8Q?Z9F)JrSzd#K< z(6JfYg5pXfYT6qe(Z(2b4ndBamXw^_)}TRdtW9gt)i$ezu6(_fO>bqTu1V8WUNHh; z*K~uAvJ&z$taN;xl}L4}YtH+y{fmuJr!WN;fgQvSAp~U(YtfX|=;E82G$WRvdxx^NmA{1?aDL*Wq9+ zGCFL0&sGjd(~$fQyX-!`AOQ0)YK=PMP$aQ~{oy{om@h~GiN+cvngjpP{x|-agB!1^ zwdb83-Prx@Am7-8ew^%h#Ohcls03A@8q^qv8AljL8^;)9c@L+}j0*UIW@G)vADkj^ z(%Rm+AMfVcvRIO`Ctf{ZlokC%jhi;x` zU5yXtqNWb!9ga@Cz|EIHq4)1+-(E-0+&w!yMEnw0F+bMLC#dT;Cl#Ub`^+0hZsjj@ z1O#;i272^BrXyJF5my)2b?Ff|o=-Yby zb6(=V-q+FitMRe%Dg;gtI2s>9;BXEUq9|63nSm0Z#y8?$;F~}>{uM6>NO9eU9qUB# zX$Sr_zSDT!c*FRU@wPGcG^hs+Xp}vCeSkl45I@8(2oN#Hy6}062MKok_W>+(s_XiW z>AKfx{5#bCJbnSc$Xf({N?gUS@oNI@+`q?S)I!UoLAgow<-=-qbuAh7jJKQ2V=}6u*yO;Kv2I^?ro^nuqe~ zz>E(35&wCRyx$Z2Ip)`i|BgS!pW%NPe=|NY{%(BQiEY9Ez+d1mjWIkIY_O#W7HONH z3C8$`@p%Wqp}_Pfg36-cXe%LMhxsi>vDt#KMDf`gMQB_8VX&2}J&MzggcIRRxL_U- zkPuiwU=4u{1hx>^@xNy-mJ`0%vE_sx;ZFn*fkY4yOo$<%AYdU7K+qe4z7Pl@ut3Ts zLNPNUjF1vCLe4)Ciwsh^I4E2!mCGfea=BO%CJ7603zSJif+XQ#QmI59A{B>+$`xY* z+(t>nvd|!jBwQLACYOeXi^D<#+!Ru2kQ`M9;?R&#nOGXCkfA(jii|`A5s6~pAWh9M zT?Y|?#qe9@4-ZFEkV+&$iVzt}qA2%< zNW)~IrYXon&s38S7ug3X_Khg@?() zWb&{OlU}34CI4kCsXQ!5j*1HOKSUu35lbY0%aztWmKlFkWp67rsVht&4N^#DGBkN) z3b7C!4yFuVaQIQ z3WZ!Iktk$xG?)a*7b-?J6idW%u~dc>(G}u|BJ5Z@Q4E2-F}5IKNI_s|cwmdTi5DxQ zc!yADLph;U3$0A)TU*&u-=suLpuTB@DTj9uI+Mr$>$88vFG5dL5S2s~zLuyVYKb}s z`a$3WfiDCuUGZx$1l|w~dMAJp4Maj?cLXyv^n}3Gc+Y4wUNh>t=iZ7!C(%ZXAVxyq z27x;S9&aKVp$DTuGa83~7AkUl9nes2XnP&;h!0IM4FZuVs0m*OHR5CZA}EExi&uvY zvv?DTh>ua-{>XIur!W`8Orjn0TSd$wW)q(hbBMX9jGIp^AQlpfh{X{2LEsNT00e;$ z1VIoC0g4V12tptTg&=Gd(a}{Zc@WEp<-`hr$|hnJhNP2r)l4XQghLPmK`fdox=84* zpB!@Phc%V!66>|PWPZv}nvWZ_l~0nbPTFd+-ZltirfLUOc*NHz$`d=mXww0K+&HT1 zWQ-Ns_MirpnwU*HaS#GUJ8>9-2!6fR%aJ%noWN{4h~vb!5JW-{)j^!ZP})Q#96wCn z&u{0$ZiMypa)G#PYJQQp1VJ1G@g3L};-nExJeuDnx8)ZWF=A8r=($Rx_I5GaueyUSJL3GvJ{$nV5c2+|-( z?;!ploj#2^^b1I)7j1SaW5_WK)`2rmWi;F9!4xFmf@ zUlM_#5CY^~C2w#kByE9@33x@Qs%%64!=KExc7L6P{|Qr4)(unj+b|^~yJ4!~FGsWl zyn!hh*$q=o7Zk`ORJ@SM5NJ{LOe!%mGL1~(QxSROpuZJS1A+csMWEoage)dYNHqi%5L7}?WsH@-_P}=((Nl-~iw3H? zHBfERfZ?^HzR`g3t`U0;7qXc&Ajc$I$YJDgvXyKjN01}QQRHZH3^^78WZ8NM8X#zd zpb45R&;r3Q2!=z@3PIZ{(%9{o8$@$0|$pz#>(*XhUMdXvFGxEm>p;gbkmy*koN0Q4R z7~4*+fWTn#NCUZ=TxUZ6x8z#nju04;i;7d&C92BwjM z2OT#Fi-N_+Z~T=!MV@`vU&(XicMwd1;1m8~tXJ?Q^85eluGh&Q$Q$I35KM+(3ItOj zn8s%enH2I?ZgbXXwF&i2Xer1?9Z_CiQ{NQPQc+o6U8ieqj_~cOgUEa2{ps-z!~-m5 zdb}MkjdwHrNzd|0lL7i|%e0B5XT&e@EF;c8JqE zU}lsfex7oooXJSam2v|zAfx(G?x2)%r$l^0!Za6+kQ1guuvjQHVwSI7|Lal#M5I|625FqfaGNQ=0hW{`r z&OefhGyNoril$l+50tY^B)>OO1)}%-qa@g?Kb493wNn`otmelg zdzcTTa!uDc5Ug#d@*r5pf1d0VH3Zw=K^0IaBCdzv%Qu{gDxu0SnJwpihpEr6js8R4R6!nbQ?WK^rABO-r z^a%(~LU0O#(-53lP92~QQirI+)Dh|^^$i4PA-Do@Ylyo-ydT8(LHvqog~yv;qj1{j zsf-5R=cw~0#UsZ**G^r4;Jh*9EmB8a?M_nP@e|UbZLU){Ol^OF;6givod2S!FMIw` zwY{jzJ*nF$%20P8xMYkyYoeXhUHm)h9(5m-Vj5jzj)Kk7HU6K0-^(PtvUvGTaPW=VJRWzIK@`8FrQ<&dU8lwS?(*#XIa1Da*A-E0! zn$V4vVcNt&Yt5d3Hgyf+{~mB@1x_VWk(^OnIaUbF>mg90XPNn6o~Cfj(Rs(OEZ6EA5I?Tz9k?MZt<@CyWYJ7^yoWt@8u+(-3I zL~t-2hT=(1#I4?;!aWYf(=$;Vp=Utc#;Kj21#!D~ z;u<}#E3VU6-bwEwo#{P9EyUd+E`m4;J!NP@K8SxWqxaJX=!5hj`Y?S2 z;(icUL0k**Q4pUA@m24N`)#+l9%^9~|0sJf|7xJM^&2KapXq+;sTNjrJvHW7zV-A% z_fv1Ra2x+u?k2uCm;IY;)7=RI_vQb{-NRm|@1dHW{(;|}XN97QKmT2xOV}#Mqo&fPg zh$o>;!h|xYK1+dkGQ?9*CdmnAB29BfL!`b*B}^=n@NVhNBr-`5S3*1umEN9WM#*IU zx7wRgFG(Q$;o-qJl z(*f~9qps`pw~XVMk4$?15aPw{%y@{GbnAT*GX;sxe1cXkaKtmCI+&?w|$abcKS;BNOOPOWNa%Khd8M6}NI(}D? zqj)8=`n8bj$#KjEBqVy>1@ZbGA)65~gm{|?YbLVzHz`C?D)*SA+zauB9w`r)q&&zR zG93_agm_c;8F9jFVXq#HeaoCQ$$A3fE$z%Hhz~Q#n!}u9E|`S*mO0;rvEe3RE}4XB z{U;}kZxCkoba$Qkv0DkKgh5Ie`S&xm`I?wAi?IOWCPs|j=s4inL#7D37!dm=SuKZdE)}D1lI$=>LozTuYL3|?8iMPRx_2^Ow>)xf3 zk2_cq`8mW<;r>sa+~x8tTE9nc2YY%AU=cm~mkN#zVNuD2{BAPDrx^d{q1}DRP16sD z_*Boodx~UZ&`jPc=h#>lDSH~kr<;5*k4<9J{%`6zqkqpFv)P<3^RZ|l3-vhTEg4Pr zt7M0;S_ERYfGuQ;*kZPXRkNk6hAm^uA&yejY>0mf@i`Em3-NgnpAYc`5MK!KMJrid zw=LO9avWQYELm%U_TnB(BBF?(y~1S2&;A42?vM2IgnnbBb~I}M4*PBD4$GLsjx(@kEy9C`60t*gTv zc7C@85Hm*_Scw44PB#G`k)R4Y1KYu_MF3`(u$}Bub{V^zUBQ0Fu4Gry7`q1It09ig zYazZ4;_D%f;IRSX8zKIMspQ8utzy@`*1|?&Im#y}jWzl!jnk>E@ z;(JUq<*h|oe)3SJC%Kr=oq(LYojnNguX;3d1Zn2q)@6H?a*{o5QVR0)E$!?Xh;KD1 z#qm6Q$t3l+?8Po@Z$ow`PIh5C+9bA)z0ThJcktd~5%ECa-2w5h{{h~+rrF(t=sn;Y zFL=m4MkBqA-QQT``MV&#yBoWIurL2d*ySjW?Xn!lbXjiiyE52&_vM7Bis4Ww*x$~X zL;T=7bp~hCT`L^mcULqHw&&b2zZIMV=g2v6&YTP9%DHifg(9#WhWHVPABFfg5I+X- z;}HLL1?RzuI8V-t^X7awU(OHWD4rq}BmJI*_&JDw2k}b~zsyglT);_D4aUhJev%Ka z^5!B;SCJ4u&8w>h8sfQxcj_rFnM*ZI8Y%dEJEw&B1rs35bZ>2ONK0z&iWFQ1aA@Z; zA%4+VI3TE`xoaUm1HDEu(f`-h=LT{)@Af~4%Qy8u7~)skxgjXlegCdjO1iajO)V@% z_0=H0quRn#!|4zfxH7JsLq*$li2nfb8%wyMoSv(I_>U035Ag^5h3YBNI%I#Y9^yBR zv1hm@^ez{@(dC-C7H$|fd`Ppd2JP!X?`}|?%>9928jOx@w>clZ7lI2_Yq%R>rRa4Ch${h^V}zKpS&yFRBk%LI)~!oqjrvm z_^&)u7f|sjw*<3U$<5*Ba`U+P+yZVPw}@NJwR0$d{04CZ#@`|S6ym7N`vc<7A^s=C z|AP37m0Tyc)a1V03hpy*CAW%0*&5<6A^ytby^z2l0U&`x0&RpP(FTusb$SrIn>&D3 zxVSysUTz<^9}+Yq7)Y>?K)oI04l%vCBaje40mDS2YC! zjvw6=Y+%vOvy}R}mNRH8NME!KPlT>>Do5(jeyyP?H5z?0x;E`%^N926UM)6fg=hmB zqINbx@eK_%x||Ws=-pmknyHh7`Wo#iZy#U30#UhX`&%9QVSe{lov9;Jn+{)p)OYt@ zuv6Z4K7m2+ZrKX-M?Huo{H_*jq9eo~eFhjxFfCS%c4Cdf#$yw)NoYsbr`RHFIko}& z0{aqe7TJnz$M$01pmqJr*bVG1_5gc?JqEpjIdB4=AOHk|a1agBK_(ao^q{I+kT*M^ zLc6bf&cI+a>v6N+xfh59$&h)Taer`^xj(tTx{PEC2|HvbNZ3QdVHx)l5CTj9(CRKi zj3ZhVC7dAP!v8U>)8G-uc6WSA&LM(I3&g?>!z!JG6W{!3V_C#cXt-v0I z0Mos8fdeG^nf9a@&?`QHGkUMn)f8=tGF|uU;)>nhMcA>G=bE62dfvcU5C z@_J1R|4HkJzj>S>yt^slac@}O-#ktb-Tl-TMeZt_w+2Y)zVH99%u$fq{WS1jxuGDV z`)M%ZhFkf5qihX(5L-PX7{=A(X5x5jhU-ipjnt% zq*;>L05h%GP_qiNDzh51Ifc9J6_53(VG= z?KS((>}Ru=!d}9D!Z2ZiFcDSv;auT-;bGw&;R|y!^I&t8d87Fh^EKvM&G(w`H$P~8*!-yZ8S``I=glvgUp6;f zHGg8kS-4w7Tclg`x5%(iSq!$&T2xwmU@^^NhQ%z4ITrIQ7FcYy*llsu;(Lo9EI+ZF zWx3PxfaL?LT&rTMW~-4_A6d<^`qXN!)qJakR*S7VtU9eWT5YoW%4&<%Hme<0JFRwG z?X@~*^~~DUI?=k+`UC5g)*G$Au>R6|r}c5`vj*!c)>o}>S>Lw)+4`~dGaE-69~-%i z!Y0xt+9uW}-X_r|*(TR!kWIc#flZN3iA||ZnT^(Fg3TJ66E@Fmoo(Z6+iYjser3DQ z_KNK_+v~PBY;W4$vb|&bi|swz2eyxFAKN~$>uV>pv#_(Wv$3^`;IX!ot%eY+R-=JsCp5%vS@3+xN+i|vQn>+LJ;o9x@{$JiU} zC)v-jpJ%_oevy5<{Sy17_8aWKu>aD2v;9{4?e<^W@3OyS|HQ$}!OubAFvOwIq1d6y zVVFa!!w82_4r3e)4igM-45hQlm}wGQVT1qMfd#{rH7jvB{uN1dbIvC{E9 z$4UiDphT~1gmrmwRZcgD&{hjnql}^=8wNCX;jZV!@%bnIcZE@P> zw8Lqq({87|PA8l$IbC z(3y9h={(!nFvoef^AYFM&M#dEmmrsLmn@ee7oCgVrP8I^rPgJ*OPk9`m(ea`U5qZX zT~@j5bGhbn*X5qeeV2!>y`n$-nG(wpu5_=&b`6C$-TvW zxOuV&!Q%^$EgsuEc6jXcxajd%gozjtC+a2YBeD|Ni0niTA}3LTs901b zsuwkinni7*k)qL}v7*_cwW4jJuSL5=dqw+22SsN@XGP~k=S4R~zlwen{VsYYdM^4) z^wP7JXCF_4nWwp@rKh#0t*5w>q?Tjky2y~z85_Yd9=ynprn z&HJhMAKriZ*!y_-`1=I<1p9>f$b7@uPYphEedhZt^jYlF;nV4}%x8tq zN}ts}Ykl_n9P~NtbJXXU&$m7&ea`rN=X1g5vd=Z2>%OF~#5d7b<=f~x!FQeSe%~{` zxBYtg+4%+d#rnnjrTY!?EA-R*HTkvpLBIF>-uD~lH`#BRAMZEQufuPh-{*cC{Wkf1 z<+sIeo8KY7BYuW&{J!-&>37EOoZorB8-7py`}jNhd;6#Qr}+=^FZ3_=SNm)Ihx%9e zSNR+Kr~A+I@9^*RU+%xsf3^QH|2qMF0$c;40|o>X2aF0B8(<80FW`fKj{+tJObVD1 zup(e(!0Ldt0qX-c1bh+jQ^13OX93Rx{t9>*hy~(-W`X8`mVwrWz@Wg$!05o(!1%zN zz`Ve+KyBdAz>2`pffE8h4*Vo=O5l>f^?}<14+I_xJQDa#P+U-2kUB^kG&1P@pvgf~ zgQf@F4|*C*1hc_b!To}LgQdapU`23ba8hu3a8~f>;0eLggJ%TK3jQ>BZt#-erNPUC zKMP(Jye9Z?@D(v3b`l$W#X(}UWee?TQivnP(c)~eUfduaBc3FlES@T!E}kj=R6I|- zP`pOGUc6EKrFe^YyLhK~w|KAktoVBgBk3mzl%z@uB;}GO$uLQqWRzsA1WG=Td?cA5 znI&nLtd^{kY><2r(kH|w#3jT7W$@4tS;*v&c_9l!+C%;@gwmm0Xzx&MXk%zgXlv++ z&?}+ehu#Q_3QG&i2+Imv5%zi5*I_%uc7^Q;I~w*(3Z$fzk#bTisY z?voyr9+4iCo{*lFo|9gXUY0(QK9l|_eIb2?-ux1>-ZD#>jm%!=By*AZ%YtNLS%@rM zHdr=9W+;>u%c^9xvU*vgY@BSmY^H3sY>u3jTgz?b_HrkAh&)z4NIpbfBv;GJap5 zFDP(@tHMX&uRt$g6beOxB3Y5DNLOSS6e>lwqF7O?C|3+sR4S?!;}tU%a}^5|ixo>1 zD-Q>a9s9&O9MfZ+2i#Ct8jE2#Zqo+nsH$=~jUKrgT-5I?sdR_F9=ws0*q8~;77X38( zdGw1IEQW}oW4IXi7|$4=7{8c+n4lPOj3Op3CNU-@CM~9aOnyvZOmU1lrY5E?rXi*& z#t`#f%m*|ab9XNxfF9X=6cMJF}GrVj=2}}Fy?WrAhu7e z!7SE1)-u*6);`uL)-|?YtSHti)+aVORvSA$c4h4G*x%wTiTVt5n-mhtz(l zqExR`-&FrpMQTdwkkqo&>eRMWL+X2}<5I_`PD!1fIwN&<>YUUisjE}Bq;5~$nYt%+ zf9k>1OR0t*Q}3kSO?{C1DD|b1QnJck%Dzf#rJd42>8x~B`YJ<|vC0%>nleL~r5vOj zqAXOFC`*-1%2CRx$_2{x%8kk|m0OhCl{=Mtl>3#3lt-0UmES9GD1TDkQQlQPQ2wfX zqI{-&o@SmFlopvbFl}7g?6fb^4y0XAyPx(V9W$g8>2x}m-aFka-6FkTx+vW{-7h^b zU7Q}8E=vzjFH4`Fejxp6fA9VS`%mgWr~mx^i~4u;U)q0p|1bJ~*?)8YZT%nif02P@ z5E*m^m(eT3J42iinjy;w&&bRu&8W(#&8W|4$Y{N6%UY4O zGHZ3#-mG(3H?n@sdXn`l>(8v0*&v(DX0mOv?X#V-U9;V@J+pnX{jvkHW3tn-8?q;6 z&&pn(y*K-t>{L zqX)(fOdM#KH*m$kRRh=N&^eYlHaYe=O*x}-#^+4PnV2&vXJ*cxoTE9%b57=*&bgZN zOV0hAM>)UcJk9wdm(FE#1-X548*|6zj>{dN`*H5%+?lzvbLZwR$X%4XEBAUHnb#*z zm}i-1o#&G0n%6HcJTEye-@RY&R z2J?ewNfQ#^;9*lo~fR#o}-?xUZn0&cdD1GKU1$#A5z~c?Nu6RC>>DRS~{nD7E=~imROcjrYzHyeNeWd>`2-Da;tJld0u%-IbS}%d{KEv zd1v{W@-NG`ly5KJS-z)yU-`-MGv(ivUo5{;ey#j3EvN0PHP>2c?X^x?S8YG7SgX)R zXrr`g+H7r(c93?6wp3fL9jdL=R%=_dW3)!?``U2^?R4#I?Og2wZHIQLc7=AO_JH=Z z_OkYx_6O}v?QQKN?Qhzr+UMFA+E=>XI&+-A0gR{co* zX#H6IC;F*+UcXqsRKG&MO21yeLBC1=mHvSKwEk!PJ^e%dWBu>?KlFd;Usd20R0Uhn ztHQm)v%pqAOx6iYi7`EUMUDakH{lrB7u>Wp(AmO1^Sd z<($fSm7SGqD%V$TsN7WfRppk-1C@s>zp4DT@>J!S%14zitB5MPimU2dWnN`fWmDx@ z6;h?BimHmON~lV!%Bae!8d#NERa`Z+sC^u9{pmt%|Q&S7q2< zwZH06)zPZsRVS-1R$Zz3zUoHRPgS?8o>V=rdQpv4Q`KyBuj)S4PSw8E0oB3PA=T1q zd392CO0}}OfAtsDyQ@!CpRGP$eX06t_4Vo>t8Z2RT>WSD%NkHa)-W}Knm#qc8p|5% zn&6tq8dXhYO>51>n)x*=Yc|$=S+k{Po1x}n&5fF$YVOqhQY)zSs|~G@;SUa+IOs%o@z1k0I`P#*`?X?}Xowc9Weo^~X?bg~IwL5FSsXbnM zqV{y1tS+%Gy)LsZyDq1$psu*Cw645PS2v?>Z{6LxCw0&2{;YdZ-?!eb-m%`LzF)nl z-tbZV)cOVW9ra7=SJbbmUthnmepCJS`n~n%>+jY-X|Qf^YKUqW*r0D{X&BZpyrHdO zT*LT=j~ga8Ol#m9+8Z`C>}mL};bOy;hVL70H2lP4-RxO*KtTO(U8UZl2aWqj^^I&gOe9_ATx$q83R@cuQnUOiO%=vZa5Es%1b+PD?>c`S6_0}7$cU$kZK4^W` z)~8L_X4z)bX4mG}=F--$P1NSy=GPX`rf7?7i*Ac+JKA<$)poJ%a@(~L=_3Y@7&4-8 zM9GMQBhHLCH{$$=OCyyd^F|IHIb>wfD2GwrqkKpCj|zIdRE>9k3RFFd)ZL$>ZvQU= CSmwY0 delta 17233 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%0D0E)8wk3AUr*2>0fUC{ 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 EA6;d+1^@s6 delta 15354 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 - + @@ -109,12 +109,6 @@ - - - - - - @@ -216,7 +210,7 @@ - + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.pbxproj b/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9f23915 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.pbxproj @@ -0,0 +1,721 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + BC0DC4E327F84A030045EFD2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC4E227F84A030045EFD2 /* AppDelegate.swift */; }; + BC0DC4E527F84A030045EFD2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC4E427F84A030045EFD2 /* SceneDelegate.swift */; }; + BC0DC4EA27F84A030045EFD2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BC0DC4E827F84A030045EFD2 /* Main.storyboard */; }; + BC0DC4EC27F84A030045EFD2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BC0DC4EB27F84A030045EFD2 /* Assets.xcassets */; }; + BC0DC4EF27F84A030045EFD2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BC0DC4ED27F84A030045EFD2 /* LaunchScreen.storyboard */; }; + BC0DC4FA27F84A030045EFD2 /* CatStaGramTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC4F927F84A030045EFD2 /* CatStaGramTests.swift */; }; + BC0DC50427F84A030045EFD2 /* CatStaGramUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC50327F84A030045EFD2 /* CatStaGramUITests.swift */; }; + BC0DC50627F84A030045EFD2 /* CatStaGramUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC50527F84A030045EFD2 /* CatStaGramUITestsLaunchTests.swift */; }; + BC0DC51327F855680045EFD2 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC51227F855680045EFD2 /* LoginViewController.swift */; }; + BC0DC51527F859B00045EFD2 /* RegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC51427F859B00045EFD2 /* RegisterViewController.swift */; }; + BC0DC51727F895650045EFD2 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC51627F895650045EFD2 /* UIViewExtension.swift */; }; + BC0DC51B27F896580045EFD2 /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC51A27F896580045EFD2 /* UIViewController+Extension.swift */; }; + BC0DC51D27F89A000045EFD2 /* UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC51C27F89A000045EFD2 /* UserInfo.swift */; }; + BC46D8FC281D8AF0000B2BF5 /* PostCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC46D8FA281D8AF0000B2BF5 /* PostCollectionViewCell.swift */; }; + BC46D8FD281D8AF0000B2BF5 /* PostCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC46D8FB281D8AF0000B2BF5 /* PostCollectionViewCell.xib */; }; + BC984672281D70050092270E /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC984671281D70050092270E /* ProfileViewController.swift */; }; + BC984676281D74110092270E /* ProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC984674281D74110092270E /* ProfileCollectionViewCell.swift */; }; + BC984677281D74110092270E /* ProfileCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC984675281D74110092270E /* ProfileCollectionViewCell.xib */; }; + BCE2ACA5280AE197007ABF95 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2ACA4280AE197007ABF95 /* HomeViewController.swift */; }; + BCE2ACA9280AE8A7007ABF95 /* FeedTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2ACA7280AE8A7007ABF95 /* FeedTableViewCell.swift */; }; + BCE2ACAA280AE8A7007ABF95 /* FeedTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BCE2ACA8280AE8A7007ABF95 /* FeedTableViewCell.xib */; }; + BCE2ACAD280B013B007ABF95 /* StoryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2ACAB280B013B007ABF95 /* StoryTableViewCell.swift */; }; + BCE2ACAE280B013B007ABF95 /* StoryTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BCE2ACAC280B013B007ABF95 /* StoryTableViewCell.xib */; }; + BCE2ACB1280B044C007ABF95 /* StoryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2ACAF280B044C007ABF95 /* StoryCollectionViewCell.swift */; }; + BCE2ACB2280B044C007ABF95 /* StoryCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BCE2ACB0280B044C007ABF95 /* StoryCollectionViewCell.xib */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + BC0DC4F627F84A030045EFD2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BC0DC4D727F84A030045EFD2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = BC0DC4DE27F84A030045EFD2; + remoteInfo = CatStaGram; + }; + BC0DC50027F84A030045EFD2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BC0DC4D727F84A030045EFD2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = BC0DC4DE27F84A030045EFD2; + remoteInfo = CatStaGram; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + BC0DC4DF27F84A030045EFD2 /* CatStaGram.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CatStaGram.app; sourceTree = BUILT_PRODUCTS_DIR; }; + BC0DC4E227F84A030045EFD2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + BC0DC4E427F84A030045EFD2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + BC0DC4E927F84A030045EFD2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + BC0DC4EB27F84A030045EFD2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + BC0DC4EE27F84A030045EFD2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + BC0DC4F027F84A030045EFD2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BC0DC4F527F84A030045EFD2 /* CatStaGramTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CatStaGramTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BC0DC4F927F84A030045EFD2 /* CatStaGramTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatStaGramTests.swift; sourceTree = ""; }; + BC0DC4FF27F84A030045EFD2 /* CatStaGramUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CatStaGramUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BC0DC50327F84A030045EFD2 /* CatStaGramUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatStaGramUITests.swift; sourceTree = ""; }; + BC0DC50527F84A030045EFD2 /* CatStaGramUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatStaGramUITestsLaunchTests.swift; sourceTree = ""; }; + BC0DC51227F855680045EFD2 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; + BC0DC51427F859B00045EFD2 /* RegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterViewController.swift; sourceTree = ""; }; + BC0DC51627F895650045EFD2 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; + BC0DC51A27F896580045EFD2 /* UIViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; + BC0DC51C27F89A000045EFD2 /* UserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfo.swift; sourceTree = ""; }; + BC46D8FA281D8AF0000B2BF5 /* PostCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCollectionViewCell.swift; sourceTree = ""; }; + BC46D8FB281D8AF0000B2BF5 /* PostCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PostCollectionViewCell.xib; sourceTree = ""; }; + BC984671281D70050092270E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; + BC984674281D74110092270E /* ProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileCollectionViewCell.swift; sourceTree = ""; }; + BC984675281D74110092270E /* ProfileCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileCollectionViewCell.xib; sourceTree = ""; }; + BCE2ACA4280AE197007ABF95 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; + BCE2ACA7280AE8A7007ABF95 /* FeedTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedTableViewCell.swift; sourceTree = ""; }; + BCE2ACA8280AE8A7007ABF95 /* FeedTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FeedTableViewCell.xib; sourceTree = ""; }; + BCE2ACAB280B013B007ABF95 /* StoryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryTableViewCell.swift; sourceTree = ""; }; + BCE2ACAC280B013B007ABF95 /* StoryTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StoryTableViewCell.xib; sourceTree = ""; }; + BCE2ACAF280B044C007ABF95 /* StoryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryCollectionViewCell.swift; sourceTree = ""; }; + BCE2ACB0280B044C007ABF95 /* StoryCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StoryCollectionViewCell.xib; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + BC0DC4DC27F84A030045EFD2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC0DC4F227F84A030045EFD2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC0DC4FC27F84A030045EFD2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + BC0DC4D627F84A030045EFD2 = { + isa = PBXGroup; + children = ( + BC0DC4E127F84A030045EFD2 /* CatStaGram */, + BC0DC4F827F84A030045EFD2 /* CatStaGramTests */, + BC0DC50227F84A030045EFD2 /* CatStaGramUITests */, + BC0DC4E027F84A030045EFD2 /* Products */, + ); + sourceTree = ""; + }; + BC0DC4E027F84A030045EFD2 /* Products */ = { + isa = PBXGroup; + children = ( + BC0DC4DF27F84A030045EFD2 /* CatStaGram.app */, + BC0DC4F527F84A030045EFD2 /* CatStaGramTests.xctest */, + BC0DC4FF27F84A030045EFD2 /* CatStaGramUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + BC0DC4E127F84A030045EFD2 /* CatStaGram */ = { + isa = PBXGroup; + children = ( + BC984670281D6FD90092270E /* Profile */, + BCE2ACA3280AE179007ABF95 /* Home */, + BC0DC4E227F84A030045EFD2 /* AppDelegate.swift */, + BC0DC4E427F84A030045EFD2 /* SceneDelegate.swift */, + BC0DC51227F855680045EFD2 /* LoginViewController.swift */, + BC0DC51427F859B00045EFD2 /* RegisterViewController.swift */, + BC0DC4E827F84A030045EFD2 /* Main.storyboard */, + BC0DC4EB27F84A030045EFD2 /* Assets.xcassets */, + BC0DC4ED27F84A030045EFD2 /* LaunchScreen.storyboard */, + BC0DC4F027F84A030045EFD2 /* Info.plist */, + BC0DC51627F895650045EFD2 /* UIViewExtension.swift */, + BC0DC51A27F896580045EFD2 /* UIViewController+Extension.swift */, + BC0DC51C27F89A000045EFD2 /* UserInfo.swift */, + ); + path = CatStaGram; + sourceTree = ""; + }; + BC0DC4F827F84A030045EFD2 /* CatStaGramTests */ = { + isa = PBXGroup; + children = ( + BC0DC4F927F84A030045EFD2 /* CatStaGramTests.swift */, + ); + path = CatStaGramTests; + sourceTree = ""; + }; + BC0DC50227F84A030045EFD2 /* CatStaGramUITests */ = { + isa = PBXGroup; + children = ( + BC0DC50327F84A030045EFD2 /* CatStaGramUITests.swift */, + BC0DC50527F84A030045EFD2 /* CatStaGramUITestsLaunchTests.swift */, + ); + path = CatStaGramUITests; + sourceTree = ""; + }; + BC46D8F8281D8A9C000B2BF5 /* ProfileCell */ = { + isa = PBXGroup; + children = ( + BC984674281D74110092270E /* ProfileCollectionViewCell.swift */, + BC984675281D74110092270E /* ProfileCollectionViewCell.xib */, + ); + path = ProfileCell; + sourceTree = ""; + }; + BC46D8F9281D8AA7000B2BF5 /* PostCell */ = { + isa = PBXGroup; + children = ( + BC46D8FA281D8AF0000B2BF5 /* PostCollectionViewCell.swift */, + BC46D8FB281D8AF0000B2BF5 /* PostCollectionViewCell.xib */, + ); + path = PostCell; + sourceTree = ""; + }; + BC984670281D6FD90092270E /* Profile */ = { + isa = PBXGroup; + children = ( + BC984673281D73F00092270E /* Cell */, + BC984671281D70050092270E /* ProfileViewController.swift */, + ); + path = Profile; + sourceTree = ""; + }; + BC984673281D73F00092270E /* Cell */ = { + isa = PBXGroup; + children = ( + BC46D8F9281D8AA7000B2BF5 /* PostCell */, + BC46D8F8281D8A9C000B2BF5 /* ProfileCell */, + ); + path = Cell; + sourceTree = ""; + }; + BCE2ACA3280AE179007ABF95 /* Home */ = { + isa = PBXGroup; + children = ( + BCE2ACA6280AE885007ABF95 /* Cell */, + BCE2ACA4280AE197007ABF95 /* HomeViewController.swift */, + ); + path = Home; + sourceTree = ""; + }; + BCE2ACA6280AE885007ABF95 /* Cell */ = { + isa = PBXGroup; + children = ( + BCE2ACA7280AE8A7007ABF95 /* FeedTableViewCell.swift */, + BCE2ACA8280AE8A7007ABF95 /* FeedTableViewCell.xib */, + BCE2ACAB280B013B007ABF95 /* StoryTableViewCell.swift */, + BCE2ACAC280B013B007ABF95 /* StoryTableViewCell.xib */, + BCE2ACAF280B044C007ABF95 /* StoryCollectionViewCell.swift */, + BCE2ACB0280B044C007ABF95 /* StoryCollectionViewCell.xib */, + ); + path = Cell; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + BC0DC4DE27F84A030045EFD2 /* CatStaGram */ = { + isa = PBXNativeTarget; + buildConfigurationList = BC0DC50927F84A030045EFD2 /* Build configuration list for PBXNativeTarget "CatStaGram" */; + buildPhases = ( + BC0DC4DB27F84A030045EFD2 /* Sources */, + BC0DC4DC27F84A030045EFD2 /* Frameworks */, + BC0DC4DD27F84A030045EFD2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CatStaGram; + productName = CatStaGram; + productReference = BC0DC4DF27F84A030045EFD2 /* CatStaGram.app */; + productType = "com.apple.product-type.application"; + }; + BC0DC4F427F84A030045EFD2 /* CatStaGramTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = BC0DC50C27F84A030045EFD2 /* Build configuration list for PBXNativeTarget "CatStaGramTests" */; + buildPhases = ( + BC0DC4F127F84A030045EFD2 /* Sources */, + BC0DC4F227F84A030045EFD2 /* Frameworks */, + BC0DC4F327F84A030045EFD2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BC0DC4F727F84A030045EFD2 /* PBXTargetDependency */, + ); + name = CatStaGramTests; + productName = CatStaGramTests; + productReference = BC0DC4F527F84A030045EFD2 /* CatStaGramTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + BC0DC4FE27F84A030045EFD2 /* CatStaGramUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = BC0DC50F27F84A030045EFD2 /* Build configuration list for PBXNativeTarget "CatStaGramUITests" */; + buildPhases = ( + BC0DC4FB27F84A030045EFD2 /* Sources */, + BC0DC4FC27F84A030045EFD2 /* Frameworks */, + BC0DC4FD27F84A030045EFD2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BC0DC50127F84A030045EFD2 /* PBXTargetDependency */, + ); + name = CatStaGramUITests; + productName = CatStaGramUITests; + productReference = BC0DC4FF27F84A030045EFD2 /* CatStaGramUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BC0DC4D727F84A030045EFD2 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1320; + LastUpgradeCheck = 1320; + TargetAttributes = { + BC0DC4DE27F84A030045EFD2 = { + CreatedOnToolsVersion = 13.2.1; + }; + BC0DC4F427F84A030045EFD2 = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = BC0DC4DE27F84A030045EFD2; + }; + BC0DC4FE27F84A030045EFD2 = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = BC0DC4DE27F84A030045EFD2; + }; + }; + }; + buildConfigurationList = BC0DC4DA27F84A030045EFD2 /* Build configuration list for PBXProject "CatStaGram" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = BC0DC4D627F84A030045EFD2; + productRefGroup = BC0DC4E027F84A030045EFD2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + BC0DC4DE27F84A030045EFD2 /* CatStaGram */, + BC0DC4F427F84A030045EFD2 /* CatStaGramTests */, + BC0DC4FE27F84A030045EFD2 /* CatStaGramUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + BC0DC4DD27F84A030045EFD2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC46D8FD281D8AF0000B2BF5 /* PostCollectionViewCell.xib in Resources */, + BC0DC4EF27F84A030045EFD2 /* LaunchScreen.storyboard in Resources */, + BCE2ACAA280AE8A7007ABF95 /* FeedTableViewCell.xib in Resources */, + BC0DC4EC27F84A030045EFD2 /* Assets.xcassets in Resources */, + BC984677281D74110092270E /* ProfileCollectionViewCell.xib in Resources */, + BCE2ACAE280B013B007ABF95 /* StoryTableViewCell.xib in Resources */, + BCE2ACB2280B044C007ABF95 /* StoryCollectionViewCell.xib in Resources */, + BC0DC4EA27F84A030045EFD2 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC0DC4F327F84A030045EFD2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC0DC4FD27F84A030045EFD2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + BC0DC4DB27F84A030045EFD2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC0DC51B27F896580045EFD2 /* UIViewController+Extension.swift in Sources */, + BC0DC51727F895650045EFD2 /* UIViewExtension.swift in Sources */, + BCE2ACB1280B044C007ABF95 /* StoryCollectionViewCell.swift in Sources */, + BC0DC51327F855680045EFD2 /* LoginViewController.swift in Sources */, + BCE2ACA5280AE197007ABF95 /* HomeViewController.swift in Sources */, + BC0DC4E327F84A030045EFD2 /* AppDelegate.swift in Sources */, + BC984672281D70050092270E /* ProfileViewController.swift in Sources */, + BC0DC4E527F84A030045EFD2 /* SceneDelegate.swift in Sources */, + BC984676281D74110092270E /* ProfileCollectionViewCell.swift in Sources */, + BC0DC51D27F89A000045EFD2 /* UserInfo.swift in Sources */, + BCE2ACA9280AE8A7007ABF95 /* FeedTableViewCell.swift in Sources */, + BC0DC51527F859B00045EFD2 /* RegisterViewController.swift in Sources */, + BC46D8FC281D8AF0000B2BF5 /* PostCollectionViewCell.swift in Sources */, + BCE2ACAD280B013B007ABF95 /* StoryTableViewCell.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC0DC4F127F84A030045EFD2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC0DC4FA27F84A030045EFD2 /* CatStaGramTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC0DC4FB27F84A030045EFD2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC0DC50427F84A030045EFD2 /* CatStaGramUITests.swift in Sources */, + BC0DC50627F84A030045EFD2 /* CatStaGramUITestsLaunchTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + BC0DC4F727F84A030045EFD2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BC0DC4DE27F84A030045EFD2 /* CatStaGram */; + targetProxy = BC0DC4F627F84A030045EFD2 /* PBXContainerItemProxy */; + }; + BC0DC50127F84A030045EFD2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BC0DC4DE27F84A030045EFD2 /* CatStaGram */; + targetProxy = BC0DC50027F84A030045EFD2 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + BC0DC4E827F84A030045EFD2 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BC0DC4E927F84A030045EFD2 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + BC0DC4ED27F84A030045EFD2 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BC0DC4EE27F84A030045EFD2 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + BC0DC50727F84A030045EFD2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + BC0DC50827F84A030045EFD2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + BC0DC50A27F84A030045EFD2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = CatStaGram/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.CatStaGram; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + BC0DC50B27F84A030045EFD2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = CatStaGram/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.CatStaGram; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + BC0DC50D27F84A030045EFD2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.CatStaGramTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CatStaGram.app/CatStaGram"; + }; + name = Debug; + }; + BC0DC50E27F84A030045EFD2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.CatStaGramTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CatStaGram.app/CatStaGram"; + }; + name = Release; + }; + BC0DC51027F84A030045EFD2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.CatStaGramUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = CatStaGram; + }; + name = Debug; + }; + BC0DC51127F84A030045EFD2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.CatStaGramUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = CatStaGram; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + BC0DC4DA27F84A030045EFD2 /* Build configuration list for PBXProject "CatStaGram" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC0DC50727F84A030045EFD2 /* Debug */, + BC0DC50827F84A030045EFD2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BC0DC50927F84A030045EFD2 /* Build configuration list for PBXNativeTarget "CatStaGram" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC0DC50A27F84A030045EFD2 /* Debug */, + BC0DC50B27F84A030045EFD2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BC0DC50C27F84A030045EFD2 /* Build configuration list for PBXNativeTarget "CatStaGramTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC0DC50D27F84A030045EFD2 /* Debug */, + BC0DC50E27F84A030045EFD2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BC0DC50F27F84A030045EFD2 /* Build configuration list for PBXNativeTarget "CatStaGramUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC0DC51027F84A030045EFD2 /* Debug */, + BC0DC51127F84A030045EFD2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BC0DC4D727F84A030045EFD2 /* Project object */; +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..04d17fbbcaa3822ff77dc77dfbdd57d5fec9d71d GIT binary patch literal 70399 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 V+wAOX^O!MjzD?u)_VMV={{ssGQ*;0T literal 0 HcmV?d00001 diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..5d78be4 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist b/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..41decf4 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + CatStaGram.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/AppDelegate.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/AppDelegate.swift new file mode 100644 index 0000000..7bed9e0 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/02. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/AccentColor.colorset/Contents.json b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/AppIcon.appiconset/Contents.json b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Colors/Contents.json b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Colors/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Colors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Colors/disabledButtonColor.colorset/Contents.json b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Colors/disabledButtonColor.colorset/Contents.json new file mode 100644 index 0000000..f789857 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Colors/disabledButtonColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "250", + "green" : "222", + "red" : "198" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "250", + "green" : "222", + "red" : "198" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Colors/facebookColor.colorset/Contents.json b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Colors/facebookColor.colorset/Contents.json new file mode 100644 index 0000000..9819a74 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Colors/facebookColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "239", + "green" : "147", + "red" : "65" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "239", + "green" : "147", + "red" : "65" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Contents.json b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/ic_catstagram_logo.imageset/Contents.json b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/ic_catstagram_logo.imageset/Contents.json new file mode 100644 index 0000000..967d0af --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/ic_catstagram_logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_catstagram_logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/ic_catstagram_logo.imageset/ic_catstagram_logo.png b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/ic_catstagram_logo.imageset/ic_catstagram_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..51cbb629a4ef293816c1114c1bb84741eb1da600 GIT binary patch literal 11616 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 JDPa=ye*m{=V)Fn1 literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/Contents.json" new file mode 100644 index 0000000..73c0059 --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/Contents.json" @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_facebook.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_facebook.imageset/Contents.json" new file mode 100644 index 0000000..30dc307 --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_facebook.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_login_facebook.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_facebook.imageset/ic_login_facebook.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_facebook.imageset/ic_login_facebook.png" new file mode 100644 index 0000000000000000000000000000000000000000..f93cd37698964bd2ec9547b5417c4541707775ae GIT binary patch literal 6350 zcmeHM`9IWa-@nF;M%gMAAyaoFBH@&StS3c-N{5ijl2BPj$ex*U$|*!+$u36{*>|BK zCz>M^9kOJ|WX@z64cTV9udklxhv)eRo*$mu>ownNdtdL*a_t}YY))B7Zj#>w0FWe{ zIQA<5A}}ig#5TZWG4#V4OfZ2*33g)eFH-FC@36el|HQdKh#y3LsH3Wxp|Emi&~e8g zTcT%Bh+BXM2nh)}c-7B4(A~}7;~+7>D|60J9soHj!m(fMLbGN+T`U>>ev|pFcPCfT zL~%qeayQ5IM1``_A5uSQU_@Bxn*CP^=kD1!DN7}q`~H1-q*ymgp?=!8A$_q}I&(&{ z^f%h3t4Xn&j$0+4O)Rcbc--_vu{1QX%JS%u(#@6^WLkB)^dq$0-4%RtW737hDW0?D z{_B@!0$-9lsLMiK>Z-6XH8r(rtcEFDr_%Tn?QcSvWtAqa2X8>}F`4X4Kk6=GGw=;> zk<=XAkzhfI)TPA|oKJ9mEguOh{g9adlMV~@F306#1|t<;uiU837o}bdQUwE<&TGHS z+fj(hVO4{(&1Zt2)TTFpE4`Z5h179+hTsojUc-It<8z%!hS^aPh7Y4QH=hlDl9HZ= zU`7)eV|Z%%<+6D_;9u;JWSGI2aIRmlX1S%hm!KaTQT@+R&;;XbM#ig2&>CQv%kZaI zgn#PkDu~g~tdt-x@m|#ur1q^Iqu5Ve2d+9hk|H%|(Jg+=#=iEft0f|?72PB)TDfw% znxPbsX#bVc*(ZIgGAzRNfCxfvOEDw(X&P<zzLV-jbmzvCY zVNTTsafMzL$j3Nz8Qfltiq&~OaWEQR?uvF=?sV1G%QAI*t@ zE;+H|>qAWI_$?#p0+P53aJ?a=vO1%xbolThV$a)stJf$AlHbMArrAoT147`K-b`}| zdWI;FX2towhk3*wJdRy`@|F&t48*|sH_kqpfpwdIW69T_CBR;-XFajZ+zUZBET<-a{z8ZMhKkt8umfmY*NN=rtUln7kfi0#vVR z(>kGrx(;g!4vHWe%Z*f_DXa56LF>B~%4MM?ZgMLz~Z&J6$nt-@iD@vqCg}LU4 zW&OMmpYs(9CZ`4#2mkRnzjPkVuHMP)qYfo3^HHw615ZgwkBOR0Sxt4YVPumqk@2Fo zdjp7jS4}8m-!O<}a_@AtWC&Xvc_`P>i;n~uH=UWEP(P-}YrMgru`JNKWI?&`_jItc z{-CB)k5sFy`!_6^P0~NpG_nkRi#Y;qg^F5d6lF1g zZu44age!qH(Tf3x8D`MQ>C0>4%wd$C-XCs&?3r~#gVeigeX_IYZ2A)HT2`=_D>FZu zPMYvB0N8bzx+&E&BF)~|AFGD_T7!ggHFC7(ATyU!WU}fU%ez(0GXz>;POS1-_8RTb z&G5x)xS53y4o5iw3FEbplkSrMoNVx{=*SJ4l4@;{ltP2U%o-g5V^1qwYC#ldN~4Gw z3gmykMhS*fNL~LXc03jFPD_Rrsq}O}I2aw~9RQ}$r^>z;ZfNxt-GBlafdQqtPEJmi z5m9d56r4)>eq9;pw$7vrAE$%I^eWN*RtE&2VyyeJzts3K@KJyz9~n(;mI1mu6fxjR zPdv>4<$t#A7eJo6kb<{u!iD-pQ1b7@K<4WF6vkhDwgAMxs7hgc{gHdIoh?M0`j%Z& z9&!lZ1RkH^IuU|Xb40+E4?XsjT?K^z$k*nlT#jJv2V`^KdNcsvE8%B*74jK=f4>vJ z?w(Fep$=#b-wVLwqwsK0!~dUVYAP22m~MML^0Oy<)Ox9>o6(ouKU7zgGIX+3kZ`Cp zL?C=?U}DnZR=oJ}c#CjcyujjF1^|vvyZdd24C?oDr#u5*!;X?WcrAg4hU#c{)sl__ zkoEd)M-gw)<9xuvSZQMz+ox=#T35}ZB843>%T^$x|bAROj2q-wPNoc1;E{RaON&= z=wj1Ctb?z*(WVl05g>P7_hO|EDeSbtBRvMzjCocxE_=X^gRSrRB2CP;>N?NYf?kqS zmnDv*Q5y=gGnf4|U{7yYr+^#M%`_2y|CBy5m9S4vr~IeK4U(md1ivP|T;b})I$qu$ z5ukbvdQ9*&C)iIozSNg*dKLq0-qxKgH4Jh+?f2ppm;dfC3T)G^_jQ+|*Q?-0TN)Qt zZQuYLp5w$mU3kNP#N^91KF1~h3(otS{>xK}FNq0B0-H34-S5&e%RXkrNX^h79c2EF z0q=fauVObH$ozRN3m1ppDHS)04I%cJVV4|vZgFD3^~pi%l_N3i>RIW@4)v{a>5aY$ zh!lf)tosQ7_E!4Zs>iTLCiA1To??Jj&>@9F&O4Ou>aFYh06A~?oh3A-ZiM?PO$M~u z@ZbB&MpQ^!0~CtLUzBV83CdADI98`k5==MsbIy>`#6*B&HFT@&a?Q3#5OHZUXYm)+GJv1=`w0BzH0WZf@aZ9mVnOhA<+0${&BR9}pI+1z%_R?u3^hr<>~ z3x-8JQbdN9_xbDy0;fQ@51aroa}7_Rba^QKb(ByxxS*xqEXtJvXTtHNi-t+`)i=ir zI>7)mQ=M}sfvVZBpva9Or~tA@)57qsMi06VOPjoBGia@P>v!$Tc)8vRcKQwgsb3aM z>ebl`=_E&84DffB^%+X!5?@q6QJXh{@n++N3GqWsG@NWjPUvwo^Kes93Vp5OImwX- z(WjQjLfsC2tjdGvM2Ie_v##fi)HkrCODsfy-t*IA6^EKUFY?>N@qoO1xca&pm0MjV zaBG1^M!ev@5RaV04QQi4b0K{tJ-fgHm_zm1RUv-+xXV5VjW%e(eJ=D-(~^r#IL7r) zC@`a_h*8EjfPRTf1K?C3vb5p00}VH~2B2{Dw^`D&+puBS&?v=)Uo;CFI;+fc-uxn01D`A0j5!FRT@KvMrXq%9{(Z&D)`ddw*phYF@y8rn8$7+ zK&>8bNQ1LO7hwB(F~raIXTN!Y$p>>_`emuRJ6Q>(Y6`Pun}JP1d;u2xRfX2t`9LYc zKwKDfpqJlY$)h_YFyCYnnJ)ATZy)N?dmieJ^}KhF4U4$C-^z;bLJi_jDFs=A1r>qr z4H;n5*dpTUW5nfT>hl&K^4PfMbTWEB2$x9C z>P8#3bH2vh@7_fIELbB6^oW|NIk)A*zMH+C0m3Qhgr6 z5T+>Ix{pw0p;Wu1fHIPOs%xF1ly}YK+eWZyD*!foQ1E>HPv(C+A+P9@v|+tnxR~k} zuC((-xc0X12=C;4)4W-}C@0e`1&<^q)iChMqL|ES0JP3Ho4ib;@;mbjI@3Z$0QnxL zUKo0~9OrVtsN+u=0Iau-2;27nkR~VI;(qR*UFt?1FLoovGC_<5?7u}(pl}ccT95g! z5F0R%ylqM7zY&0roOV*X3Vf|bKp-M$00Jc;&@pg@co+iRyO)Gl#2_%WosBB>m{WQmzk8 zshs#qT~Ej7U8)hHwh5{yLUemph@|%eSIWa6?(+q{F66UGZYOy_h3*HeltUWRGj+l) zcx%}(^4#737;gHl^kEiPl>EfaLnxSxcH!cZD&3h)O-w?0pI|wHc zulx#$RG@Ju^m)2Du)|&tF3AuIHt&EyWeD5}f$+YewG{$&5g=q9Lx2b)1l~m$5%(Q| zK*W7^Qo@t)KHX{q8FxaU8DxaVBe)A0X>i2lgcEgfl$W^H3<9}0*fJV2m+r&VAaXl zsPV>vJ~QAEDQtUgm0(l5jEl@$6oXp_0Cpz;Fuenz|H=HnaKf_`Mkos&^5(mU%KA?0 z?=Fk;1b7aKf_KjbXW(SGiG1#?e;(9P6!)*U>@CI9HP&>kmZ36!>nWtLce>cUeSbHl{V{ zo1-gmWIrgRPA#J!!}!!g28$wt0(+MiUAs*h(TSl7a9uPddGm`<^T%!>kIbwK)2AiL zDoM@jFIlv6_c&k3NCq%20z+Iie;xy^Z;XyOPA6r(L?_~yYfBKH#1f2U>%}&Zuh*tA0;6@f&8baj|Ieo5vi1hs;U?&!M{lFrY8ZeGg2OZ! zb)FGuy<+rGAm8GnzKPsc_o|SRwd{|Aj|~S(~1JNH%ak#sFKa_B?>ya zHte{)A|Uil2fy6ey9to{loU&7>UG6HPA{1;FCS$BD#Bv|b4K_%UB$>CA5dQPp>#TM zbpZKnm?6yz>;0{`#ABVe6d$4z7Ovrf1&(!vNux%N(GI>RQHgjc@;g2RA9l6WMI7iA zKBA0jJ4Os%?*7sAHy#%+1|1u=pJsvep39I_29zg*v%K9jjc>sKNR0Fo{C1YqnrYX7 z6Q|}dqdTx@l&d3*vyD`g@lMNK19+8Q%;-+s77V%eNEV~Vt(i%Bnuj$F;Pj)G3rH@P zLL@NclylZ3$rfU1A-~siQuXDP0FE(Am}f+tUnVFK0%*@(%7T);hk| zX#e6idaEGx{%(-fD??aH2>*6kUX09Opnr?nPb0wI&10E$MhOCZg9r zu6-v42KEwER)3{TXRM6@t&W_NoJM3`?4w@YYXA!6d6Le#47!Sb-{LZstbH9$iutvt zvce~AyU4bZ!DxYPMAQUz-|8hwM3Qw%nkbO3OJ+>qr59ghJ6KAd!GlwBym&f2$7Q9L zWzsjlL|Fi>zu(+T%iHR}^HP%sDiVTUB1>TO)r=BNh|&ng07o1Qv8FQGpb{|hdrB@A z=(){HPtj|cvSlYtFvbr>nA?D>7!us&-x2;-Xm|X#Hj`%&NQ_chKatT-DBBMw+<~qI z7X8)oL&y}sa_jNDcJ*q}qD0qEp%LT%9cQjM_&-l8iQeY~rIVa_J>P}gu0JuY8)1Zy zwTLr_^jDwce z(KJReeilJ*UPt#?aV}8``&<@tXW-V6yz8oO2S=jSQbHga891MdC%|x61VugRLUz>V z8LO&8v6h;$Eb43#$Rs?r0`)hB`tO`T^n3dXjBhC)~ Q?*R~wpE_1{)b;ki0fakz3IG5A literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_hidden.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_hidden.imageset/Contents.json" new file mode 100644 index 0000000..a58f03a --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_hidden.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_login_hidden.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_hidden.imageset/ic_login_hidden.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_hidden.imageset/ic_login_hidden.png" new file mode 100644 index 0000000000000000000000000000000000000000..3649e7d77250938416d3a18be15177d39db5b79f GIT binary patch literal 11970 zcmd^l_gm6!^gn{5G&M6dckY}#xy?bD1Im$mk1{naM{Z3(>nR60!pw3+kCLWlnlnv- zO0(feoTUkoBTYq|s0iP;pX>9(_kZ|!;l-QxeXet#bB{A#=VV=SuodK&;s=31f(W>k zGYA9*9>JhPyujB+(#S6G#T{#ja6JV4r5?JP3H&}B4fl!#frPpb{<#V@g`|O(l5y6a zaV}9Iafz2>fX#m5ePB47?(;o)f}W;=`dSPz9`+_-A%%3*sh)kN=ROuXjpix`2m^R=TO4Jx?t z)E*&Q>+)eA_#x%|&F$WH51pctTg0z@sdIguPxE^B9*k)8m73_`OxDXx_>>XY|3ja` z;FBp3WkimVY?5H~KMi^f>J2?q4jn~Rpo&wANZ&~XqfzC`2#qg7B#-pxq++3_Eo_lDBA94Tt6^R_1z|EMS=Oh9;Dadb}j!H&iUOEsu8oDO^a=-2lX z6Cls1RiY8(u?2Jc*KnnS6l*X;3-VNswdX=wdq;$FxnouRy^J6CX6z%Alo-0Xh}639dafMzSAbflf>|r4azYUy1@(&TYzPD)9D6I!_xWP2>&iG zo+*A`hYp1|x8q_P6k{sF2Wa)V$&izqk)hP1)U(d-C>9B^-oGTN7pRt0Md~r?=Wp6I z!Wg1*;Z>pN zoBFeJs?=fpQ0NN%tQSkA`l2+X7qkwC7Fq`!x3`nyB^;JHhk}VQCjzi%nx$7yldG=Q zW)V*mrvxjkYE$8X8k=*U0B{Do%^`T@6xDEn&;XHj59#qgDo$>`#ay z8G6a{PGpahd?K8IG~gWAJU-IN%hUMkg`arG-&w8jp9Oly_3*$Q*kt+ zE*7*(!3%_L@bo`cj1>#}vVgeX-^<2&2Z6)h7p`X!^CevpRut_WTI2@jKLt2z+9`DP zqeU4Kr-Nz+lXbL)s!Ti=JjrpoO73S89#Wl zP(2FdS{RWg45EX_gr~(3T}@1*?j$H4zW+mt=|lZFf-W>b1o?1xN@37SOzyM6-}#xQ zR8R5}61&!V$hfo_-zr6t28l(8Qt#l6%CuR|dY&xlPr#et7pu6l-dKkY6ok2CJ=BmckxcwT~@%CIQf3Uw_FF{^aY-?b&R;=hmrK8{7u zD#z%$_V+^*w>PS*9Mg+G&{;n3r9tsD5EYkRWD^C)XesqebN1fB=u!+c)N7BU-+d1+ zonD71u!t~V&gYT_nAUI|Ew(mIFzsWb`Klv6?836(`&|s=TrleXKZ+ixtDl5ul}=F z@bpBIUk#x0U9%zmDbT7RwTV;->@c4L||~}pU-DY2q}Q2*490% zaRYNOuANvgrawU#|L7L9WVI+DEJ}fX&Cc6@F8;kNAt&<8XHk*|T=XkksSeV~!{q5t zN&fbLICAVShyyS7Hr##hQsgVxDeY0~GJkK3l$6h8UuP8Qzmj+`SC~imGos)K4&d{j z^1#$At3qtSn2!Ty8<-bs%9hgrBQaqWED~n; zL3=L(9;U6u#)b!jSGfY!84@L#Q?|vO!9P*b&Qj-ddngpvbKc40#zjm4d#e>yF?RfJ zgjJY~i8$Jp?R=+8DCu8dV6?qJzu?ylyX2BoQBQz#gdM~|fvxBg9|>a1Q2uzt1M&a{ zN-ErgdWHtob(@PMXHwqRYs`AGWOptK>Yv^1dTWo{?w6mMujXmSjR<0hc<+fLOhB5) zHEV}f&!x6Ip8j7Re~zvNB;kj(cr9`<>!)&o+1~tT%Z_;5ixyk8Cl?5*UjSt8WjlZ9 zt2C!<;e?l*?3xhfP0WiQj#x#Vy|`>!+>4r|E}xpzCS|0>mK#~sC4v@YOC*P+$#<6#6$#?*=J6t@y{Bs z4el6mWYf_+D_2q`V6RnMxPvJNv!?=rW@v`Vn_&*C~(XG+aVz7BVU^@bKX8TGv)WO>NtnY-^iLn;)i~8-9b+yG%DLa-JBsFHNLNZX5h07 zRgYA%usp|!A%hlZibfZ*Th$icOPbf5(jzL{2jau+)WotJvC}H;cf$Qu{RF2Rq*{M5e)J)~vhFUryPc*>QW-k5H}8s*f-44X0TLxOFjisU6Gu`AaQ63rBOEE&CS11-&5*LaE|U z&~%tP*aQ32Vd>%~%wW!w6^oqYG(8+w6$V3VBR%j}Zdj%$%ORcAHHo|l>MvXLr*udh zg`BmDyjZ6l^RTt5I)NKiEuUXA=g+KhE`EtBnI+wHTh1S;NB&t_9+zp#)K`ocmY#8Y z43@DjhAQlyD0F442IFUl0t>T4H9r>Op6=0VTutfVMBjK@cY~{x*XJZ1PTI?YdJoQ%YBeisx_D$kw zj5HXgMyQZ@@;1FlV!|fEkcY`vynXeBgV(u^Z?;jkMq$p3j1}R z_0n6}^Qa1E7gS8yuP8GK5lU;>%rWxACFenZ+a{fr`Ct4#)?T;`N#_G@7e#H^=TUa4 zdkMv+H?v|k|8Os^cU>oX8NPuzlsesCPLs}ry{bfo#V|6OFS*4@Cl**6$3E-$=fQ?{ zYFw|jh7sh}@gJ7UW8FLQcBjKq7#Xg16_f3gFHhF_p2`W6+T8DZUX&{9S^rHuye*N2 zw+Pv2{*MW=H%0l zQs%nmE}A7et7r9x4TSo7fF}Qk=xp&R_1!ncDV5bxdwIc8YyRIB(b0ljfQn!2tDn_rten_ywOoq$MI#q4SXq>9VDHjA)9Yf!+Hy{4k%dRlF z&w>@A(waa1>pdf@A2hHv9R1~MxnREk@ob9&W_zE|_{99}JnaB%4@!6 z6<76F?d$5>vBp#^^Y`^dNn3WsEv00Xl(Cs3q#oLF*%gXYCRA*eP`KVoOzB*hKF6+* zba9Z`$%{a*1?UCytUfB2wqX>nsgJkgclcd9J!s0XkPT0ksa^@I$Sy#zllbSJQ)w1n zl2LBzPApryT1qO{n#6LX`zuSC6GYPj_;}5q-(yi0PH`Vzq`0KfMJP{pKOdtCS{vPG z2TDbYF?zp`K*c%Jvi9r%uQK1H+84Ti!7wF)=w`uWS{Wwtgt_gCXz}A1S`df5EI4J& zdcCB^;j%l`ae1-4`s;^R!hH}Sq3K+dFQ_w1pWn%38nK)FV!<7>4PHkS*qEPw{G4>} z4yP=Oq)Pfo5+_yS)viS}=WpcfjRKAX)ReA=pij&!9-%JdO@v)z_29#1HWZVsTjM6x zf9?&%E^(V7Sg&=?tf$L^IMT+Y8>sV8daj++jS8duu{2o5MpxVhyHM7a-BV)KQE4qd ziCTjMh67u9rJoyhF*S6-Kr`*PK$KXH{*yV@3i07a{HMbtA!|eODOKF8r#pAc^fhcj zRi&e=BdZ7Y(TuW+Mch^Rd%6SXH}CXoB-XCU0C*Tt6#yK z)8@_9h@gM{^(i1Yi7Q%@fqY)%d)D7*)ypd{u1E}-f9rEFvl9JK79px-y`A1ND@C@3 zs^zlWDxb`lelrygJ)$+GzvdUZYZjOS$RbpRNR{{W(K(rC#L9@Bsn@XMA)HU8EU3Up z)l&j`clCvnH_k1qaIA|H+)pQ{A#aNO6t?-gFf(qX#jYWPRwZeBm+HsaA73eY7FLrS zGhvUrX5_D}gk#!Y{B`F#@u5;{*107^O@!#csj-le z*e{gine8MXZY`I!e=2Yo`Re}1YaLwF+HU#I$aVdluKu5{oD3%WA6=pXk$zX3U4uEF zu9is;_&FC8tZ!4WFTmidn>6EgI*QZKuvL;{j`lrq@S zPXXW%`wb_WuzOp({6x$BkCz<%9aC{9o(7ZTp0C_w01DNXrQgb;NBz8N5Pd^3XGj@s z&q`l3O#ICai(eO@?e(nAY&m4+?YkxV6i~45HPej_SgwZ7Y*)_Lksbx>-%DwgU5GU8 zh&nR6H@gz~OJ+w?qUm$gxi_I{9uJ33nA|ehObxpA(pTVyGaxKfUXcJ$a*n`KukXkD3}l_TfaXAw3qO+@|mdb_AAeN93RZ*Q%Xwbso{?C z31$rBRl$W|ocYD+@p9ue4;MQwf^8~iNwsMBeel0VpjT=H#@o+cc%jPOnylzcAK^9vc!-AlIlLSgafny>0 zn88|Jw^57{5SjhPWznIv>0Mu@hb+8Uqv8-!Q6`=EQ@IWC;Y@-SBWKc{aIXiZR&U*%F&fWb3deYrRn@Oy%Pr)cQwYK4Ecmi(8}-mt#KV zhr;z1y=1?n)Fm9QNQ4q4?m}_2LN3*}eqC0jMg|V+N|j$|f)~hI8HH2<;w&h2ZA_=_6kyzf2y=nF!Nj@9bJI_N#IQQUT;9<<*q`5wik2m~h{MXNgvHxPDYbh5<@?P76 z2==K&x^iuLTjB7L9@B@Z(@c)Mw?0qM`G|9=u-(9gTi;E9tn5h^iK9f|Bw|C;?pzGJ z3-0HUQy{!P^LYn9*!!@pNR(2vxHj>F28|Epk2Yk?pHoYIC#rqW-j{N&t1ELnN--{*i3zkpL_AyE7k$0I&^Ak_z0qPj zqsLSPk5mj|9M-l~TFwo&D(%~d{QT$lYzldbJ3N=8-0FyY%kq;c=n2sHc#kyNjg$X9 z8^-T_v-yQ1#o^@sBj?aI2oQEExy9Z%%Aym^S1DbvRXy6|Gid?D{4c=8DdsdBN)n6q z0T9(7jp)=3nrRafW-a@m{n4hI*CVcap2+*t zM+-wfP|+MwG0xr=56aR%Z(o9|9*AJR^yc8*Zr}jC^K_?K3gp0kJ6vmi3G#QLp=#yI z=gTH4S$-@CkfgTwUqq_4zJ1xfzcS(1ZPfkgCGWHo&7#(#oM`d=1!QnH!%PCOAV?C7m22=3l$(IB91-h-)+Wp(3Rl$x3;Wg0ZuGy@)%UiRD< z)beXHCWY8BgJu)(VYQ);Safx=A~l+vGUqIMwOu(%Y()`}?5cY~Wv1J@x!sKbA7VE$ zb_Uv0`_~baSoCI4_U%JpI&DojH2TH&Inl{ML!B@pu;bYt^F^S&r}sKDZD)za)^>D~D5R5w z2GEb5ua^~{jTg0U`+4GaVKu2~x5XVVE&C#FP*Qx-_h>TD!<&ZwI}NC>@4qQ(SRub%>Nl;oqaQJ@VIE6OkC=@hJXH#d!}L)k7IIvT$^-fa|V1Tc~3Sf--AAe|x@qAQ>Labb(n|usgy!9i+Jz@-?tlFB&IV7hs}Qy{GLa}c_NulnlP+)44Uj$I z@`bb7NA2Yx2|!IIrcrwp#Nev{Cigz?y%}r3*smC17@-aN?beEKCJ3$`kG>?C^PuAA zr|ad<3`n!2m%38tktXR(g3~zQhlg+Q``R9GdjvuI+62ukiIMcW>csQYiS-L6t%@;@_V&m2};E6k7FQtpj1)($M_&f@;cPn3a3` z)*I*ODvn^=;|FwAnXoNu;C!hjn=kx0f%Mk z0HNlnGF^>QAOGs3+vN{-5R^@-+8={}ZiHgZMhGo0nFv%r)GM)cwdw0=KADgPWDjjI9~>&FCQ-R)S!v1ec9sQ+UDHi`2>d`KTP= zr1V6R`~Zj3HRsKG`8HvUYe0n%yHVVm=5c&lLuvs;Jxt3>Y(swNWryGP;KF3Ws#5yh zjMwVx)Jb`nbRgg3iBA1hSFXSZ!eqe)es(_~7SlY}o<95=llrTEs}c-5Nod+AfKtDv zTYr7cG+px$wF48ZSUV$*I`8SMrqd$KGmNrIoG;v)?7siM6n$Wn!vO^3qrns{c5;r{ewAb< ze>r*(HH@jyds6Q;f|Gn++gEf<=4=?7cdT!gOs1LhN0v-2o!6)w>Mbt;F8)BmN$pHg zq}*mn72NUesAcRm8#RBM;*h2sAh2l3Ro?vdXZ;1mjJLCy+UgV%f(`kJS-EdLQ$kAV zF?6i+Vm)89Q|VHWM+%8Oc$+4RiIAHO*N9eSlr?5f9a-|eu|9(B38)$HrRvfK|5J=n zY6yPaUoj;J!U4xY{DBIo1QYSp8*n|NCimj(vn(>QKh>uR@<+%Te98EX7O(xU6l|y2 zn*4s*7Da^Zwi09X%Ql*>Z0xwW4!o~Mj;0%7{a<2M8mwnR$lDD22BTcobB!ekV95Xq z3t_uc&38wxWnd|Zv)8}sk!%s`ar2AT#%o1i47r#-{= zRnyTZ;+mBBVL1vERSKi@sJ^tpvhp?MXsiFU@ncVTSXkzOuj|v3^(GKtd3JIYiR(9B znw^bB?YcIolhDT~)tEjsqzQ8af6SBhJf{Dowt*aS`uG#+v^vZU<-=sKSvyqS%uFC{ z902fo0KI3=oOqMLT(A68nt|J?wn87#QU%b;!cn})*|LzMQ__HaY+XU*i+*Q+Wq;Xr<|1zJ%he2{ozn(iS!Vbk?P0C04Vim>8GF&3fOef(6WqBKHkaE=s} zMQ2%u7=+RCym(q&fHd3Lr1;Dt`hLn0-x15 z&3XlU*JhXWW#!EoPoQj|W;OJL?}k&LX&qn)tCRH7pu?rr{TcIW%tl=JOddRH)E0Ll z_;^PU0;5v+js_K@!>Jau!9v|NleMR#u7WKptI{;beBBs@)%UbwexQi_!F+N06GzsLllvpCC}_<)JlrL1P!#Q4_{1me zL+`bNo`?fszKb#q81pty+W9=zTp+RtDJL6TU~q`sKZ)Ha`e8HYI}v|X0boQ;8p|sJ z07ph&`tq_$vRpNO3Xu%Dao2)F)Wd!ZZ33IV|CqazFX=IJy}S>oL%@^QbKotf1(_NQ zHNWpW=0Jp+5`QY%oH1|p{fT4oE6?!4HmrX+#)8W}TM0e-Zh-W?8z<~G0}dR%Qf&Wa z(19e_jmwMx5|N>h0@Qo}g0*ug!5vGGE52m)0bx?Yo0`KcXsmn9)FdCFDhC3=r=?gs zt~ntae|h>Y?69jWdfnq;7%2m&$UQ;n=II+{o0r1x~OI? z$aG%R&c)xwR=dcTfLI|{?%;t{tKEQLVsMnko z?yu~*g&3^zkol{zDgQv2;taJ|7r=bFsRB(3ZO16|+JO(G>$_2whPs^onGEPI;`tNq)ZW;O0hv*g0_pN7Z*Bi$k}bg4;JCH|TOwXGY+jL%a-7Y0MAu4J{yp?x!ruW6zP z=$Q*O-;~Lp$L9pTc>@0LnkXEz(WgW}+s=ElIvx4vhtuYJY4wWz+-%-AWlVx>S!fzj zP4KMpD2bcAbj48wot_C4@~poXZ9Jx^ceq57w!`p0e#pJaXA0cy31~0o>de^4H0^+m zt2jdB)sVDP)gM*C@|8$ONa|$3e$wHvX38VCoiqK`qyc1eEglq`9-Bo>a|NQB#hx*| zL|Bv(L3msn0~cW0{b`R^xoZl31C%qfdjjGc6v8SA>xlJH+il935V;1odO@vfe?yRk zt+>%$tQ2sPI*P4|QQP}?r$zJb?Jm_BF0%c|+4&4h`_Ry9IP-LYPk$K{ zATyo2TRI2U)1S?)rj zKtFnbGL|{e6_Jr|)A~1;$$|2%a7Xe+az%n8Wm$WAqY)w1yl`mgip67d#Bw9#)hUlh zqol<2ya!Lq;e)>g&))K6)ozB9d$ym+akgvI-olN*i4Q_zWS;T~Ong*~I9r^u%X0^v z%X(X}yU4oTEdI%DNV+^_o%fln7JKvwq?DJ1_{@14nzlpQ#}6t>2<^otJvzcpED~bV z#S3gQG?TU-tj3Yysr(JoRCKGt!fwL=l@cl(^7~z%+@t?+2FL%Zk~?_kt)epVDXVvh zi=DSOBI4uro+QkosP*vLvW*H>&I8?Nvd%2DL}?ECCn|o{sCudtTC-fpBIB}5mEqXV%LuV zy-Vr^%5{Z3#1xZP_-l)}|K1aMu=+w6^k#wzLD0Ub@euP!F4Ik<>Q^7GfYqIZ+~9q2 zS`JLGOljgUs1Xky0vPm7ly$04ac8X3su20eeeB3-47!wyOt5OJ`m84bv8B2$VN(Jb zc?KBfmVPo&WicmXd$A*;7QJs)tuM|=DS>FI5&G@hy$-XECuugwVO@1G zXgR{U)VC!uF{06m4JKGu5e(WQK{@AW#Uy0|=SF&tFDEKtT><4#7WS;u|8xaQMq8|+ zeA6ABPh!wdRk?~p&{}q(UhtF$^b>&}PxZQBHG&y~lVc=$7EE<+$4k)HcK0e>jJZX? zOTA_qeQ*cbNaUpx%boU+e*}P(o^OvZf|v^K#YzM=eafjJ9V1JKwsEzAjD*?ArmJ?;1VkNhbKfO5pvMp4$vTQUV}+g>nh{Grz#Or1JY^Xo-DVAS@(xf`GQi z-HmvfK!+0Wq!9-SD{TBz{y#*PFa^R3AC?ND=9}-DQTgzT?Q+F-<6^ThSIclDX_-vL+#i{}>vIUWt5BA1{D=eu*j69B_tU0?fULDzBtMt9m&ByD$XyLbEJG2`RsKKvGS*K*JF7_JP?Z`nx4iKocJRl^xKn!TaV^3S`yjK)6I> zAWl%Oj#mXI{yLD`dmiZta1Ad*kTwtc{P1Jn}A7h}7Ts!1E!ZGJ%8=Vjf6Fp^)K2h#%*`nv?+ z5FDWxRx`~3MS|T|S>?n-Op{fSz{&i&hsrG<&u;KGk6aBDYT#N3q23HU_ecEyRA+Pb c5srMN;?CjV%aQp&zc~nD?O^rFGVu2Q0}eoq00000 literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_show.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_show.imageset/Contents.json" new file mode 100644 index 0000000..fbd5cda --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_show.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_login_show.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_show.imageset/ic_login_show.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_show.imageset/ic_login_show.png" new file mode 100644 index 0000000000000000000000000000000000000000..a659b824d32348e5e5384b7b629d6b5efccbbf8e GIT binary patch literal 14608 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 literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/Contents.json" new file mode 100644 index 0000000..73c0059 --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/Contents.json" @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_comment.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_comment.imageset/Contents.json" new file mode 100644 index 0000000..b63fc29 --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_comment.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_reels_comment.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_comment.imageset/ic_reels_comment.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_comment.imageset/ic_reels_comment.png" new file mode 100644 index 0000000000000000000000000000000000000000..cdadce0e35bc2a02398b9e4832950d3f60e5b4ce GIT binary patch literal 12247 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 literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_send.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_send.imageset/Contents.json" new file mode 100644 index 0000000..91f2f39 --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_send.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_reels_send.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_send.imageset/ic_reels_send.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_send.imageset/ic_reels_send.png" new file mode 100644 index 0000000000000000000000000000000000000000..25ffcde6231e556d621e5a3f37ef8b1ed9620a5b GIT binary patch literal 18159 zcmaicc|6qZyZ_iiwuq!GJ(E((5+TOYLP(?}*@*^OvkbC~rK}-Z>}eq>*|TIPN!Cg+ zk)07^4}&rDyFPlp=bZ0(&hMN*o|mVY`P_5g*L_{@>wRq#sjsKWv3<{W6bi+mt)+em zg`$W5rAO^xgg>0abu#dW(e9M)DHN(Oj*Vi?1pnS=qjgCag}QYRg$lZjLaoC~L6azy zmoy4BWrad1C81FKZfWl>D#IJ}*K{=1QK(lj`;nDc-L#B7;4gd%{+o9^-3kB0?5VA* z!8}37&7mZ4Z&zLh3Z*fkt$ymV&ySg(zDb7PD#WPXGvbG^I}RNbxPAMSTg0<%@w<6j zulfWPWlDNBzupwR;5bqp=g=+{TA|nS?Yr`dT;U8^{p*jvM9H|i_6vDA_2T)l5ASnGdyw_NJv@bgQc!DXXH;<%7v%@vfoy80JJ`@sRXZ-x^*ti{jJ9@V$9 z<|g_0Bu{uwex!Y2JgKfOdMC)`Cp#0<#6jKd(SgK{w38(iDKt9a(!7eFyh{(p^U!kZ zMz+WNKFY#Qt~ba|MCm9`PEABbu}9vnSQu@0-+$SLU(&O_CZL(B#=Tt+mMp&Ax6Bhc z|7)|*raeuzId)3DriLQYA`Mb`HZ>_6a6T35@}32!oNZ+3I@#C5guw}J&~7lnZpL_n z(gYi`5>w!|j!4Q+4-U4;5XFu^lGwPhpzsyh@0eo{ZiapI!<`GW7gDi#-mbXw61ap! zn(ZeFEH9cB#3{tS^Wg2;-`VGnO@Fx~WFXV=N!slRo4hM^3jcX~P(;N0U0u(^hkrOW zKC{%f%3IL&HKuc|y7_Fx^#v>PE;}WNZelxIqu6Tgs)~p6j{=)>$p%#$mSrGL(P_Ff zVYiuTGsh-z)hFuahb6C;&8pM1kHEzxJltbC_2@xTj9^Qw*ye!WoNd$K`JE_k?iQYt zJ9dAF<=8vJ%b&_2^eHy-b;<9`G{dd;uFh9fU~N9!=3038AZfZUQOLkH?Zw&!<=J4k z*YEaDIi{Dc$@>-=_vDe1p4p9_C2r$V{g%;%oI_o>tGe19VQb)DI*;h!3CxS! zkgrm-)L*;cq0IUP39oEDx#^xbAp_ix;^wZ(GB!>T)t?#N$eNkzqqu7IhTNAQ&GIcs zNkx{vz6MdX$p;b9sh=!!2@9e3xPS1DMy-b(?)<`+l5xE3PkQ*yDIi-`DmSn%ibIESxoF}v$yHT7QPb>w=x5^-}b zQ-fPF7WAUvsz$#amVkbSMroTpR|MeB}1kuSBD>W{>!C-)aALk{_k(v*Gx@7n31ecG`Skz_JnEF zKB+Ua5XKzT4&ytLV){qaEjQ*Fwj~`o%AV10r1O*1P5E0KElPFw{<5x@I;{B4{6bN$ zLd|f07)Sf&)`~6kF%n)n=i1K3+BWZZ2r+zZ)$#N=mZu2CH{KS#k@rT(lzzV=Y+o$|9SD zmse{CDy$n#*2~_4V0n(6T%8S5Z#Wt+eLl@T&qfn>o=2nDu57~95qXzxer*kV=gzQH zfOF6NgZA}(wAd^J<3#I*>W7PJN~g6woC(+Dt%5vG>d=@FaLK&5r6f+j?QzNpITLLT zx_`_b?0TPcR1T4jSnZ-y(~tgWwCJx`*Bp%3E92TgQj%yYso!DpYctdG z@|!iI64*upBVgmd;IGgo!O75K;K$!`iKhGcPGI}?Hr*87U7H>0gk#$5C5yjk`w>IQ z6bMocU^jS?1VB_dvR&4>XOvL>*@^03Je?;S-D=9cI;YUu>77?v`j(b}XrUfMVAnnm z4UOSe-w$yLiLdAMb~*K0Xh8ym0o3Jy(Q{A+Sj$wI>aStPU#3hPw8B@6R!UW%$aW76(QW@gT>qIL*X3OwKYiBFPg(2u)_r{3HHWNleE0f_ zSN+ON%Q`=zI>xM%pBvp~-dcZ_IwYN#*bLvHy4meiPk-imma*YOwyCLaQV7~1%Dh21$V;Z>nZ?ah7G0HU3+2zZ+n<9L}8r#1V>thK@z!q{1(Lrta@ zg$Q{@7l->lJ$$p-1E0XshZT3Y*NbI{E?Is9RI*L`@aWxv1K9nVMWc~YbL|#>5|k`U zlc|v9>HEu4ugN0Qu6P~XpUaW~CKXm_(|8sGvOE)BWD}KdvohJ&mfYd|GljJOB^htH z2p9`M86OeyUrj2_Vs(!kGgnGA`C7TT>#P|+>1XPz{W;`}2mrr?@jq@u_p#IWw?{B9 z$Umz43VaHUK?)8zY%6mYmbncn7i`=25LL5t-6vHK~OG=w{_(D?_}7+YJDbu9aq%jI2;ACclJn|^b3IBoS;?8zqD zTLgja^cn-pS(x66zPP8};w%~r6}d-Vs~0or#YnY*1L-%$;CN0=WbC5=q?&fF@- zDwbVm!#uMA#h|-X21o?B@a0@{S$De9H*iJZ0SrwYOMPyZS6N<}2$8C>()2`&Q+0!c zFm21fM$OKgdBypZ55?_J`T-2+w(-UI3XAIAV@xb1CXU5b;7l-^-j;EeDW;Rf}extb5nE*?OWh6a(C?GE0_DI6x zRDcTR*~G_OORAya@XAM#%?J-lzy|x;8nCWI4>b`13NrtV6#1J7gIZRp58>F`W_YEv zG)vNSf{!+x$5wW(=p((eWS6&0V(6${9w^_9j5T`anOV}XmN;6Y#D zX8jK5Zco22q3I71OHMuB;`HOVN3~xuSOf!e+oPkbWz!EIRy=u^B6RVUt@}c_h*G!H zn3jd|FJme~;zXxWLQ1<2o@c@Tx{!P=5Alu*Zygf}RlZ?S+dY8e)TT-ABQEp>erV^5zqEHf(hJAqz(G)|7m(s4urgW9}Bxpb65;6D!M(4 z6Nrpub7^fwA4^px*xt&$h2}C8*dB=G4LR&rEYwjVBPe)Bds*5bae-5xU^- z>sorl&_|87Moc%Yd?a-7MeD=}S4N^8u3fIg?U>`P=`Pq&$&MY2Sbnn|frs_}rm18l zT;)KnafdUZA@-1|t;YfvA8!)(#`PVgG@+-gL0YncgO|LgE*A%G0w5SUzafk?4KD^7 zC$rU%Lxe7twopf0VOKQDlH159`20tQ<68tdDynUHswwtxslI`Y%danhD2g6mzA320 zj(}dJ3C^xPd?X{H4ze9%{P!pe5iL=#cx@@prN`q(UgC|Lhp10mrGQUl-_g6ZPGbRz zS;IJ3oK1KiIlM^yW|ZpK_ejXV-}#5FymuaWRwsfCe|Jyu{-|g&5YpL2+G7>0RP}pI z%|R58$9(9vazAA*gh4A-`|#ss z8@+lKT$m+|NBU=f<~|qEfdjJq3r`9_yyyCsWlZ{`)!e=tV+YrF%{=%hMN@nPmthQi zeK_&Ke^8N-hersXwOZd-lf$E;eFqN=q#ltX3T{jvr@N=+@F(1J7sWg#FAk92_B5Qu zWMxTSt$i#MUlJQd<8GV5AGva(%>bvAU(RL`{ zqW1JlvPjFBxdCjV#V1VJ47I5 zKKEWK%Y5&&Xj`0Tj(EJ;pJ7eW?czt;&*_*K;MFi_Z4N$K{>&qQ)EZQ;DNH~hb|AHs+^DoI6qEblCq+PD6jMo0RU1>1~4x&rX7))T| zzioN+YqrN;hxb1V`hOqS}+xvMFtkJ>{23!cQQhey$V zNhi|EoQ|!2H?11YNIQsfPy0WZjOgd*BlsKh&OdIKi5pL}Mc$}o*G0YmUtvW|EPG^- zceFXxYpw$`XD^B9zz$PL?4;Ec9pdv9*)Aw{f z{k`!&L5;@3xNyMC7cs>zm%Kl)^HY%TEfCj6E0IyS$oWzH|mS=wG|rC_QV0k6qIf7e{z)V(P{R z1s`s8Kg0nC-KF=Ov`Uixi;|^wJ*q;|96^XYbL`Rt!u8rS=FD_CFz6ONajJxRz&fq# zuSUumi8n3wAc~v$|E4LTl{w_HZ9u7fZXV2Kyg|R?)Zd7r_WMSTPS(t=)6Yf*-L1pF zEOm@(?AU~OOBaP^*exSUdGb0-axi}87G(0SRwM|;ypM~*AF-agJaDFqgClIw9k)FN zc;K^t#5PVP23xTWfm_=@hb%_7L{+o!ZDQ&xD>mrjO+jUH|1-ido$5&Y1z8JHg!*NQ zDHlZ6q`tukje7!HJ|R=k-zATbbZq#H|A7O5yjfAzq0H?T{>S?B6uk4;R%Wkjin`y9 z!WTj6i;0fd8)R%OfML+hVm-P2|KccGW3Mk$?nkVz)YfjcGiCTU=$$ffpuZ>jkBq~@ z+ki@A!*8D-y$d#(I_C-J;_7;nO7O02jo#-0Ve@f1dpJp)&G2SP20t8Si^kH6wn5EVZ>k@xNs>8`AOv*v^ZEw#y(9`(Ny6ynr0h&MAK z3cp9%k{Sx@Rj-R8k5Eqho7$k$sD&G|m4LPU1Zc7B1|(yVox7g2;~rb1Wd-^?JskKPhJUYmDGWm9Uk< z*iKyiL^}xV>SfB;i1pvJnt-qaI=zSObMB_11Z&On%X@S2cv;!aFLp2A$Q`wGft-!g z2XN{fpKvRDa@)8@=3->M>Dj z3?6H>rQe-~Pvf?k?7sxh{d=`@@G=Lqo}zuVJS=hT_ztO-)rcQf5iN zq>etnfEr?Emo~b(JJ9V9Ls3uBZOUK>oYXIl-|x$9yRuj-;6RUJ7vKrJ3LlvJh^vm; z&Yx@65_?wk{!8-CiW~!9=KOqxw$x_5Q;u2A^j!!CxQ%g)#%yIu{u50iZP9#q`H@Zh zQPYVKAq>dq%>!N3zq6Boq6IG}ouA3F>3#1W3dL(3G*}?gcPM`)J&U%dvz>3n$3E<4 zMg3PeSzo!HVf9af67Te6M1-!Kddnl%v^nT$Ra$z8t^G3NlmD8iydYkKo0C6k;dI5= z;R-=-|XK||2yD$F5$)Swwq6ziH~NFy7gOn zuR`pulaGKy$=T4OGBRART-{Cm(t}gUi`m=$skL$D7O*m+3I-kT1hLow4UO*EyhBH9 z?DiS!C9D6>%tU?Ubd%bRBv+c>oC-gjqvK?eKM43gA|0#$9_hqu^}i%*viWE0VRs&! zM<0^7PTZI59{eXON}r+n(~0bc(_io+;i`>`)B1|;fQZd<$eQ;mS4@f__G<2&pHQ<@ z6QPisPkPDS9;>5!Qo>6Wp>dxWWnWp|hsMtP`qowXl3c@IBr9;K3I`Yj)lwfm!qPr# zl9n!9yyW*AC47tJWsbGwr-cGP1xj1;yutv>V1=w7MI)2jzpwNp#1*E`9CjT=Mbt)o zRg#3|2C7RgZ-k3riTj#ZJ36_<=$W~Sq^5I6%UqeEm=^d&lnmJu8ws>!_K1l2d?cg7 zpiOg;$GsQoT!Jjh3yk_k%{A|3`#!|fJ&$X!^fDwrQs<5ohbsxiPaU?XDRXAaNQ~x2 zOv;@FF6;_I+PE^D44k|f5VP`5O^eE!`vz7_N1aQT=$Ka zhRmh%PTC#*wu5@M&F;4h4Y56za|n#YXPWvPdAPPxyXel}{jG3mhvIOfvukzJV}6e} z795tBo^5QRlh3qLlNqQ9I^}Cp;@61#zBEMEy)q=lvG!o*>JlFmG@n-2(``~KMscZr zY{lzxJaH+@W@0==*75nNw4xa{{mm%&NS;+(c!Ym?6n=tEl^i6`7`WoJvS!CimUkY4 z+GKA*@aDqP#pqr2mj1{5l+{HuM>2eaEtUe041Y|2=4~Uonk^L};W&EOdXP>r`c97ibXXia(*h&}mj@YuLh(&T!Rx&wPTfJD)w0u{PwlIwAkG za+DLtd5vlJF?5xhFtH;PgS*|S?KxS8d2&7cf|+rtz^78o@h=P( zI)$wRwJ3eA#hq#^L$#aEPo9Nr9vlBr!dkwDCHI@H_(Z5DN8wk3=6=Ze>!*y*RuUBN zN!5dL9yR?y{pjx8yF)T^e5?S@bmKCmm~DP@&ZF7B`w)(E?8s35yY1w@!Cn!|JQ|tH zOPJRI_7M)`nQWJWUXB%o}eFUKk~SY+S^t1lB|1&p02}j`83)p-$ab!a5h@I?zUq@R0hU z5j`aJE== zZ4x&-7Sm%BSLm_t&6AGMlklxi&mdn0WP`v_^CSp^HL_^w2o^mB?^OE{EE$x;xx!tq z2r4JRH@(Rk$=D2P79qwEVufxEo_iELM=(fd{^qMMlL|*-2tk1Y4wS2G9>*{zpyxL? z7~)j-Om~C^(x`4So*x82&l~_3d{7@^vpCaB4eiu`UNl%X-gG%}+5AmMXaehH$|wVM zINaB!%r)bK5~UdDiD^4= z!gt|-<|2=M0KMG9h$%`O9ibV4+bLsWQ_8!!UWdn5<0(q_F$f)V}%-}&)G zf@Of2S#OuLFxb8;}P!J{5^5jq)^XYaT1ROAd^Xf6N&|6Jk`EhSUEW?&~ax60zC;S^! zl6q^*q{d9gD_H|R!%W2t4#)ag|Ak>jXMgMUb!*+VVszl4WeH39{OJ{+k2yJH9fLZx zMHbio*RS7hXvv%-H{8vjh_0kFZ?+{v*8*wbUcSLX;tP6tr=gflkIuqQ%S??SjMpV% zy%dIPWbYvOjN6wfSvf&$Qkk5!8A=b~Tvr@O%2&IZ9|e|bD?aa&qiT;c&*#%V z?WKh>4`aNs#FL_jq3xg%ivdqxYvNl&Ql9i%#l~p@chT{ndu_Co7hEK>vKB2W1YH)x z`NQv&R(g(h)&vaOzBE;U6@y_#eoEKV{^_9?slAFjv;~H8i@SZ@h6$a;>Y?N~E$U7DKvOBT{deYCR9nB+Tbme^AB8e*_E9Gijo}f+hfSurU=ryn_U= z^;yoW3>u;kT8cm14$Y=B7hYp>8-GYzhybpvy={nD?TN*nz+Pz_r#hNdRe_)SVehco zdc-DQ$7aIS5*F%6G+~q*LWqQ;CpDPne)z1KgWS|D$anBd?R9n;g|!_ylxG@sO#B7S zp?C>6+w`4Hog`QB>N!qY}|iwn)9CH@xs;hj~`AJyGmP`l_`RJ-&fs#bKbzlAW=9A;z| zxYXJ@g0-&b6Ho0+V^mqUFWp+e>|=i4#T5qun-eJvmI2PFhPP7B?5> z+V==9pqEavlD+MNNc@hUTkG5i@))*F8c((Z1&e%JFx+M8TByTfRI;^GrQ5HA&ueJF z2_kZqtR}a7>qu;9RBa~n{7`6>WA=+}lfCij4BTs%K*C^e4!SYGEx2z7)g5cS@fkg2I(W|c zF)R@$HR%OVP^s-yD}%PG+D9)Jv`#ubJ37* z11Na%5XSoovA!`p*e-)9jNKtX6_(lzD(-kzv!iX$oef#&bWA!km!MVRfyZaU8;p*w z&h$MKd_`tsb=#P!a?E-9G%G`ZJ8}ovqPijSq_ae&`hp-_&}*ZwOD{&GlFG_~Kh1Hrn5n3^Ld6V8iRI90%`RWtt-E`XPH`g`MIqpycwaK@f>!12Y zVz*U-a)3>R6_m-K>T&+D(@F=1adj=9*ki$%l?5v^z~`PTGM7Jj)<9}t zZXGnhmIVP&&r9=f@VELJOQsXZ{L_9=##9?d3gv z4~+ox&;prYyaUxF_hfVU-fcdVn*+ajdQPo7i_NC;16&k)Q~zyL27(7gFAD9(VxRD4q}+q0^_wIzYug5P>uF zaI3z*HfuqkN~L;sEVvQ+E4fe1qXK_j0@#cY6myDl2i$}#kbAnBIQ0t)LVZ-eZ&4pK zTC5wqXrDapmkXe`qOjZao2u#0egI&AL)?m4V7Hat#jQTAKva6_rA*#e9vt+)A3%l1 zvTdGooXX^V6zV1^VL(}pOo#F2k*E`}LxpoK<+WIDMgda9V_T~_LiZ{(5XTFb4t}QV z_uRRfB6utWd23f|ZbxX@z^9Hxb!Po5A1ZelQPj zy!D}NBETfd_b;+DPJJJ^Zh(uu%@sIxdDiR1EnvuK^kF*uY(+fy-ZEFCQ9ioIezD%% zF8wSgn!#$c&e0`WY^RMxliuO0D(A374WEWYvzZ@FmDIph4BVOemxCl_@?tCLi^z{+ z=E;Hd^qs?~yo#_}YirQKN<679`UXV|g02R0>KP=)2zR^9^}5I(X<~xelao96+p%6o!_E1j&lTU#k>zK=H977Vrlat+0S-h2-aFD!$qxa>%X50 z4YnsY>cJv>lrLE%4b+p;U4eBdoTO;LkcjVX?`lJ%Y3B!M9>2QliG{6dD2bL2>;={g zbJzT46_Hz3Ki&H@?oEyGZq>AOHf@!m2DY5U6okEmp7<<_LdWBJ%&{niu(UL3&Xz!^ zD`0R?>20_}VuxnRZTcpkEErIG_6&7m;^1istK}O5a#yZ^i|3BlqB|Qyzh_Uc0g6m0 zCRE((4i-^*;1~SUb0;G1cm~S26ltByhYR0wfMlk11LZx?Qdv4R1!f2&PF>V}2&nSV zy!8q%>S{p{EY301uHcXej4c9DqIf3S*nQHX$T|@BihE;_6uaN|c>TL6qI{9L^E&Hw zbhtHU3*S5Nq$Ih|Wz_t_0FsXAI3?(?_lZ`R8&si+)ixuA!U^R!=b4yDheVSj`e^eX z_Dc3vC8yKD{zE%EnP0soIHKlVtC>I#!b^m^o-;#&r7|gy>H*hs4q8|R-a{oIb#s`D zbfV4NGah>WNYbJLFU}upZO;<2*q~{sr)-=2&e8iex|#)5wJN-0Qd2#QL0sIYr>kd+~?DklFhyNq(Z)2LR3sH!(CO2R3q2k%cAZy0~L?gBlIu<OQ1GU!KeZkGF5JpsKLY(8*%j zx+USFmIu`Ty?&j>RHe#>BNDpHH28vpqA5}cJ9=Xp42?l+_g)}*rh(-m=TzLic?Vc1 z3A)nSenPR6vlu!9X}>R!4rMw(!!q=(E}fqZeoH5L zE0h#UWm5{B;E8ZlScFp$1xl69rg&_qp4qqOtS(1a;eHG=4>bE~U8&e`!7*ZI`cgMX z_(_MxMD;Jf$KVVfddSIiR7!$WTgVau>KCn7f4}g8=5f zhpNpfKY1Fg!h`~pN@ruGvrA|4xlqYVktizwf5pRAmupN~Df2*?R&1e2ZaOfur92 z^#}sy-d+vh61vudw}R}t!q|FiF~lN&2xj(NixPb_I@v?mm4>hjD!(fu4j&(Kr)jvIEFzs?-u z*@@Vm&lopO5c~Wrj0tqaqJ@*%@ug5^o=8PdE+VR4=pgqR{9wBf#wfW||GjTu+M&7^>(RII|!_p#_oXsrHTjhQ3gVk(Ky%7g+4 zT}B`Rnexsdovk*+{ibwrY8`v~gfK%M9+2xY_zKF^cEZTiab=K(0h0-4XzxbL^EHGY z%|1V@p>Hu8vVIG(7sz+@l?2l!_k)%J{|BfjT#9#wa6F6ib;j|?bOuEgWKZ`HO`S%<1!a4ZKf8M*)>m0x083;Ek%!54Ucb-$GmH8`yb<*`xc!Z z=Y;C_7I77HbR4eqv?iWRISbZzY(mfq2X5>{o06Bz292tQ<0S<9((nt{_%9n3%OYLp zjJa3q5$h9XM656R{5E#sHlaol>OHU=C8XV1AaG3w$AX>tk^IwF3=?%X45_%d+Dku3 zEH;1z+8Gf)4YxgSZS`>nPEv-?>p-)V`6pGpgD^b)F%ce@4cdDqRUYg8v#CSYFO-@byf2(Z0;X$7jbyj&N-O)|6HGxXim*JeV6QWuPH2rahy3_r=PuDZwcs7|%c z4viPV;Eoh5fRlp9<90YePL?H%i_8m?znAq^eB?z`@9CMUg)fb%ur?P2u21q}!E7fw zt`pG)C{PihBR`8^+F^TL2CYq!_VkzyJjQs^Sitq}l`T#aA<7DaBmD=)rPlB?Z=B>a z&dDyimpQ{6Up7~Pn@T))4ni-=N%oeskbY%@<{deNf{eV-*%H28DPEIxn3_^rB!rbu zTrFp%TK7K!E`t6PR7<&|Air0Ez5_Ck63?L{RPirjsf!$CAr3AE!oEXCZ8fE+(6%)JLCjkY*HsMwRNN6_;$QfMhZjxD z?}10;*4~fQ2-GO|s<)y17nuBs9VGv*4@eHj-4bC%+MIAWt@{;E@;d(bvPT)Nvl+Bx zAtnY$;6O+9BDxFF!$RI!H9S!W5w)L!8zgJ}Gu3E7kXn7sh5S1ogD-QphbHxuCdIk- zz@@z)q@VASf8J!qME$m@*>oio&ClFJqj_|KJnB+i5m^Up4EZfzBkm9IRa-h==Vwvl zS`GOrZNwt9ith*w%~r;NaYLOCwN( z{91TSQZ^m)Aq$qE1BU&@y?k2ok5z7b%kRCIxd-Nj2;Mbk*iOz-`MKk5kO%--L1bGP zyEPG=<@2B0^3zxxwM8t>ioT}@u0Dsoq z!vK{hcz1Cc<3-OYJT@-Boy$KeLTjwU{FaYLo3@j@$^* z7&fHIVCpbVaN^*3R9;nr&{;>AI)kaP5emuuVwGBJ6NsTiq?!z#@0w zCcbXo^$O6(N?6lbUZJ$Wj? zee=EZsqpoywAVS-z+cp%d8lwd1Tf$-$z&U(V!392Yv63mvlp#fDV&5` zFwGPH;AY;750D@N)ag)a?dJ-ti+$Eo=f7T?*U=AERUlGYVKC0I3kq(CJ*^0IJ!Qa% z=zbEok~cP<^C~KUnjpNjj(h!Z)5Z(kD- zk~wnQ>|(sJ9;Z}LkqmA-Oq%K3F<<}quXt}^x&P?Zy4Dt?FlPB>OUBsMf!5eMXeh?k zl9@1fZJ-3V)$fN&dyLopC=S^lE4UE~;l^u0iaiUL*)v9way# z6(LldNZHwz($w1c&I?kM-0?}MU6QtmcyNzHdk2Sz&jS3&n;X8qK9heTL5AqTpwG|g zppt|dO12|cK*j6D79T{M-(A%8>#*o-rHEoeYBQ!v@Zh-5Q*I*Y33 z>7|&Rf1ohliyfQ5%6ecX@BNN>8dao{!Z6SX71MD_z$v=b{zy|U4Rz6)p>R&7sVN8& ztp=1ot&J`;`x2|RaxxoxK&_MLsjD<>A|3@kYw`EvGLO3Kj72>%xH=n{w>-8@&268) zX=@%0@@(15RX2ZSb$?yHf3UFG!soZR757sQ7+Oi%{B;2otY%5bs^NBt5NF%>+Ktsz z(~<~pf0#y9#Bux1DbjVzI{7y{;r_ev>Y+v_+jXQ z#m1?AifttId$gZBcgc@^Vw2VsZLV+Lj;Vp6I-@#-WiBuwEVEMH&~GfbKD2_k(kN2Y zY)sx-VgzElc4{-ts@YoJDXC(wi&l$T43Hni5^TL&<$y1_o# z^_K(j4ivJ|qqhoK%~zhC0xRLsmOSC#OA2NNzP&u-xH2jtHk0wZW^>LJ&o=n(5Xi$C zQqq)-IgkgK+l3D`zE4aQhPwV5i0tL>6Ods^O zuarkdM3iiozIA*^#cw-`GDzzy@d-uszI&)Z=d-B2k_2yyGC^WRl6LY81I3q^E`NoV zKIb-i^r&rIBJAuE>iJnZ4i3^`6oo{{j`;>PE2+Avsmmp@ySTZZ(oq-rahqI~)8_6i zIit3?Zlr6fJxy?j3<`P4?%wc1RbA_h{d`t->^6jps)~c4}nXB z?NK6xO&eb`kXURlv!%WpiN>Jj`I7K4zAEYWq6pmTHJEOMRvNe!>YFkYG4{h9OKJlD z`9G|t@b6`g^E(Z?tc0r-j7ocd*1+BuQ=p2x-=JMBZG)-j0t1-$PPhcrx*vk;$iyAlq#S zdHRm}@mAKfb^v33b+3Yw0wfc*w+JGJ zoD1_@p@e*Vd+h!<(vG_@j4elDh(qg9isIM#)!O3cT6?p{A>x6N2+<>o%t>2z@@rv@ zh|RC2lh zwPIBK*sJ|2!FAw76O0kr9CJg`7?-lE%%BPg-Lu>+w>sWjwU&osS!{RLVr|J?7#P+% zS_kFC3d7fJAxGDTJ%)me^DSV)v0#6E+kFiYMhHPY>LIE6GynQ!L#V>)AObnIC`G$= z#j-ON?OWt@dcQ*!Yf!}dFz69YQK2bgV#i*-mQ`>S#%zZlD!7YE^V$V8UdFM(yRQDViqQl^_UQ5U_U8@!SAE+);_qYGWD|#t=Wns>>t-yBEc! zZIEP(#U5KVe|A4|1&V`?$W9h+GYd=imBk62ZM9#sG`RqWj6A?k>a||!wIQ7_d69cp zDA-mNxvxSd&uMI_btu3J|r9LopG#HuMU46q~A0 z{QN{UBnBB1R1lr&^~fvzzJ+L?ARTieDC$%8$q{u&lqAZ1rm2Mb0|R495}Mt%$666#|M-S=GU)iq4vk$hXCCQt&$dT5){+3cC= zrOQZd?TE4EM`p|wAEnB*7Tn;&+#P+GSox&pvCth`eq_iymV3aimMDx9Z2pXdGU#7) zNJLNOn(1RMafB?Ytq9mnL>kLh82@x_sd1GzLNpOMnAlKAqNV&%uvwr&i1_panS9?H=hRR=1ODg2KPW{XBS~PMwi6ogl}OAjmmlxl|r> z<9XlFVDCJ>e3xmH|8W3Gpn;;S(1dW~M`2jzyRF&JJ+LQ0K@O8$l z(8v2EZ}$BWRAX4-DDpEWymXHP*QdYHetHvbBGTCYC#(}zql4-jq$0uBT&okIx=|Kk ze6H9AA~xXO>nlt*ZZu5Mpz80V-iDy0=y+P+yKnPgnbE#>7hT< z2F@OToIa3Zv#;yZYxM>i*AuV7#$TV5k)N44;Hh!d)7IMax{{6ib@+plk(QB>kUl1H zOyTk|StYp>N-}asq@|UlrGFka{P=GVIJ??9V157p59E5zc)|lHZ4Ev3ywg@8{|_?W B+nfLZ literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/Contents.json" new file mode 100644 index 0000000..73c0059 --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/Contents.json" @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_hamburger.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_hamburger.imageset/Contents.json" new file mode 100644 index 0000000..7787869 --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_hamburger.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_my_hamburger.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_hamburger.imageset/ic_my_hamburger.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_hamburger.imageset/ic_my_hamburger.png" new file mode 100644 index 0000000000000000000000000000000000000000..653738eeb98cd98424e1421a7644f80e41032a81 GIT binary patch literal 2793 zcmdUw`%_cb7RMI}FQJK|7AZ*SSSwmouoBSlQXV401%!kk5D20mB)r4`H$05?3ilR; z=}@JDfDDRo5(pF__<+QVma05#@c{CU53mOVjCe%CBlpDq3H{-YduGqxd(GZ^*39~T zzUx2!zMd2VQv(2?cpY>P03Z=t66os^%e(ZaDq_)2arI*86C+dq*uRPMs-%NQQUIv; zHB;-N71fLgesRhp_*7tG^r^JSlgA+~Ep2CP0yiZpGU@ov#FH^4(~hP92GL&b`x)t1 zlp{`gr5+|7im@`9dT{Efsu-~4*OYR9tnr9XMR%-ALEqsO%$9MUn2 zTC@3R)cN35hm&Y|XEzs9wnRR7o}4_gxn%3!o?UnPncbadPTS8%PTaadr1A+5j$?}Y zsFH$?#`*CR`58@(gQ*KqTCgFxnI4|SGOwdDW>_-zqXO&eFu+!d%4+JC{fxAPyRIPb zS=OvE-p!;+(s(iU09(M2$Ho-+xw#SLqK4?qWN1@$mNL zHe{mTr;Xu6OWg#V^-_ivdQY{e&q>4i35&#~)L#@Wr(z^Y_`+X zW{{uo_1oE|3j@3@sHBmq1?RE**Ho!}+dz2Oyp)j}Q%V98w5Sl1ivx*=suOh;*0TX_ zF<${ECA2Q}AEx2D0`9MoAWXh{q%!k>?jM2|3=-$2- zY~m$BGf!tX<>*1O2-3pf>oR6<9+KRj%F&NGh|OCB3lG0DdmB5ao(2g7o^Nq;RB6 zZ0z}&4*2xUqc3WoElmxRu3`uAefUHD3K0Ie_vvhpT7<8go2;wF7)>%&rNBm_Ly}*7 z>4P7_!}!lL4Ri_$x3$40`Wx*M4P13=@kktZ-Z3UA`*je)q(E7`RoR=j6O!~&2|h|LrayQLC|&^`^AF{ zRZ1Qng1h323&vs95L`W4J6-0x6FA!COg~mfpWhV;w8&KTw__h^DjdpdX-_+*99I#j zscbd!-jav9p?_fVV_h%bV@eHD&xS)<`;%l6WPw&_&Gfrast+a)cA$s&^()S#0d`jD zkcPWb-k#}*5GT?=QI`eS-7%+>>)Q1{QOpDvFM&L$K5WD>_Jgf_Vl{-e^U-yMIio8C zHw`g=%+{}ldj9Zk)^ga$o{!LE7mIvZtRDI*uqo~>%#&z=U5OVyC3Yhiq3^0`E9Dyi zhv8|7Dvl85c>sTPjczlw9^}=|#oX71cb`7_LLUZ(FNxTNIYnnii<3=&voSHql2uL) zKaoyNAA0i^ATYJ+b|amx?w4DmI9jpR!2z!6Ys zOLOYD<@-ywxFR}JDOsuhvXgrTlD^p+*c#x(D%~@7*6?#+5?+MiIa~IpU0(x2b>l1S z0`@FtXvJznNXyFF@dK%h46}qaqpXp`N}t03jY(DX(b?*;4U~wtSx=sfum1Zg{DglP z|6%vMb>FDJvi3b=K1j-P5XBd{7(h(ro z(q0pH8b6759Wb>27V2uk#buJZ+q~aF80!$)8r15)oZY6g;af09(nNP|6dHoby%@9? zZNS$}TEVo$S)5ROlBlrC*lc>%{D?zK8-bd07a{MGhevV$rkiw|BfD?43LG>@*-3+D zXu~t2xa_W%-e_QyB?%5Bdp8A`i)94_)De@-#HDCUv;4)PpaOV*d`?f;mEhTa<4hOr z)CY%4TU?<2^X)8qKT~H)J<-tmlGynDYZX!=%zty)*6|G)c;@q+Oc$1Y_eW^TCy-tg zZS*C<`H6QgpL0Rj6&H;Q^m7UHt&^zAbN8lUqP5xH$%ec zVOn-e$6a;yX}n0#TH*DCy-gd&)x2ARNY$7Pht@$p2sYn{<_Hds8ho-7GKDB;w`a>t5%|Ir~40(X7e< literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_invite.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_invite.imageset/Contents.json" new file mode 100644 index 0000000..cfcd8aa --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_invite.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_my_invite.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_invite.imageset/ic_my_invite.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_invite.imageset/ic_my_invite.png" new file mode 100644 index 0000000000000000000000000000000000000000..f656727d93fb7c02363555e12096beb8a6e9ceda GIT binary patch literal 13230 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

-*b}~`RC$L8E#NI-7CPt(7FH?ol(zqi?ZR5 zSz|t630G69{(4Vrq;HEbT~%r+_4QpeEAQvniFnFsvU!)tq0pNn8d>$$>;K*`R2=zw zhp=sGQsP*yyCc6euiE{cH(WnZbE#=%QS-{^BttY@<5=9Nt)aQ-RN|?ay2l=#{9aKG zBOksIeXuq7A3c1!mg1YI0~MBxmx`kJttu6?+J_@`mK0{t+}}#?vwgFr$)my?7;ii{_Oki5RyiiKPcf%=2$h9(0I^j;G9UseZ^M z{vb4^tg-$+_8a~Yd1tf0ogi4SfY+N0kce4yVU|yjGfn#H^ZXrqAhL3AnrkdE1mF50 zM_Al%K3F@DB~Zr8wq+;NNM=}NXnkP(g|e9O2_Fv${A%}76fu+WR+ zCVcplj4lv5!$%8q0uAL1J<5XhmMyy9hVsd%X#RHLDisi3CE^bOuwij73yf*>vtIu;=L@{3fs6v7Sr68n=7984n}?p<>Y zG4ra*Y#?@mOwL~$Le^JgvVmA|z3^pBxi2(?Rsp3@h+1j|?8w+v~IM&GKAxg&A3WK$l2C)lrz2G8AE+l{tjt^VUOxN50zZMB z)_2>z@>wI)DDtsfelGDLiH#IV(jqa@>IMbWpg{Io^AL_xZ$aT#nN8z}23A^+ye)R7 z1!K_zMX`It_+Yhh00{8D0d)EzVqRLrTt`4}aL-M62XPMI-uu)KEkccl#b?bYc67h! zW5P}`ZC3p~*oz>#6X)(79^i)MPrLn~F-gNp(iCo-`v(8I5!PB|4qyPz2D`)(eup$G z2X=%lh+tm0dUtM_{}@CtZ5}n^ea){9-!tYNG(Zq>#4E(tL^0~E>fuyoY%&&T9)oNY zstkUEo~~QTkE>a;3@B0tADV!SwX`(7#vYY3A8;jzb$Fv;RrA#ZeJ{0~7}>oK6W3Li z1P>k)0;C3ezdmZbsk&5*%R+4rR~XZdihj=xSy*|rc;R87@{+n2_qjq9{~DMxn_0pc z6YusBq9XPUZv9VJxiRML0LSzh_8d>^9TW90TOcnCXqbp-yM6p(XY9?8_K#v|70wT9 z)K$W_vJeGIDxZ`K>oW>jtC0t9`Q5Yzd@*13xMjW^?J7CG^5;mcTSJ`xR-?p}sAf9# zmwU?6nGe_i7_$W9j^2j!KlY?h>%*t~6Q?vzfeYI0vpv}*FR<#Fuh!5p=Q8UVJ!WV1 zU;gPhYZ;$ae9qaB26s=X)COX3h-uueLQC6ib@{Sx zX05@>?=&0Se)#<;h`~zW?qPM&(>I%hJ{l4}Mf)@R!xSV}@9F9jh9dcy+A=9`%M_1f zT9sQ9`H6ez0NSuII6zMqGezGfyP&K3;X|o+T_l}OePQy2h85jV(!cb74T#`(7cit6 z^t#ZO$VrbY`($|%oMf%Bfc!i9nK*%R(NU0IMas6ppLm-?dKSy?ihS^;PAqLj6l&9w7HT&*ewi!S=9o^?(ZPxsfimg}fL^kx# z7f*jMR6I?AzheVeqZ8<=r`o83k|_=+l%g+G|8$TTh6ODdakT0keKMT1yLh1m^k4p_ zB0fv{-4{cT*BcR&Q2{(3(^#?J9v^XpgaiOy!F~2Rshh|>_~4{FLKLM)*^wa+FJ7?s zfRRb04i@IaB6fMHDR2OV1AZYY;q?#9bdXOL;+$dVqkpB#h1$Z#iVT?+(tm+iXRoazFDVRLnj;dMt!B_&l_y$cTs*WIw4;VjY@z}2*DYI?!}w~|)fnBcU1*Ue z*$zGk`G@IAdk=y)ORed0n7@RRZgaF!YK&KcKmcdpkxKq>HZLbW4*M(bc(U$Te)`j6 zpgPSrq<+{?emOTIzkECMoif^7l}&?(Djul5vEb|N8&ADqpsIOI3@AIL)(@L?qV(CL z;cxS(!3}H8%wx?jGIK9&@y}Q$Xe~$$&F%M1vDFTXBM9c5pP6h<#2tK7Rp03PRe(vz zO4R3Aek4iY6^=MZ7;ax8GGkj4^q;RhD#`lsTKwJ70a934^@JvEp5 ztF02R6T=cp7BlURw6O%VWiHR7NCGpX3I9&A_$VQB>o8+GA$qs37y|Pky1R#(abmPeR}RDoGelwCW5o1=-tQENdPQYR?D>A#g_L2 zCcXrnPw-e%nmuQxdahEeck>k@=w5%^|E!Mll1wQfTxhCcy?yAAa*Vb z5Zk+puYQkvKsp3nJwgfh)DPKrTSme1HV29 zD5(x8k;G@Dy>cEi3)y_bqPCi-;s;hTy(+U0aFFvvoKfAN$5_%^QMdBGE7dW#{(>5G9NkT0T;lk- z`1eK9M?|ASKt=YOJWhSF7yZ4Lgrw1{K}avtsm0Lhq{1sR8ZuXZq1Q9gic_p-5t3M?2fq5u$0($~L> z9K>Cn7e#Z{sxLKdE~>uz-S+0=d5m7H0c}=JTRIP5Lnxp=%-B^E?C!y#)C}J3pE^(L zlG8r4T0Vjv$~tAR`z6JBz5nCVv$PCvKaD`vjjUH6X+CtwiD1}~Pmx-#l1s}E?MdM? z(vFXJe49HZ&;_9?wRfq1YpQ94$)8tF`JI0wyw?h?-^e-n>t$CB&V4>1DBdJA`0Pej zb+sd_uiY9Mg%J~xj6EYSmzSrbR+V|1i0HDcI~$}}Ye?t!fEDr1f|h^HCQTtM+=}<` zB5hVyA|1Auqoal&et8Bv&Do)rn<#$b39oN9|Z0H(6;Tt7?>{`0kr6roBC$+EAC$*7h5B zB}Hm-=TNlP={rIKEh)%UXhO^X?xP?V9ZH21@8i>zl_wBVxs)Mw0+$ z#kJ$8=wTl{+uaHjrlHs}QrnBN``z>%_+3#GYe_>cORB@%OhvfSRX?65xEMzDdAl|Z znHa^~Ms9FbJ}y-rdL`XpM1SGp1=xnCC)S02!0x#SFSASc6+j+*~uEvcSj2f2upSl>?g;ZaF*@F57I;N46I=U#{LIK4b%EG}t>Z{rUA;)fj zwPP}d!Z(EX%UlHzG(Uy>QMSMFR4#)TgE$>8lFwQY31HGtnAEu~$v!4gls_vGbm1}x`Pu~`4Jy17Q0vP>7kXt}yfzHL zL#kn)6X|lUIg>`gdK6C=V?U@WOerFD)14SYXTqK|Pa{z--f_}Fi96nt(V=4#T zc;bWErsfAoqg^ymWq--ElGUIgXm^V}@Cr~j`>`piX7W>ab?D)HrYH8y00;184gdu( z9>R!Mi3+8VAXe{j!_Fbv@1vyds4QA`z7DK5Cc?+023dbtl7>3=O)+6X_WIIrZP515 zc?h!vv#@e>E11m0WYr}81BxIk)x#iD(C_47Y*?jhG;fnNS^)DLhpap&7U5d;Om6`^ z2knh56lR>Hw^hn;8=x7xm`?lyq^X1fDawI$-7ftPFkyffxZw{T%^G19J+r_D|5mzz z#XorI{yk=31c=v?6#$L+bM>l7#wX%e0ien?78rQ=1SmkC2B;S9z9nI7p;R?;#Li-+ z5(s=&?0>KjQ8equ=^Y(w)jqlqV4mpnG;92_LY|^^83>Qes|Ye3KQ%0x48Uj^09s)T zn#~3ZQMRTSwp`FA6$1tWs1(t>=@&>D=H}{3HTqh|h)IiZWaW7}Vn+_b4_o~2#l|r# zvLS05s-Gm&(HEzjut?Q^tAIIFCP7t(z*7gyKvq8Rg^*}(F@WmF+XGn8|Bu7BngDPJ zg}+eU0NjZO`e&OK;j@z+zl}(Oa1B=h{gE>OO@$YbD)>7M{IU%YDoC$@$wn7mGG)svJ-TIt|3KGc3M zh&{Et(Tm2b_5XE&X!@@FBOn+Hj|IY6!LdLKFhFGTRaw@KbHTuL@&QWO4D;1=qX(5A51* A-2eap literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_upload.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_upload.imageset/Contents.json" new file mode 100644 index 0000000..3efc94c --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_upload.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_my_upload.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_upload.imageset/ic_my_upload.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_upload.imageset/ic_my_upload.png" new file mode 100644 index 0000000000000000000000000000000000000000..a5252f5137d705f9a1a58f451470598217c3885b GIT binary patch literal 9855 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 literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/Contents.json" new file mode 100644 index 0000000..73c0059 --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/Contents.json" @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_black.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_black.imageset/Contents.json" new file mode 100644 index 0000000..d97cae5 --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_black.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_bookmark_black.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_black.imageset/ic_bookmark_black.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_black.imageset/ic_bookmark_black.png" new file mode 100644 index 0000000000000000000000000000000000000000..01ac1c8ccd7196c0fc9c2dedf0ea502c2b3cb31a GIT binary patch literal 5921 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 literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_homd_search_light.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_homd_search_light.imageset/Contents.json" new file mode 100644 index 0000000..447961b --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_homd_search_light.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_homd_search_light.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_homd_search_light.imageset/ic_homd_search_light.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_homd_search_light.imageset/ic_homd_search_light.png" new file mode 100644 index 0000000000000000000000000000000000000000..9461207603ed1cb2f52e3a0f1bccd194a49bb0ef GIT binary patch literal 14511 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 E1B35HAOHXW literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_comment.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_comment.imageset/Contents.json" new file mode 100644 index 0000000..9498248 --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_comment.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_home_comment.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_comment.imageset/ic_home_comment.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_comment.imageset/ic_home_comment.png" new file mode 100644 index 0000000000000000000000000000000000000000..550ae9deceaec25f05c40f67b1fbcba0dc01e6eb GIT binary patch literal 10913 zcmdUVi9gie_y22V%*akc_H7VZl5MiL&EMt@` zAsM8yO%d65#{PRvpYK2M^B9kL&AsQIbMCq4o_o&ob?=|IKFh-?#t8rb4+eb(3jk2? z5eghYfM2T-eOurcTkt82{Q>YV`oPuyz~3A=v|}&;@HVslArFu8ii3@!A!ZICb~n63 z!Y>DT0pa1{>b`;g!Je0KUg|f3d~)Uu#Q;DCz??a0ACWsZeCwV2ge4it_5d>}bn)$deQ^66QLQ$SP+_7DB1g|mMNR*0QVz@GN( zXr$y?vj53hB_+N?Wm5Bo1P2%||z%>Q2> z>{@M+F@SOG1iWE(`1Xdq>k`Kx+22-t>KTMm$PQstAPb+HYup?r4SeFj3a>V&xYlqYZ=o0E88FOOBbZ< zFN*iU(_D=NO%gKR!sI!WZ9OaiPKrVlDs!2#dy~uBZ)NXnRjXS+%EA80_f9kfz-RY3EEHM zbaCoapN#qkgz56PZ){ZN;Llx6=}U zI=@&dSCVvTEulkrG;?ETJI`|D6X-KKdXEA`cKhw)2yY+ycLSGFsoxiGzCuc*4Anm%dtAzf*ukcC!*H_xss2BcQxR%U3DeP;scwantEz3V(v>R}?)G3&a;lLJq_ zufLAc_jMk7Grs9slBWf)L#XIT*)^&kpV@Ri96voTL}SON7aOPE&V-r3 z=HXJ_gP&q$%Zh40ij_B@6KF$u?tBQMLf1&;OVoGnXkrMji9IMf#sPTs##&bGLr`Qt zJE_NMjjFYFIF&3)$y(L8!i2KaZs3t;-K< zhW>WmwM6%Rh%Uxw0d>MXWa`XJ7b?rJ{_3GY+xSPMkVE2^E` zF?X>J(^dqO(u7Agp}yYa>9~Pc)%@>{&7W1kbcLwQkj&I% z6i*p(dcUjnXP}(g6$MPmacQPhKi`KJ0Xc+0^nyxmvfb9pa|*jdCRUMI&!<-|t4{V@ zpyidDNMVZdMQMd-n0tTx0(ua^5T`-u;Y&p+^62h?IP>a7;4O*YBKMdA49W9 z`ubVAE=(^y&ul?pU)~n6g5GzlvD@9$qK9cwveU@KGLfmxi2nE)wEC7Fv@uy^x4=M* zDYFRl=U@65;E$;gHNHsdyCwqZKEL}^Kesx2)#{d8X8xU`FuAFYq=M)x5lV}{RZTcB zpRY;bB>VZEpRVent&vZ8odSubz4fR3Z|9%i-PFrnS>dF|@{sJKZamu?UDbe&B<9an z4JQ(gViH&FW2||gLy!S9|r6uXce;SsC`QbhO5o{-wITP_o%Y z-tNo>{U5&M59P!tJ45z*S@&s@_PZCD8!DHK4hN22Bo$bITwDTzw0+Zy^o8H=TR6E` zk{)cT(KkL{;KMfz`Y|*Ju38f%>-u8MgKIeg4(OjK4C(D-?j2Y^Uu46eKD`hd<<;r@ zbNU+n!^Kb)wr^=}S2D<|>o43i&Xy>-GAGBaoWyADIzd(MiJpdpY2sIKiU?c2GZj6bWdMJ?s^h?Uf4 zJNwbIwU|#^9TncHlo#hKWsTWy+ zsK9~N_!g7v$6oM>27fR3P(?Rw^}@M_8XRw6ZaD_Fn6(#aHjS#qwwe0WC_gA z{?ek@8qtWTbybD_Z`=&wBJn$!&`IxZY*8Tx`3IiIet>9!%(abv zpQNNVWLJBDd8&IbTyR;PhgfV=p^5Ccm7O91iKQ6^o@nVmULk8SXp@;?J z2bBjG;u_K3q`M{K`oEp=8yOf&I)4FVl5j;oTLk^eB9#0_pnOul8$?}DAG9- zNXJq6tD_pHn9=pG*6aKlak72|*;Mv16ZK?>5|D^?gSEvaFY(42}Iy18#9S z>{;i0Yz|>S=racE=t_GCb{JncqC|NSnzlanW%zOpkR=Ru>9oN1tsluT^f><@Ltls6 z7F19h>`#$YbOhdI?s$H7@`!zwOC|<$mZj}Tu06nVV2ToaX_Dj8&QdLNA6^O!CLB-P zjeb&@_8&Ry5zNqhPje#QLID~2;GjX&u(gUdvZ}WyW3%>e)yspV$MdI_!`qY=qg*kI z7Rx4n(ZZ;w_NTCRb6l!QH+-UbQPrSezXiSaxZWir`lSP;EzP{jii=;Y-wB}Vz%H>d z&v7ucx~hs(F7Mshk^IVIfL!yD*filAI*{w4B1=)8fQh#aZc_2iVHRTF=V>p-CGSZA z1sN9TqB;(;-g0nQ?eQMtyIzc!f8ZrgnHn)2(e#za zT`9j#eyo3|zJFbqar?>-^z@U40~@|;#O<19{5=F?fv+i)xMLu5Jv5opu-uervHg!b@s=x7sBM!?xYpV z8zDMXGLy;GD4y?UsO2+)f3RsqOV-PwPrGQM;B(*cIBV*XzaY-)y*>Huy3E4JptNe% z1G4aA^oR|t^k~)t`-@)^bmn1-S)g1?N!V+pQHL-$koY)JX>_Hci$-B_36e&-Ll*gc z1>`86LuaVGE8&0WsYpf0M(Pc#-~`jqq{OxXTPb(}s4)qMQ1)HdY<*Y#pwi)S-!(AJ zb9&E-cURAPfLvQrUQQ0k66rks`FGd&jEp){o?_8UXl>V`2FT2r$>`9L^g}VkFXa;7 z8(dUKeWmUSqvyVu9O^vHxpr|mG^s|o^E3$&tahvA{W;oY_)6+2D@2}*P2sz&)j$HW zzbTEZye%;{e1X+dA0HjQKM4*c-5pCG&w-A-C6P&%mMNG^WvVQj9ZxjB`RZ`LjA%Z#6jNc8Ux-Y z`vr&w+?yr9d`x0FiI9Vj=K|^5B9=?GHi#?%4nyd?n~UiR>8a+wuPo=TTH+}z_T{K# zL(gAav_SC3-8uI06H{@eR}X?=LVr2w-h$erJK7S3>62IE4rXJI@nIe;%Fze6dpv*l zq7;j~saAPV2AzaKW1p6$`M5*}w zI_GQb%`CyVQ#XKhkOV2TC_euFa^!_}BofVK`&wr2DzyllrF|RHLvzVKNm$~jY87$2G6EY3Nx*q zQ(lwI=bv+Hn&itCAp2eQ78AH{y4sliA}Ud_F$cLcF>|2@Z%M(JIyCnKoQ^uTI&KJE>YgYEbid~oA;N03_YdZ{< z*jgO;y5N||!MNMCtEEVVje`c%K*0M@*VD_E+G&+3LGTF@qs##)(7;AL*lyPkv+`@Q4Vq+KAmf#zpH^Uh!ket#C7(@kA-q`;h+8ae&I9Jl(Z#cWIPY= zl*SfW(k0pM0Y+kXurgNd(GK#kba-ZW{7cZ1|TaGC{!%E%`5 z`99~ut1}e-uU(0~bsORvX-U#9+edt3Z>JvY#vg+%d_cYY_J73BZNOOUtA?R}eSRZ*0@@qzsrwk@LF6EYE0rmt3Xq{i z(umd>xmcGQ+FW>oSvN2wAYnt&|ItgcYtmE$BEc1R2!Y@(u_hDJ(^23ItX_e9ZG6;R z3jE#LCutw=87t6$uCv-aG2MxBCtxxdc|oJViNk~99nhz7XW3hwxmCGXhEuNvvUz8v zB`V5Qfvs>Hu09{$noiaQVBl~m^>Dq#YaQDw@< z+(dAu9`!AtHG75_5ytyjZXx=6=XdeKc@DN zN5v7;NFcaA?NE=ib%@p|LsTBJ>f9O zY|_kPXR*95b@mB_2K9*2B5)9vbfLx2F$`Ax1T2*RH+An(`V{2Ch97#69HGHPUnWyq3mY^{UJD7Sy(V@{p1cEn(F%u)(_^rDkJ zAzv@8a+J%mX7vzmdW5zP+VJPD1 zH0+hA53BQetDO%}WQ>1&6Jbi4Za)h&fzx?>SmK5!YsHVcL%G4R1DTA7y=VpHyL#7} z>P^?GPN3Xo^m0ueLZ52<6SQMx zkj=83MuHUwYlZ(9{1f$HB=GIp_Bl&z>fdnS8|ZNo;5^(8fNW$in*Y;$05nC||HfG2 zS?YlR7CG{YvZ8=H@cIC803{5>l<*Su3|OY=nQvOU_)8w(!q0&_ zp~jgttrvfxtd$4dw!Ap{X2+8(!SWE1v^ug;PBpN>-fE}`iVv+F ziyzxnMY+9j1h{AO{L=(^s$p;>!rRsdQm5ss;^dgQ{PF94pIbYt2c} zfLhRTW;XjK5LG_nk88pJY+L!Oq{K=FYtvDlkLE0I?uV%Yh;`K^F7sS5R;0BR!8zfq z8vliBMj+omuRCpk_^yU3&;ka*@aqDGU*wc4fT7dAbn>rMb8Cusfu}|dqE-A}1-Q-` zTJd8>geYJZTx?toMEtfVm1V|I(oNb_lV+pH|BjwOwWWMmo*+wNdwH|T{yu0Fq6g$y zB^Xfe{f-3rq-tq3R|0?U!D(Nga{-H*cqGqw5+kDH{^+j@YUKvL!B6LDVWBZS(KH zugclyX0g~7UA&6xOvlH^ds<+7bNNExtvgQO^gTf)?+en^Kl6inj}IJgPKv!=oN18H zYz4v(b%C+S%fv&gd~CWmBEHxo9plb+Mg(&9U%EAJ)SN=ux7Uup`}UOBqflCv=f84=1Dl7B-Zogc=v(W=4@6rl;$Ps{*>>`C$g_k??^3EUUn8N+8DVs&?ky*58 z5u6B^B|ia^*6Jw73}R1d_hlo14o_klUca9YYP+2!RWR z@uM~nbedDrYCwC7wS|sT0^MC%**#;SP0N`23@ijpzsPrU?0sY@QRWi9-+^xj%NK+; zZ$!4C8Lb1XlrU*Q!@G6M`F?*k+Jm3R)v-#NtGs2IOwF5uQ^in9?A@2-{&p zV-urA_BB}b@kIRi4AKM{a8?zgFb8`LYFN#IDsoZ58rK1d?U&f4K{%jDS9@9Jlj@mK1gmTh{OF_j>n6F#KU z-Vj)+M&^BT^%nPLOPK;KDTsphib;!pGI_!S!~l?0oyB;yaq*9Cm(+zQ^}G1~@fp(^ zK#+xvsDW5Q-+uVbkJ44Ny0(G)wSdTmV$?=gTRU4{+W@Yts+p9X;TB( zMp}68RzHZj=#aLfeg;MtU!?za#f&UX<$&{vT2qr_tE(+pxXX755BO%`&z39;G}f~VDurB#2vLh&(b)Yk(>D04cF9g0 z6EDsl9bZ58)q;Ne8Yyzq@Rq@dFhhsMU0YgyKeL%yGKdd1~1>5&q;`DGP z$4bsyV_PG%HVu6A7VojTxLn6a6UpKcQmD7Au1%5UaJm{<53f-5(cN}p;`bUzq~CLu zp}QR3$3r^-f`s-@MqhS*+GpW+Pi6rR`BCZzpgHX(nL`{_${_Z*^aY`t z);MMim$-OD3G=63!og-wbNmF|vXcbe74RyU&w~Flonea%sDym^bb5Cv^Ds7Hf3LOe^`5dTT#+?XY zU{UbRfsN6hv7ED5kfs!>i!BJ=w#N6VP6GbQTE?n~b+zl8?{jAB2ab+MFF8)^Iy>(l3p$GYBVzzRceRwB{oC8`dU^OBhGHV+(U1% zkkyk7Vu3iz(4Ayj&I1thJ>p7ZKQgdihl$Dvd5E^Ae=UI4V8i&CI2KZT)zG(Vm$bX* zOtV%XrE5pmLHN#x9bC_eyz^C0UkzzZ@WLitZ3 zzI7?bL%A>>-+E-QROHtw6YQA1E!%6@h>P3_ikD(q!7t18$(Qep-t{>YLW_?WTnmBa zx9BCjL}%Gn^OXX`qohaV>((!Z+!zgmCA3@?#2+X&b=#$A!5G%50d~$7J?3!Wtbc79 zi&d&i4T@P;9@;oPjcPpezM(0qhKIKB>+@y$wHVUzn6;bH6B?nfwO$n*_^YVq^tgE8>x^!<_p-RY^@8D);;jgCe_kv1RGevyyE=d7<=ttyuIxX5qvD)cBekX8<@zP! zd1$RS>uf0|Ty4Dzd7oVOkdOtr2J$5@*+u@v2SfKQc(kM<03Qg_Yctgs&$~S7U@~SjP z00XxIjIGw-rg~H5G!cCKTW{6=957>iQ1meh;tamO;L{dw<7pw>=z!bAcndf~vk zxibBSpfV~O<{mo#YsI9f*1xb-a>BEk!w%yN`rQ}V#lP-p;3D2gUgr#BerP3 z!RB9&IU|uA@L%gB^D_F8>#02IuUdd)KF7~6FKTS#t5K6z(}@;SS0_)f_^E}aRjuryvFf{=EQJg+%uXZd?vop&jR*yW#>6Pq{>uI zU&p9vJ*>ozsuVTWNn1L@Tt?G71fH451}q$*cW*}C-3XcrHrgNDfK1YfzSl?LseOmH zQ|BF~sbMtS;4WyWh`= zKuB-1Z7Qh3hXbPyFhv;Q_^~Cej|40BJ@zT1#L`rCU4^|EVbWeqtT^hfeT4Rt1?aW& zRM^v6eQD-V#@8Y~Wcnl~{8ii}R8_jOHx zK*e40hVxSb$zK$}l|Xnvp6chIYvs4KJi&mIP^bv@xSbW4oU6Wch}%GWMyJFR*HVc39n|kn8W&t~zVtv?vb6 zW8B0FU^)wqe|%MVF^tZ2xmU$Fv)0k3R^HVr(sup?omO8}f;6g&fqehkTb)nOvv8^v zrROZW($tOOjDzc{*{42G+1Ik)mc~E49(8>&hAy9{^H9q6w&>AMWGGpgwgBa` z+XZe^B$G=3s`=LJQWiSJIp{soj`s@`T{MKETih71+oc6oBo_oAV<18BMRSk1->lss zGy%JmW?WJndA{aH3tfuMn5+I#CHJy}+XxletEmrCgn5&yWvKV2*@JS_o|jx196|V` zOA&aoDu6Ax3C!L(NuDS3hOCidw!_>n^krRWO>cPG^FsE`QTN+3P_!S#kvv&EkS-iZ z*&=@NP7I8X31Fz=0Uv<8i+6_!r+y=6>G$~2AG6h@dUNnX_KZJ{R*RSSfGc`IT6FG(qf?FybGq3+f}S(iAjy_%p@Oi66PurKSuVTcAA{=s zwo(#e^GW>{nx8Vmb0+e3W!HR(QR4{Db*ZNPIN?Ps_9RXWckl~id|cJE3%qZ&X{9Ix z(U|IAFJArcv8T?W9UZDylV)+g7S#w+4xs7_B{?Jz66l{D zPX%AIOF^Wjefhv48>xAJ2KrU$ou&(|2zkdS_b<@COgP^DMg{&ox$f01^4SAXu;9pD zr!}~*d|A>eWjvis+4=amy$6W!6rKZXsnyDE_wo*axsSg zCs?$TJ{#Pj!cU4ziPv+!tDcSr?|)err0G75l*SoN#U#!1`j_6{x{Lt>|4s@cBZ(-xG$kf3vCR1kao%;Rg3*B8^0Zuw{ALQ=_JLJv>5-Y#($w_p0sxJAFj zXCt&1rGUYvOl9o;1e1lI2k&D@Ihv)J0BV$ z%oiV8)j$zl$dcb15XKbVj)QH0TD=YREVOe0g;X+fP^Zc zNsWqyu5<(;qJVTz&`5n(Jm2&D`+mOHd#>wT>Spb=X3d(pXYHALxM^!`%Ckp!4+KFx zSThrQ2!eyZ!l7MA@W*OY-#Yk%BkTnB^e*sU%&tph@bBHhW-ei1{2=>(*a^AJDDY4y z{N$N%hag;d?+<99etG@xk`W z(g;Njxv!P?-&;h+z5CKwlYP=;$>YZ@S^WHIZ?&alaoj|1x#Rox@8y)97DYaI1ebk6 zB!3&SDjS#B2=DzoY8w&cSQ0kdkURZ?FMhFpd@;Yh5Yy9LxL&}(kNgg4K{y;v=^K&(#@`r)($dqb?ukI)C4tcYH!oc- zorhA5Da1UIGn5GDaz2;X@gw)$y6g2GBLkS8$JWoAcH0~Lj4eh0W1)XA8?9Qk)ND)7 zNLJS>=Pq|!tI}Kk$CISfBp^E3Y=aVqi7zny1|)u~EITtx$>gS;6D9a>hQGha`W*6{ zL^nA-(bKE2L~UF9AOpX?c*_i8tf5!#HeALv%J}T|jPT^d9bIjQ9X%YW`D-`RiJ7X? zEIIMK#cjYl5Q;Zi^NG81N_o{{QYJHHgycu^qY*p=(RZg#9TUl=-XK*Gt4I+vg7QOQ z--$)s^|w%MCmq>2jePW2x)C0QD8I5+WfJ(LDU)}!BaG*zMoK!o`Bb@j|LWGPp7ZxX zn8%ytva)ql@bR^XIFn5cox4^<5n}wk)nn$53C3&veKz|VSLK3vgLQ-B&xs5jJl*?e z1U>>gYd6qR`FVlyr+<(W9TF5OJ_p{)pi~|xm+y{kBUt%9TF=4EkCOSvah@oI;b|7hPlrVxz99}v;Qia6Zb zwq|U$hJ0FtwJCm9MTfZK)2f3JAH4f#w?50?AUqgV*SvLBjt+lk(qc$|eP+ijT6J=1 z!XPE=SaDpOP5OTAvHIQX?L0=gW}k#vD&-o>|9;aRNIy(mLX0n8^R7L8hjp0v`~FpU ze#D{+)1Qe!hv4yP=f5Ezy^{_=#E73cib=E@N@k!LgHjVY^D3GllvECb-SY_re=_Hf zFcZ-sPWXH0Cy3jYAh3Lq*E3nz-eNQEy6@aBUu7)yykuss}#lK)O8;qw=~MB5Y${v3P@Zl(z@Y zwj%^B)*Gb9Q6CRs9cbyb?ZGQGW6?^%FQ`jquG$#OYNu|<>=Umx_ZeACB2QzxJRMf! zg(_6Tw!`0jbq*dNM8YM=JYz|EAytf8D!p)PaD0fV zh{%$i;lP&Ynb0ECAc_Yf3tpU({l;}pyG?x3lzy#`M7<%Wu=%^A>q1F{LblXX3@ zx&L)D?4kK5eT-S^>|mU{^0pHn7wj-r_l!Np_iH)WPc(Xx|8459OUJ1dN2Z$UJV8jI zztw+dcgzt?;;>v-UdIRhdt%hw_&V6VO5~*F_sQg^eZ%2QJX3(#%`i?WEH}llro%mt zdy>%GFQ7?$bslMQiuFUW=s3+^u zG1m#!FoFEdM&YI0M3@3x!C_mudLns|<1=#p0J^!8x96zw2oO;)m_vntPtAx*v;PL( z7Gi~&=xMw^Nbz_d*2$P7Ruz>!sp8_V5`GLZ?mj;}Sznqt5E{xWTe)h6!+6EZ2qo)(MlAh>ue#5m1Nfp4HOWHLSaeTppQ8 zeF(2t|ML2DL3}Y0FLgv5a41cb6X&Pkg~f4FfaJv6`Gbur`*?`dLD)3x zXj@m0x0Y>}Dhq2e`Y=o80rd?rKtAD}|I1}%oGxrgN;2OIH4{&LA%_y@nQKkxmG92F zNK%uWc>GPTP`Ytn6slaX+%+(x=_)iN2?CGUr3&G3$jmjd!t3J7ykcyq;W9rn?L14c zR6<$_HP6$K`9<&3A3?@Q=8x^?t_eRKJl}y)!H1WPncvB^`WQ<=h z#~1;xvOnc49ANsi3dE zHzXMoB|xyLc&(mJ6?E`GEpQg6XXY1(KQ7*V`CDoBDNW$~e481ewQp3kC0Hj++)~9Z z4V^}H$LwWkzg88ya%g#~PVB(xErVR7e`ZNmFHoZtXh7aeH~he_g!*MZGsi~;P|UwmTJVR6kpZu1(<;eL?l#Tmvh@-yWN;c6 zVTZ>>YFWG7-NY?Lj4E=+7m{uy{ZI}h%_}By4!5xzn90f(qw?Rm3`qxA4Dadh zLsfARtgz8C%VM6H^NRf8`sKi;_1+(sQQG5X8P>gK$i)ga*_6kSI|FwKlM3V??#jL| zc1f=)2%=38_Ndt4Q+L;Lu{=$(GQ?Wm%FdK9c5Id9d^5P@74GK>wRlpd9`E}gtIVnw zW0{$39>b9DSoQp3JWdfgnK>oYmMvZt2=|SR&Y<=ab3Zmr7>cpPmxTp)aL@Lat{qxwWfxqif1&s!4fQ-t73az3&m6kLZ=!QwhCWcm@NOrS);pX$b`@4h_Wq>n@ zI;!(Qx&|@VwpnU+-mpGq zb!6K?o)Di|&(b=(X6c^z<90xgRY5zf;Iu{w-d6{4aORBX-FZ>>+Py>FT({!yQmf=6 zRg2TI^NeBh0%*gzNv#K!3X>O}W<-JO-$z)T0<=p54QCpXmlpX?P zlP|w)W4|_NoRVIf>-~3(VjiyeL3SRZS6<_*^YC%pGb_YFp9tnL&&TtMV%h>x=SvKT~<2-Frgw?;R<{q;!OF9V55KL|ts z^a_l5u&r%Fcf_G5E_<6f`)(jsSA%#>asq)~@y8!1N|+UD6qrnR_nvd`JxA-)fck0g zul2&ir*^fae?jQr`ZG*>FvO>Yzi&RUaTwFz9zSDf*3;}TFhM2g3cXfijG zy3b3xv#}63CHbAiifd=Ku%8k_Q-a+^KKU@B!Cm|G1s^z0I4pFP3I@(6QU*XZsBRmr$qv6c+u9;cL|Z}lGBn1cjp z;5L~wew?B#Tw)7`B8yPaOKi`B&ukiz)22^zWrn7d(ZsA0XK?3e%AhSM|`RsM_#Kwzfq3} z>72uO@^;tb`4xgXhOF_BI_Q9VFxoma+IP>~{=+Zy&!eOfo z%oIbJdNS%!?Hro&aWiq++NPd2>imV&=c2|h+{0Y)d;JWsa|mx(;w5jPm$}42o=}Uq zM}{{fEcFw2t}z6_JyZd$swDXf*_4s(weKjk!#>L-HgU&U>OznpVeY}KP&4Ob`Z=w# zON{ynq$a|)gwjM@w^B2J{Ds&zrVFpJJ?9x4^f2{~Fa0`PIQ_#uJWMy{$zhkNy(x%k zW#JZ2gbzfEe4+)l|BfwR9JTrLjbM!5E8@|=XCoJe@$&W|CllSLM+-d%EGSNhE`$dq z+l#jbR>X6mv;C0=EGBV%|IUWobV5Dr>^a99{*laA^NKvdhsx^{o7c{+&5x(pbIo45 zV%|Al-^9Ky$5&|8S?5rTYb|wRUVg&%NHX3q<|^oYeM>|u$%*?wU*i3b!yno=hr!1V zpuN|^h)p0W3LpZX#A)4Hn8mzSGLGOH+Rsn`?{u6a@kl-i%zazTDZOywik0)>NJQBk za>BZXQS&+2Cf}OE_PNlN8bwFa$gOoN46crEsO`;o#c7Q%7Q;GI<@@sDe_4&=An6hw zH>*actz3@zf6}!MrmKeS|M7ZB|ZL^V2GNKy#WmDin(d33fbtl_sGEU4@E%ec75LoJh#CoxfGihmgx8QYCx1JL|~r5@5d6N=-f8m z^P1w=pSIPgQjM{@0kv~-BU?GGycfX3bDX?wU3M?{&&W&W%=T-|uW>WB`wCd+cw#g$ z*If?N{;gG84#uY%L?0L$elsrojh^Cak81z$b?}CMCb)rUPpaH~6a9nd@U(ne)vcOK zDAl=msu=OyrRndye)Cmk<=bcxS?aSVLxy`}lKd87y@2Y8rwW>;dhp#JQ(`rijFj*1>Y6Iy}7T+ksR<&pR# z@uB+lsx%E($Fsa0U3SBbw8rF~L?PnN?8o2f#az&mQuSjrx*5*f+NHWX;dC>Sem>+t z-oT~I6NQb*#|i-~&%QUL5WaLjWzf(eS3`N}ITUrR;N9gO_*7+gPJdCf1N~as8ihJZ z6lraKuRwIR!M)tLWcY$`Zxbj;RgfTZ$`KZeSv~{C>meIHzZs9WbM!Mv&Aw)T)lj=I zNadpt0479a@;GO<2>Psyx5}IXRs!CXnf=sw!_*`*NwZpj63$!h6?)s0lqWgy@#f~Y z)(&VM8@G~ct*@&}kHg#!JstenoisRc1`~68GgD76zi(o9x-H@$mXRQjsXU>{Fo*i8Sq36ItwY-EKmPx>tzt6y;Vfg z$SkSl2lJvAS>_Ku7AtLE+w{BUoZt7Y)T$Ep?Df`U!j&(%RUck5tcDMr3g(x|bn!c_ z@r<{uLH2ZVy$6S!4=1zx+u|1Cdg!57h?_zk_GYfAT1s?QGi*}-`#L(r3cr_rJb6{J z?POG<^xHJvVsLbGQ-`g-!G65lz1`rkUYFQh<>+{MF8ut58u!hT5}NDk+P4D^X^d1x zDS!qgXp~cNY{5uBoG-8Uilpi_mm^p%dtND*Lpp8aatXoxUv7uf_c;b^s+C{5a2rL+ zt1rCsIgUa&{NjxS(*mG3-tpzzy`)$${Q@mxHSgPjLrKfON$OqClQJPU(6vN)=`rd!Q{7^MfF z++Xt?vdi+oZ_DU#nZjX}XTH7Ol#9i#{j*8oKld;{^Y7}lsDU3o?5$GtVD>}yXVM5# zdp>}%Xt4Zl{)%;eAI(y1a5hPpID4u>%SPKeNK_#CMcoH~vy&~&L@~L@u+@>z4Bq}U zoMs~Jlvgg-@)Qksx2RW$*!kXl^S5@e5c%ZWS-<|J*dpO_m!bxG`?dcVY z)`i1gXDeYtArmgq6D)F+w$3$|l9MflqyUl)tkF$DtzcMDY*o$pf){~@2Qc@F3TWvVa4rzC8|EvS& zzmjXiizPm^9=9sotaofUx3xlIKvun#bWN73(Vi!FHJ*N{NljHvZ7#1P<5wLm-?00#8YHr+ z+7Xo_TXXgp`v+=rRZ8t-jo3=0=avCdsQVB4{-356Pzms+W#Hp3Tu1vwi-|xsJDKUj z<*mg5QUj~gVw{RT9!#`n4(xhHZ@6bGnPvKF2$T?F5BcnFTlw`hySbOI8%i{3@h>}c zYEOl%4)BRkL4ul^h^Avey`B&2idq*$rKcg!CJ@B9Q6YqAv8{%kdan#f69XQs=XcL3yZe=i5qh53p4 zcbNrO^M!U5`oQ30SgpLIxP|aYM~D9-T1KQp&$5&zq#(@!Yu?dPqyK89#j5$mis7Km z?eUI|PqIb7It{@0ik;^QRmJ)69X}i{Ri@~dl1Kk$x*Ft?yIp`VfZl_FQ%6&T)4uG# zHx7-8p&bA`sHs`l^*7Zg$LE&7-n&Aq8fpmGZ-rMsWB)+{lEJR+R5?2F6u(g*j!@oe zsHwH%&-T>DaRL>m%KhF?q{=gXU=OhiJuYX#OOrBI>2Sr9V@kxodoQW*W$H2P9o}bd zcaOr)gLf7J&6PUJU)>fWaz~n&3>?}8&bp8$9A(xYe0X+Spd&xg`s9Rd&-{*f6G!oB#qeBDf4XDvM~FBXo(ww$u3`g z>MHxMzLf|8jkwAMAdOP$$G#+ z|CM(EbXSI_$@YJxllWUROD%Z7V>B>f4A!Z8PrH%Bu9;c%wFyD&ve^Ls|G%Kt*M?+_ z06B^m>}y~+ca#5bZ^f>x+H){aWSjJvR3N@dW*<;dVzFx)Ft)V+6{Zi#e1QF$k)Gav z(n%@Ce@}2^4>J)PjqPDO8c=@inV#iH8`w77(q<3Sg{`!rVqtADR(lFLO1M@{zQz3S z+g|N7Qsk+YUI*GvsGt3=?*BRSvoM@GL@?j;<*&ZMtE|IOrLbjgU^>H|RncH^O3Gg+ zyhZ!vr2$o?!OlQYSN<;U;$dA#62fJ|YI)L9_qEKi{UxBncU;}P^C*vzi6vq?W8d5V zeR{4a!!Qg9{TP5X9Px1jI#hJ23c{6?86(Q={wmr9yh;WNKEcqCQOfwwQ5`jcI;mg0 z2iA2vpay72+lP&V$4AoaH;(oBA|X%ue-18BpSq)s02#q=O!|Muq7VEwxg+$?cn9*J zB)nEu3SFK-6_mZh#)rL*C1;gmsy!`G(=$Bw_aJg{@j zo1BPx`1Q)BFfgcicU(zk$y>kFVMw|YrN_~MVcJ4kCtGN{*=NIZohm{al;z%1FFylP z(O(H^0mw(|hNa@+w6-G@kH4_PhOxj0)GuW@Rjk+Vmkw2UAa)I@|6Me0B*lL2m|j`+ z-9E^zZ%6d$4_$DOB2MWVi%)cR5{3sL$Sfnbg4Z+W<)w_y67=)XS8T_Lm6;I;ErrF! z|JWu})8lfq5Vl*y;-8QcWwq`vB&|Q81qi*7XxrYI7F*T&p`C?>6x_BWhS9jp8kT6R4h`0-qr zm>8Y=^x|+cz!F?zIU+CL>&4~Dor}dtW~gV2&o$Vv{{IF#8(ZH$x z`rc|q%L?#i*<#xHv(l%uBPxIIsBCAq7%qmS1zrlu1kRXw`?Fk?Vtbm#<`q*|_b-BZ z7~(f{bT;If^_HgOfaPC2ekHwyJR{}9LJ?)8F7zpFT?AP)!<80~g4tKX6pz0V*kzFR z>4D=W%v!XF^_o=%L2*4#RrssCf3SeqpW8~^zYZ*_o`XSV{{)Ma`v)D1SUBW4Opa3b zVaN0&tm}bV#2H2TS7MAGs@5clNMH!a!>dR7ITVo%{{`&8hPw?De8I1x-bm=C;W_j# z{|^@c2C)cbkdKWI*o)Dii$c{SL;y~>DC zwm8G5tK@$&CB9djimc@FoZ?IMFmbtW82RvV^vo0Tkm6ryhRy2s4ymiYI^FiMnJ;{u zzb*sztZ8NVbN=jeE18natd|`f+(epeK!)ho`M_I-;X8XEW<5Vkr!jRa=M7jpUbl~0 z>KEo*2I01)XCfK1?mI1GyFDL}Sr^$V@zcSUA>;VuRNsOMeO(}-$K-cEA@*OxGG~q$ zU7c$Hx(ZsAwpCtc=zSC-z91SR+N~~q5Z~>YO=dN>a09Tmr)-PW#*ZSIc+WTFw1EYi ztPk8q8Xpev|9PP=%^YbJ0=C}RWVKzeV=QaGdH!ED2&5pWZaS=2yJN47UGX-#lxqWm zsFLK2*f)v$J4e~)A0uga<(2oGGeTVVWA3=|x#UVC{4OAC0&OZr34;p$Zjxwd`2_YX zMldRrGJdflHDCO!3Rp*s_eo!lrOmO{fIrE;;1J@N-&us&G5rEzcyAG$`s8m zUf^l33u&tLm!=`>Quc@_L5Bahi7VH$*KwQU@Evh9C{L!P4=>+SzhOUJ5wv;De!4Vs zUTfx!0z^9~xH@swsz;b{wZa&T`EZ4AO>SFo^$E-PE)MdXa)0l=_AS$@%a8~@r@zb} z1hU3Z1ULgZQ{Q*N{rS=QFCNr@&vVG}(cIOwE!p$CnTG$(!{VE@+bmmhTuBh`0?791 zezfAQtUYYw_C~3y(WfY@N0>;@#d$@>h~$J|<2p`10D{2Pz3;rnc0VH?eZTSK$BqtR zvmP#DJ1K%rhaQ@=$Tn`q>tE$mNkkBJi(j1+)Keot)UxhnyHl>KY0@Ho904pLyM?vZ$ zHUl`cx?wO^x#c*0=$`71Qx5JjMI2(?zkGTtHFKWtn5wg2TUh8A0mE&;LE>=kC2zUDj&nN5xG zG#z54f$hV>-%xcDIgEX%F0p2x{?Qh(#izC*(IH-jsP7xRTAx5?C>E=h>+`bLG4IHeHh+*KmRc34RJF08)+fc61>6L_+OA&g= z+Cav|Brh&zJ9|ZcW|L(hEFwfK?-Xwb!voeKY8$OIDX80Rgc5g$FeWW!AorH;vV)E0 z3f{BkvbMur?qo8m{{A>B?vvdb*XFB7@5HVVze+cP8c%xqn)6z*Vbc0O0`Vw!9n1I? zH`EdHJCw7$i-D5(Yy~paD^YAU&FN#g;LfnJPLRy=lS|mv!+a+>bYV*49$&T3rNbU= za)n%Ct7|S(s?d3buxCi^Da&}`1_~Xbwme+CV){Ij%IBQ`;^Y`DXQAF}I_H^Hwqg2DNH=n`c*-x~Q(g36#o}l}7x(?R>5w&%U{8FKj^gf3%O1DC1253L zS>4Z^S5Vq2^(qT~=>MVZmvu!{7?dE zZeBsyS<{xkemDO|x)T9P0U4rR)_e}7xy=fg_r<VyKUSecoW!PBISsJa`tcH2F%eu_hb)j!@KZBc+`bc!F z{4_olJab3{7uKG-nD-@D&e%?`^3lh2sCV&l71c!rcEySjM6 zP#mQ?zvIB*0<*f1@s^!%z4x*22bdKdBD4H)UQ{ZLOWUqzmc89iv}k0m+14#X1}s8( z+xDjrQAm30K$Iyy2|a2xEPkf`jTpe`;!mph{cAB+F)=0$3YeCtj2=i40*UFxken|_ zfE_Dlz!K@2_bPXR3YaSpderQXC(8tXZ>4V?EZZOSEy+kMao(4KZPI?*dWyhZAyyT2KBLvJGB3^D~ zqL<08ANxU%ekE||8~+jf@`?=ruZ?Svpu*<37!;|H?1i*rDdJ6p4Hk($dHu>mA zT{?>OfLg_dE)DlJ#jX8oird`~M&mil*jNNgyJGf%ZD$wGGZnE#I1niOXtP4mpuFX3 zx^q;rw3S!Y?oJG+N|CU{KODgllXCCR{+;P2^#Rd>_z>qU^ya@}W$S@>r04NAAKM5$ z>0NCbj|WX#7TAZBVbl|vaxE^kwd>feInvtS;(hW*781hYZwL z8FFLUYHsE*NYB2=T?Xq)t-SnqRAJ5?Z-<-*5o`C!tOz{WLns3Y9h3z1{Zuf{5AJ<> zPN1XXcDmn-u19)ObFd(F5}pYOnbMvP13><&!72aEQwZI z5K#_EiWh8hIZl^Ej=@I2YfI{5XI!DH3s{~7&UYd#CSf`$ago@MGHr@ zm*-CC_VGYQ5enm1!7*PXV9&LK>&rk;KRs{ zJKn(_b+litv}?Kw0-1TkZeG=0#xJ-$)hTgM0|xU>d;72??|b$r3(B?#%M70sA}WrS zc+aaU7vKj?JrUJ>?uAbci8(4ct9KHBoGNyl(;y}LqG)q|!*7Bn=KZ5e8XE$3O+LBV z53K&{1_SjZfMA5vRHyZCC%~54yN)G|+m;3r^w_MdQubK&$>4-M_E>5(la?NB5F5y{%9q5Wy8V9_Ng&TZk6iWtKFQT!g)Tvh^dqr?dXLfJ3*bi!l=eq)?NV zyChjr=sADzg@`-(=`OTcmvG6+;nUu*`H@tIZQU3wbVXNv}c=%$DW z-V(iC&-f`|m^v8N1;dlcbwS?h^h1YyVCF01M?hh}35||uCd%CqA*!0`!lQ!lNxW{b z29SyV*iWtZT|J~F)0Pdoao+o@juSBVJ(TslpsFZ@V0sh)A)?e(%p^QDytJ zAop8j6iJEbN2iB-8nm51ZZ-7A6Q^OPuV(#N&v-@r$L)ucO=EbVii$(`{W1H~xjr{6K^86??&YT$?cDcRO{aVVc-T9E!h zpX28IRV!IoTO6Ic_=+=L;U>xKN%uD1fOZ{4YQG&R6B+H(}AC!)*r6X0Ps3NR3Da3~>7tD)JC+US*i%TDGApE(pD zXERqrfRt-@scUiy-!ttuz1`-&PK=`hp>=7=93-APDmlb)6yEi4_3xZlDPt$n%?WHH z$>waxTQ~BG1%%$av^lx)Q!e};IDR4yTdW&_I5L+#8{V8OPk5Kfwn3t zeZjW)B;{gWH~AS2aESil6c0|f-3`hZ3qb$?5?nT>d|BsU(v(Vx&KJ@A%;RmnnCaZwyaBvrA0t6(;wo60CV#_Q?qmy^fg$B9R-6^pcJAo{bI$8f@YLc#AM=%5 zhuZI>i|$J0k@~aZ4}oW$o&C2u|C^PC!5+SB$!_x#y9rbbzIf zM(}<1ra4%($4H9kG%6XbHNEQSa6D_Dp(nqOn<>Aq;uAUpY4*5S~E+-5V z%`#BhAXUyT(L#X7QVO^k0HreSulh3GJPW~;wP~UL)9mYUd^Yu@{NcpFoa{U<0;>8p z*$^;erUBL-!nQkP5ch{Ahfn@aS={ad*7n-G#~v`^X~Qmc%Yx=BJK$2DnJow#ur(P) zHi*|A(lOU^dJK3hfM`NLFYU9xO?(F^U4(SUZF8=b$H?krOCuwn;n-?d_k3S)AywAO zDx*J#YDH`(da+I5h-dPw3r+)=`di6~<85{g(_Ubwl8kgpMx}OZakcaUJc~yWxE|Lu z?Cj}ol_Gg9NCUVLVgy#e)`=ctg<_B{)>7OmEe36-gl?a5$b$Pj>(>{*9fj15s@?>8 zv-wZ%dzUq^<0XN|)#<#2+Sfw|;wyfJe+l;V4&n+#F2OG!z~&;i)i9nYpMIn?z{-#%;HdFKdymBPLGIo`U_A}0lDWK0C+)hPQc*`1H7f*K+ZI(22qXp zkiGRrMyBM17C5BYC%T4Txicc*-y0d#=#`QAgPN)yS^b{<=Ni^XnZVe;cro5ks==3n z6RQ7((8HeM34&UAQzC)>0(QC9S2xzU4f8Ai%r0&B9uF|xF~@1IEG#_`)(9W^Q1&CE zn}@OYnZQ6J375TJMKHLM+6{ob-8MTpL~nBRcD^=e)HT7Myr?qR0m=TjjU`B;qpK(H zDQQdcf-lfcT(Y|0vc6|C(5&u0K#Q~lKGz5$@h+_HUe=LLU88R&JWbEZ`@gmX{F`o) zYzb*M(*eCw1=u%dK{rbZ^)pGLrQdc|Ln|@h!ML13UeP z3CGq0N15ZqV&H} z(l{_si?KtVcMkd3F-<(%*=i9Q`dovrP5?8RtqEQgWY&WvD}{pdsLp_)B9aKW7VSPB zD?7^NiHU^zTG-`uavHcaA`P&^<0JdZ;H8wx<3KO>*>r;p+~+Al#$l;fsLjfIV8SyI zynjBb&&l-^5E~^Yyus<>*hGC(b-(B1|3j)veXRy5Ic>cmNO?5}IdlviT22Svtn$-x zhu_C(g}@Q@6fAoM$;gNn)M2PHt~MGoj;*g+p5a8=L% z@`oRqtUMNUwt`gXw4Tf1e;@3c09D^4W9Zk;7-&J7$$+VS&d!?8|DW+go(L(QD{bl9 zR(`2RAB)l#7AlcZJjBi?(JKBYujCF(19c$|`M9^4@8s{_;U|@VRt={>$X^q^qP~o0 z5&$^N$T0p;#z49vKADQ#jgyeXT{5EK-T0{q4M;P8mVfQJ`dl%z%nO@?VZ#vPsIhDyd^dFURErG| zKK2EyJOuk`AGncT4owMkH+f;qz;G1%^*I-I+hy#@H;4;=8S4Z79th85Cw=k&XIw7OtHM_dhil=w zT64zK{+)5eTpjD|QLuv?GI@l=Lka`awSocp>+~k#5ASj5X}E|JVX#BClZUuwS+(cN z**4lY*%e+=>a*?JgcE<%f+#zAqL(KhY!9e`bz%?A@G}N4Pw}!Z0U(D{90@X~@=vbf zyl3no)TtJ&ifcp5Az+-8k0~=MqsEvCX9hH3Vzq&J6JQ|GlXunzUT@lR6pRZ7W)#~G zR8F8;z8FgtYmqH=ogpB-UV0~wq8fDm2GQ5;eo%3laDy?kKLx`v0KzK2TKiwC&6HEv z0-T$&QRvLLV^OLW)W!B486sEF>!jl2(hGaCY<>bqx#qk+LIm3txmO9FwEWLh8&lM_8i%^=foJ9wipR{^@9|A;NUbntj?=GIx>s3zJ(A(P z+1NAQ1N`H3f5CSipD|w2LztjNv>*OnHKme2_>fo4spKpxmzgCYX$xR7J)PifG5|nV zNZX|c1};jcdPt0JpE3F5aa8qB;eUz{Ww3JB`>U#Lyrg`Mf&`dE64@Jc5vc(tQUYRw zy`ykuGJ2&1z?B-<>8JaZh)FPXVXrn~NJCP*o%h&nX{9|`yZqCErP)ABx5?)K)@MEt zfr|2cNuPbxgAy2PDe`0W71y}=vF&zF>t*1UZt>f`Yjvj?Od96JfLhRij0SAp;WG_L zhVavu7>*`{=w5bJME!VG&(2jw9Kcm|F_wvmCc z#}}7_(|QmavcQJPHxpTUtbnITmr#fsZOgXvI z3V*Ne-l_oeAJ0~HG~*w7udr282=;o>v8WjAwGQl+)GO0?6l11|m4L+^|2YsYdJ3>v z%?W;r0Msr6hPX)##3H0P4q2_hnZelcl)k#=1d7MOnX3#!Z}IepaOP9=%JK0HPpj#Z zL``++Mj>t385M9M)-dpubr>AqztkJVT-XTkY)bcP&Ep9qBmNdu17M#AxVl;l+UKV% zwe$AaP*RcB1)}Mr(gwZb@kt_(h_2)PKN=q@`+wCSf^Lz=y+Mc8Y~1x;ACE9R|77=M z60m~CK5AqTPOLC+VuV~_UXM*hC_n70oC|#C_&G6HzEfd)8rTa^&Ev0NQTr<0jZU^* zJz)q@ezo%UDD=nfYNlTCQ%mBgE?Io4A_B^r{+HRN|5e>Df#JruPvK3a&a3dt_}rof zIV_%tI*!j#<_NYmY0v`QO9`rTg1`%$o|Qh(&zV}#GDL%>rX0{1#sjJVhIv`NAEa$y zOQ5?Bq%zhr!8!9_{T%SS)E5H(6tN+9d;Vbdz17gR3(XqrI2qTr^wSaC0+26xh*h_! zlX4giCpHo_+l`ryXR*7FoC@~sslj{vCd3YP0UJG1kGJcN=RCT4EGlu?2Uw7a7UzJ^n^6`^Tge?D4z&N0&&_0RYX$D<=r@UU|)l%`R_vh{KBD zK~6dhyCMFlg*2QN7yD&biKDyih7Y+8B9gw8Z{C!Zg=whja;OF z9QfO0&&jS>YN2_ z`;UT`sY%I_r3xFGRpyMUqGMv)e1FKg>;`ZVpS{h2J!{(~c-eKO`JXf0Z^nz&024{~ zbcy6RrGnOg_2AArI@RVr!WLn+b1*1Z{_r8f-m@-&_5s?eNSRozDHjkECVV5(_*jqYLo5 zq93`&HtBe2)UCafXXGnydVqu7+@_&o^4ZkE_3cH+wPjtN-m{1;~k7kh9jBg+2go{bxc_GK;g1c*5wjC^o`Ym(hZnWJdAZ52!FoGS%nD# zp?JX3@ec-}v^6`EO;W|X?F`WB@Z+qfyC8wS&3?hVpSETtk3tR)S))88&;;<=r8y70 z1JI%LusXiKGlyM#=j+*>fuIi-7XVxghmiF#WL{NY>dB5v=LL~x9NggsWWf>s19~Mw zFMs&khQJE~O#%B5fWPCgz*&Y5v3q2se?ucDTY`W|Row?t$=oE1kRnK$0t^ulXhv9| zLx2;EzbQE(?s<{f2f9JS1xe{1z^7yWf4&gesIIziEWI&<~=KVIhk*oVxCZVh#UR(9>Uot}4os4DX!$Lm4}(0K^k}N2+_v0 zHhbJkh4v44Tzd z>0N*J6J1_|c1)rcTmp5h;8OuCm$N!}Maxed(Bm}HsQsj)_5yo+iN?9RV+kv zzc@TBAx5U~#Vq=^94mdT^~+t9;g(IPRfV+6R94M&*4`)?7{vMZKtl1;YwtSEDb1bh z8t?d)Y#_+!&_yC-YsUTy41#WOK+wK_p#R$s7JD;QoUg=AQ@}4jfv_j7O-fIA{`-Fb D6XIx) literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart_full.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart_full.imageset/Contents.json" new file mode 100644 index 0000000..6d330ee --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart_full.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_home_heart_full.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart_full.imageset/ic_home_heart_full.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart_full.imageset/ic_home_heart_full.png" new file mode 100644 index 0000000000000000000000000000000000000000..64f702def40a1d37cbe1672bbf8e3a7f6abbf0fb GIT binary patch literal 11641 zcmds7g+ zK34d(67+Qoe*NY559<0p_zB(T^aTF?+vlpK9{?Pm7=MU(DGpxv@C5pb1=_^h1s!

  • L=5u$ajQNDe3jX3S?V=ZXwUe(Z6y{Z9!J;2yT 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 delta 16132 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 /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 95DF7FD6867161A9BE8EB2CA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-NewsApp-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + D49A38DAC19942CB5F20058F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + EBDE856FB1853BBBF0854759 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-NewsAppTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + FA7F449B2DF1CFD3F8DF7489 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ BC2A550928223D5C008D3505 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BC833455282C9AE9002D517F /* ArticleModel.swift in Sources */, + BC833450282C9814002D517F /* Article.swift in Sources */, BC0CD4482822EA5C001DF83F /* DetailVC.swift in Sources */, BC2A551528223D5C008D3505 /* ViewController.swift in Sources */, + BC833453282C9A9D002D517F /* ArticleService.swift in Sources */, BC2A551128223D5C008D3505 /* AppDelegate.swift in Sources */, BC0CD4432822C17C001DF83F /* NewsTableViewCell.swift in Sources */, BC2A551328223D5C008D3505 /* SceneDelegate.swift in Sources */, @@ -446,6 +614,7 @@ }; BC2A553828223D5C008D3505 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 32B9F94C1F3399C59EA8F39B /* Pods-NewsApp.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -474,6 +643,7 @@ }; BC2A553928223D5C008D3505 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 5068BA3B097E93093736AA87 /* Pods-NewsApp.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -502,6 +672,7 @@ }; BC2A553B28223D5C008D3505 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A4CA5093C1D3C070E61369EC /* Pods-NewsAppTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -522,6 +693,7 @@ }; BC2A553C28223D5C008D3505 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = AB3CCA52E4B2A07663210FBB /* Pods-NewsAppTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -542,6 +714,7 @@ }; BC2A553E28223D5C008D3505 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = E08E52DC16A249E16C8E3638 /* Pods-NewsApp-NewsAppUITests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; @@ -560,6 +733,7 @@ }; BC2A553F28223D5C008D3505 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 33FE2B5C090575007E13AC9B /* Pods-NewsApp-NewsAppUITests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; diff --git a/jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate index 15b14dd8beb819a404241dc0cbd7a526a0d9b1cb..93e415f08c6207016275c4aea7c0947a121cdb9c 100644 GIT binary patch literal 47596 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 xXZ&S6HeM#{BvXNewsApp.xcscheme_^#shared#^_ orderHint - 0 + 5 diff --git a/jaem/week6/NewsApp/NewsApp.xcworkspace/contents.xcworkspacedata b/jaem/week6/NewsApp/NewsApp.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..e82daea --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/jaem/week6/NewsApp/NewsApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/jaem/week6/NewsApp/NewsApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/jaem/week6/NewsApp/NewsApp.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week6/NewsApp/NewsApp.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..040e63f310b312fc84241433ce5ac40ef967d6d9 GIT binary patch literal 41442 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 literal 0 HcmV?d00001 diff --git a/jaem/week6/NewsApp/NewsApp.xcworkspace/xcuserdata/songjaemin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/jaem/week6/NewsApp/NewsApp.xcworkspace/xcuserdata/songjaemin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..8e8d59d --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp.xcworkspace/xcuserdata/songjaemin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/jaem/week6/NewsApp/NewsApp/ArticleModel/Article.swift b/jaem/week6/NewsApp/NewsApp/ArticleModel/Article.swift index 58197d7..f86b78e 100644 --- a/jaem/week6/NewsApp/NewsApp/ArticleModel/Article.swift +++ b/jaem/week6/NewsApp/NewsApp/ArticleModel/Article.swift @@ -9,6 +9,8 @@ import Foundation struct Article:Codable{ let title: String? - let content: String? - let imgUrl: String? + let description: String? + let urlToImage: String? + let author: String? + let publishedAt: String? } diff --git a/jaem/week6/NewsApp/NewsApp/Base.lproj/Main.storyboard b/jaem/week6/NewsApp/NewsApp/Base.lproj/Main.storyboard index 34de09e..728e3fa 100644 --- a/jaem/week6/NewsApp/NewsApp/Base.lproj/Main.storyboard +++ b/jaem/week6/NewsApp/NewsApp/Base.lproj/Main.storyboard @@ -20,26 +20,9 @@ - - - - - - - - - - - - - + @@ -78,37 +61,55 @@ + + + - + - + + + + - + + + diff --git a/jaem/week6/NewsApp/NewsApp/DetailVC.swift b/jaem/week6/NewsApp/NewsApp/DetailVC.swift index 4858e9b..ee3b4e1 100644 --- a/jaem/week6/NewsApp/NewsApp/DetailVC.swift +++ b/jaem/week6/NewsApp/NewsApp/DetailVC.swift @@ -11,26 +11,27 @@ class DetailVC : UIViewController{ var newsTitle: String = "" var newsContent: String = "" var imageUrl: String = "" + var newsDate: String = "" + var author: String = "" @IBOutlet weak var titleLbl: UILabel! @IBOutlet weak var newsImg: UIImageView! @IBOutlet weak var contentLbl: UILabel! - + @IBOutlet weak var dateLbl: UILabel! + @IBOutlet weak var authorLbl: UILabel! override func viewDidLoad() { super.viewDidLoad() self.navigationItem.title = "Detail" + self.navigationController?.navigationBar.prefersLargeTitles = false + + let endIdx:String.Index = newsDate.index(newsDate.startIndex, offsetBy: 9) titleLbl.text = newsTitle contentLbl.text = newsContent let url = URL(string: imageUrl) - DispatchQueue.global().async { - if let data = try? Data(contentsOf: url!){ - DispatchQueue.main.async { - self.newsImg.image = UIImage(data: data) - } - } - - } + self.newsImg.kf.setImage(with: url) + dateLbl.text = String(newsDate[...endIdx]) + authorLbl.text = author } } diff --git a/jaem/week6/NewsApp/NewsApp/Info.plist b/jaem/week6/NewsApp/NewsApp/Info.plist index dd3c9af..c0ae0b4 100644 --- a/jaem/week6/NewsApp/NewsApp/Info.plist +++ b/jaem/week6/NewsApp/NewsApp/Info.plist @@ -2,6 +2,11 @@ + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.swift b/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.swift index d482663..dd58070 100644 --- a/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.swift +++ b/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.swift @@ -6,17 +6,58 @@ // import UIKit +import Kingfisher class NewsTableViewCell: UITableViewCell { - + var likeClickedAction :(()->())? @IBOutlet weak var titleLbl: UILabel! @IBOutlet weak var contentLbl: UILabel! @IBOutlet weak var newsImg: UIImageView! + @IBOutlet weak var heartBtn: UIButton! + + var articleToDisplay:Article? + var heartSelected:Bool = false + + func displayArticle(article:Article){ + articleToDisplay = article + + titleLbl.text = articleToDisplay!.title + + contentLbl.text = articleToDisplay!.description + + guard articleToDisplay!.urlToImage != nil else{ + return + } + + let urlString = articleToDisplay!.urlToImage! + let url = URL(string: urlString) + + guard url != nil else{ + print("coudn't create url object") + return + } + + let session = URLSession.shared + + let dataTask = session.dataTask(with: url!){ + (data, response, error) in + if(error == nil && data != nil){ + self.newsImg.kf.setImage(with: url) + } + } + dataTask.resume() + } + + @objc func likeClicked(){ + likeClickedAction?() + } override func awakeFromNib() { super.awakeFromNib() // Initialization code + + self.heartBtn.addTarget(self, action: #selector(likeClicked), for: .touchUpInside) } override func setSelected(_ selected: Bool, animated: Bool) { diff --git a/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.xib b/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.xib index 604e79a..ce9450a 100644 --- a/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.xib +++ b/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.xib @@ -36,14 +36,25 @@ + + + @@ -51,6 +62,7 @@ + @@ -58,6 +70,7 @@ + diff --git a/jaem/week6/NewsApp/NewsApp/ViewController.swift b/jaem/week6/NewsApp/NewsApp/ViewController.swift index 767738f..b6318a0 100644 --- a/jaem/week6/NewsApp/NewsApp/ViewController.swift +++ b/jaem/week6/NewsApp/NewsApp/ViewController.swift @@ -8,11 +8,15 @@ import UIKit class ViewController: UIViewController { - + + var articles = [Article]() + var model = ArticleModel() + @IBOutlet var tableView: UITableView! var newsData :Array>? /*뉴스 데이터 가져오기*/ + /* func getNews(){ let task = URLSession.shared.dataTask(with: URL(string: "https://newsapi.org/v2/top-headlines?country=kr&apiKey=f2a79448bc5142e192ada2c486b2f162")!) { (data, response, error) in if let dataJson = data{ @@ -34,92 +38,159 @@ class ViewController: UIViewController { task.resume() } - + */ override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. + model.delegate = self + model.getArticles() + tableView.delegate = self tableView.dataSource = self let newsNib = UINib(nibName: "NewsTableViewCell", bundle: nil) tableView.register(newsNib, forCellReuseIdentifier: "NewsTableViewCell") self.navigationItem.title = "News" + self.navigationController?.navigationBar.prefersLargeTitles = true - getNews() + //getNews() } + + } +extension ViewController : ArticleModelProtocol{ + + func articleRetrieved(articles: [Article]){ + print("news retrieved from news model") + self.articles = articles + tableView.reloadData() + } +} + extension ViewController : UITableViewDelegate, UITableViewDataSource{ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + /* if let news = newsData{ return news.count }else{ return 0 - } + }*/ + return self.articles.count } - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "NewsTableViewCell", for: indexPath) as? NewsTableViewCell else{ + return UITableViewCell() + } - let storyboard = UIStoryboard.init(name: "Main", bundle: nil) - let newsDetail = storyboard.instantiateViewController(withIdentifier: "DetailVC") as! DetailVC + let article = self.articles[indexPath.row] + + cell.displayArticle(article: article) + cell.likeClickedAction = { + cell.heartSelected + ? cell.heartBtn.setImage(UIImage(systemName: "heart"), for: .normal) + : cell.heartBtn.setImage(UIImage(systemName: "heart.fill"), for: .normal) + + cell.heartSelected = !cell.heartSelected + } + + /* + cell.likeClickedAction = { + if(cell.heartSelected == false){ + cell.heartBtn.setImage(UIImage(systemName: "heart_fill"), for: .normal) + cell.heartSelected = true + }else{ + cell.heartBtn.setImage(UIImage(systemName: "heart"), for: .normal) + cell.heartSelected = true + } + }*/ + + + //let idx = indexPath.row + + /* if let news = newsData{ - let row = news[indexPath.row] + + let row = news[idx] if let r = row as? Dictionary{ if let title = r["title"] as? String{ - newsDetail.newsTitle = title + cell.titleLbl.text = title } if let content = r["description"] as? String{ - newsDetail.newsContent = content + cell.contentLbl.text = content } if let imageUrl = r["urlToImage"] as? String{ - newsDetail.imageUrl = imageUrl + let url = URL(string: imageUrl) + cell.newsImg.kf.setImage(with: url) } } } - - //showDetailViewController(newsDetail, sender: nil) - self.navigationController?.pushViewController(newsDetail, animated: true) + */ + return cell } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: "NewsTableViewCell", for: indexPath) as? NewsTableViewCell else{ - return UITableViewCell() - } + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let idx = indexPath.row + let storyboard = UIStoryboard.init(name: "Main", bundle: nil) + let newsDetail = storyboard.instantiateViewController(withIdentifier: "DetailVC") as! DetailVC + /* if let news = newsData{ - - let row = news[idx] + let row = news[indexPath.row] if let r = row as? Dictionary{ if let title = r["title"] as? String{ - cell.titleLbl.text = title + newsDetail.newsTitle = title } if let content = r["description"] as? String{ - cell.contentLbl.text = content + newsDetail.newsContent = content } if let imageUrl = r["urlToImage"] as? String{ - let url = URL(string: imageUrl) - DispatchQueue.global().async { - if let data = try? Data(contentsOf: url!){ - DispatchQueue.main.async { - cell.newsImg.image = UIImage(data: data) - } - } - - } + newsDetail.imageUrl = imageUrl } } - } - return cell + }*/ + + newsDetail.newsTitle = articles[indexPath.row].title ?? "" + newsDetail.newsContent = articles[indexPath.row].description ?? "" + newsDetail.imageUrl = articles[indexPath.row].urlToImage ?? "" + newsDetail.author = articles[indexPath.row].author ?? "" + newsDetail.newsDate = articles[indexPath.row].publishedAt ?? "" + + //showDetailViewController(newsDetail, sender: nil) + self.navigationController?.pushViewController(newsDetail, animated: true) } + + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 176 } } + +#if canImport(SwiftUI) && (DEBUG) +import SwiftUI + +struct ViewControllerRepresentable: UIViewControllerRepresentable { + + let viewController: UIViewController + + func makeUIViewController(context: Context) -> UIViewController { + return viewController + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} + +struct ViewController_Preview: PreviewProvider { + + static var previews: some View { + ViewControllerRepresentable(viewController: UINavigationController(rootViewController: ViewController())) + } +} +#endif diff --git a/jaem/week6/NewsApp/Podfile b/jaem/week6/NewsApp/Podfile new file mode 100644 index 0000000..9daf851 --- /dev/null +++ b/jaem/week6/NewsApp/Podfile @@ -0,0 +1,21 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'NewsApp' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for NewsApp + pod 'Alamofire' + pod 'Kingfisher', '~> 7.0' + + target 'NewsAppTests' do + inherit! :search_paths + # Pods for testing + end + + target 'NewsAppUITests' do + # Pods for testing + end + +end diff --git a/jaem/week6/NewsApp/Podfile.lock b/jaem/week6/NewsApp/Podfile.lock new file mode 100644 index 0000000..ee7e733 --- /dev/null +++ b/jaem/week6/NewsApp/Podfile.lock @@ -0,0 +1,20 @@ +PODS: + - Alamofire (5.6.1) + - Kingfisher (7.2.2) + +DEPENDENCIES: + - Alamofire + - Kingfisher (~> 7.0) + +SPEC REPOS: + trunk: + - Alamofire + - Kingfisher + +SPEC CHECKSUMS: + Alamofire: 87bd8c952f9a4454320fce00d9cc3de57bcadaf5 + Kingfisher: 184d4d1a8c36666e663caf8e08abe87898595c53 + +PODFILE CHECKSUM: eb5eeb66a7ef05a29db918fad840f9d4c14b9b25 + +COCOAPODS: 1.11.3 diff --git a/jaem/week6/NewsApp/Pods/Alamofire/LICENSE b/jaem/week6/NewsApp/Pods/Alamofire/LICENSE new file mode 100644 index 0000000..cae030a --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014-2022 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/jaem/week6/NewsApp/Pods/Alamofire/README.md b/jaem/week6/NewsApp/Pods/Alamofire/README.md new file mode 100644 index 0000000..b3fc817 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/README.md @@ -0,0 +1,227 @@ +![Alamofire: Elegant Networking in Swift](https://raw.githubusercontent.com/Alamofire/Alamofire/master/Resources/AlamofireLogo.png) + +[![Swift](https://img.shields.io/badge/Swift-5.3_5.4_5.5_5.6-orange?style=flat-square)](https://img.shields.io/badge/Swift-5.3_5.4_5.5_5.6-Orange?style=flat-square) +[![Platforms](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_Linux_Windows-yellowgreen?style=flat-square)](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_Linux_Windows-Green?style=flat-square) +[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/Alamofire.svg?style=flat-square)](https://img.shields.io/cocoapods/v/Alamofire.svg) +[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat-square)](https://github.com/Carthage/Carthage) +[![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square)](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square) +[![Twitter](https://img.shields.io/badge/twitter-@AlamofireSF-blue.svg?style=flat-square)](https://twitter.com/AlamofireSF) +[![Swift Forums](https://img.shields.io/badge/Swift_Forums-Alamofire-orange?style=flat-square)](https://forums.swift.org/c/related-projects/alamofire/37) + +Alamofire is an HTTP networking library written in Swift. + +- [Features](#features) +- [Component Libraries](#component-libraries) +- [Requirements](#requirements) +- [Migration Guides](#migration-guides) +- [Communication](#communication) +- [Installation](#installation) +- [Contributing](#contributing) +- [Usage](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#using-alamofire) + - [**Introduction -**](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#introduction) [Making Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#making-requests), [Response Handling](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-handling), [Response Validation](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-validation), [Response Caching](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-caching) + - **HTTP -** [HTTP Methods](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#http-methods), [Parameters and Parameter Encoder](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md##request-parameters-and-parameter-encoders), [HTTP Headers](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#http-headers), [Authentication](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#authentication) + - **Large Data -** [Downloading Data to a File](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#downloading-data-to-a-file), [Uploading Data to a Server](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#uploading-data-to-a-server) + - **Tools -** [Statistical Metrics](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#statistical-metrics), [cURL Command Output](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#curl-command-output) +- [Advanced Usage](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md) + - **URL Session -** [Session Manager](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#session), [Session Delegate](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#sessiondelegate), [Request](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#request) + - **Routing -** [Routing Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#routing-requests), [Adapting and Retrying Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#adapting-and-retrying-requests-with-requestinterceptor) + - **Model Objects -** [Custom Response Handlers](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#customizing-response-handlers) + - **Advanced Concurrency -** [Swift Concurrency](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#using-alamofire-with-swift-concurrency) and [Combine](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#using-alamofire-with-combine) + - **Connection -** [Security](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#security), [Network Reachability](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#network-reachability) +- [Open Radars](#open-radars) +- [FAQ](#faq) +- [Credits](#credits) +- [Donations](#donations) +- [License](#license) + +## Features + +- [x] Chainable Request / Response Methods +- [x] Swift Concurrency Support Back to iOS 13, macOS 10.15, tvOS 13, and watchOS 6. +- [x] Combine Support +- [x] URL / JSON Parameter Encoding +- [x] Upload File / Data / Stream / MultipartFormData +- [x] Download File using Request or Resume Data +- [x] Authentication with `URLCredential` +- [x] HTTP Response Validation +- [x] Upload and Download Progress Closures with Progress +- [x] cURL Command Output +- [x] Dynamically Adapt and Retry Requests +- [x] TLS Certificate and Public Key Pinning +- [x] Network Reachability +- [x] Comprehensive Unit and Integration Test Coverage +- [x] [Complete Documentation](https://alamofire.github.io/Alamofire) + +## Component Libraries + +In order to keep Alamofire focused specifically on core networking implementations, additional component libraries have been created by the [Alamofire Software Foundation](https://github.com/Alamofire/Foundation) to bring additional functionality to the Alamofire ecosystem. + +- [AlamofireImage](https://github.com/Alamofire/AlamofireImage) - An image library including image response serializers, `UIImage` and `UIImageView` extensions, custom image filters, an auto-purging in-memory cache, and a priority-based image downloading system. +- [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator) - Controls the visibility of the network activity indicator on iOS using Alamofire. It contains configurable delay timers to help mitigate flicker and can support `URLSession` instances not managed by Alamofire. + +## Requirements + +| Platform | Minimum Swift Version | Installation | Status | +| --- | --- | --- | --- | +| iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+ | 5.3 | [CocoaPods](#cocoapods), [Carthage](#carthage), [Swift Package Manager](#swift-package-manager), [Manual](#manually) | Fully Tested | +| Linux | Latest Only | [Swift Package Manager](#swift-package-manager) | Building But Unsupported | +| Windows | Latest Only | [Swift Package Manager](#swift-package-manager) | Building But Unsupported | + +#### Known Issues on Linux and Windows + +Alamofire builds on Linux and Windows but there are missing features and many issues in the underlying `swift-corelibs-foundation` that prevent full functionality and may cause crashes. These include: +- `ServerTrustManager` and associated certificate functionality is unavailable, so there is no certificate pinning and no client certificate support. +- Various methods of HTTP authentication may crash, including HTTP Basic and HTTP Digest. Crashes may occur if responses contain server challenges. +- Cache control through `CachedResponseHandler` and associated APIs is unavailable, as the underlying delegate methods aren't called. +- `URLSessionTaskMetrics` are never gathered. + +Due to these issues, Alamofire is unsupported on Linux and Windows. Please report any crashes to the [Swift bug reporter](https://bugs.swift.org). + +## Migration Guides + +- [Alamofire 5.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%205.0%20Migration%20Guide.md) +- [Alamofire 4.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%204.0%20Migration%20Guide.md) +- [Alamofire 3.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%203.0%20Migration%20Guide.md) +- [Alamofire 2.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%202.0%20Migration%20Guide.md) + +## Communication +- If you **need help with making network requests** using Alamofire, use [Stack Overflow](https://stackoverflow.com/questions/tagged/alamofire) and tag `alamofire`. +- If you need to **find or understand an API**, check [our documentation](http://alamofire.github.io/Alamofire/) or [Apple's documentation for `URLSession`](https://developer.apple.com/documentation/foundation/url_loading_system), on top of which Alamofire is built. +- If you need **help with an Alamofire feature**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire). +- If you'd like to **discuss Alamofire best practices**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire). +- If you'd like to **discuss a feature request**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire). +- If you **found a bug**, open an issue here on GitHub and follow the guide. The more detail the better! + +## Installation + +### CocoaPods + +[CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate Alamofire into your Xcode project using CocoaPods, specify it in your `Podfile`: + +```ruby +pod 'Alamofire' +``` + +### Carthage + +[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate Alamofire into your Xcode project using Carthage, specify it in your `Cartfile`: + +```ogdl +github "Alamofire/Alamofire" +``` + +### Swift Package Manager + +The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. + +Once you have your Swift package set up, adding Alamofire as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. + +```swift +dependencies: [ + .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.6.1")) +] +``` + +### Manually + +If you prefer not to use any of the aforementioned dependency managers, you can integrate Alamofire into your project manually. + +#### Embedded Framework + +- Open up Terminal, `cd` into your top-level project directory, and run the following command "if" your project is not initialized as a git repository: + + ```bash + $ git init + ``` + +- Add Alamofire as a git [submodule](https://git-scm.com/docs/git-submodule) by running the following command: + + ```bash + $ git submodule add https://github.com/Alamofire/Alamofire.git + ``` + +- Open the new `Alamofire` folder, and drag the `Alamofire.xcodeproj` into the Project Navigator of your application's Xcode project. + + > It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter. + +- Select the `Alamofire.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target. +- Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the "Targets" heading in the sidebar. +- In the tab bar at the top of that window, open the "General" panel. +- Click on the `+` button under the "Embedded Binaries" section. +- You will see two different `Alamofire.xcodeproj` folders each with two different versions of the `Alamofire.framework` nested inside a `Products` folder. + + > It does not matter which `Products` folder you choose from, but it does matter whether you choose the top or bottom `Alamofire.framework`. + +- Select the top `Alamofire.framework` for iOS and the bottom one for macOS. + + > You can verify which one you selected by inspecting the build log for your project. The build target for `Alamofire` will be listed as `Alamofire iOS`, `Alamofire macOS`, `Alamofire tvOS`, or `Alamofire watchOS`. + +- And that's it! + + > The `Alamofire.framework` is automagically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device. + +## Contributing + +Before contributing to Alamofire, please read the instructions detailed in our [contribution guide](https://github.com/Alamofire/Alamofire/blob/master/CONTRIBUTING.md). + +## Open Radars + +The following radars have some effect on the current implementation of Alamofire. + +- [`rdar://21349340`](http://www.openradar.me/radar?id=5517037090635776) - Compiler throwing warning due to toll-free bridging issue in the test case +- `rdar://26870455` - Background URL Session Configurations do not work in the simulator +- `rdar://26849668` - Some URLProtocol APIs do not properly handle `URLRequest` + +## Resolved Radars + +The following radars have been resolved over time after being filed against the Alamofire project. + +- [`rdar://26761490`](http://www.openradar.me/radar?id=5010235949318144) - Swift string interpolation causing memory leak with common usage. + - (Resolved): 9/1/17 in Xcode 9 beta 6. +- [`rdar://36082113`](http://openradar.appspot.com/radar?id=4942308441063424) - `URLSessionTaskMetrics` failing to link on watchOS 3.0+ + - (Resolved): Just add `CFNetwork` to your linked frameworks. +- `FB7624529` - `urlSession(_:task:didFinishCollecting:)` never called on watchOS + - (Resolved): Metrics now collected on watchOS 7+. + +## FAQ + +### What's the origin of the name Alamofire? + +Alamofire is named after the [Alamo Fire flower](https://aggie-horticulture.tamu.edu/wildseed/alamofire.html), a hybrid variant of the Bluebonnet, the official state flower of Texas. + +## Credits + +Alamofire is owned and maintained by the [Alamofire Software Foundation](http://alamofire.org). You can follow them on Twitter at [@AlamofireSF](https://twitter.com/AlamofireSF) for project updates and releases. + +### Security Disclosure + +If you believe you have identified a security vulnerability with Alamofire, you should report it as soon as possible via email to security@alamofire.org. Please do not post it to a public issue tracker. + +## Sponsorship + +The [ASF](https://github.com/Alamofire/Foundation#members) is looking to raise money to officially stay registered as a federal non-profit organization. +Registering will allow Foundation members to gain some legal protections and also allow us to put donations to use, tax-free. +Sponsoring the ASF will enable us to: + +- Pay our yearly legal fees to keep the non-profit in good status +- Pay for our mail servers to help us stay on top of all questions and security issues +- Potentially fund test servers to make it easier for us to test the edge cases +- Potentially fund developers to work on one of our projects full-time + +The community adoption of the ASF libraries has been amazing. +We are greatly humbled by your enthusiasm around the projects and want to continue to do everything we can to move the needle forward. +With your continued support, the ASF will be able to improve its reach and also provide better legal safety for the core members. +If you use any of our libraries for work, see if your employers would be interested in donating. +Any amount you can donate, whether once or monthly, to help us reach our goal would be greatly appreciated. + +[Sponsor Alamofire](https://github.com/sponsors/Alamofire) + +## Supporters + +[MacStadium](https://macstadium.com) provides Alamofire with a free, hosted Mac mini. + +![Powered by MacStadium](https://raw.githubusercontent.com/Alamofire/Alamofire/master/Resources/MacStadiumLogo.png) + +## License + +Alamofire is released under the MIT license. [See LICENSE](https://github.com/Alamofire/Alamofire/blob/master/LICENSE) for details. diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/AFError.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/AFError.swift new file mode 100644 index 0000000..8cd60c7 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/AFError.swift @@ -0,0 +1,870 @@ +// +// AFError.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// `AFError` is the error type returned by Alamofire. It encompasses a few different types of errors, each with +/// their own associated reasons. +public enum AFError: Error { + /// The underlying reason the `.multipartEncodingFailed` error occurred. + public enum MultipartEncodingFailureReason { + /// The `fileURL` provided for reading an encodable body part isn't a file `URL`. + case bodyPartURLInvalid(url: URL) + /// The filename of the `fileURL` provided has either an empty `lastPathComponent` or `pathExtension. + case bodyPartFilenameInvalid(in: URL) + /// The file at the `fileURL` provided was not reachable. + case bodyPartFileNotReachable(at: URL) + /// Attempting to check the reachability of the `fileURL` provided threw an error. + case bodyPartFileNotReachableWithError(atURL: URL, error: Error) + /// The file at the `fileURL` provided is actually a directory. + case bodyPartFileIsDirectory(at: URL) + /// The size of the file at the `fileURL` provided was not returned by the system. + case bodyPartFileSizeNotAvailable(at: URL) + /// The attempt to find the size of the file at the `fileURL` provided threw an error. + case bodyPartFileSizeQueryFailedWithError(forURL: URL, error: Error) + /// An `InputStream` could not be created for the provided `fileURL`. + case bodyPartInputStreamCreationFailed(for: URL) + /// An `OutputStream` could not be created when attempting to write the encoded data to disk. + case outputStreamCreationFailed(for: URL) + /// The encoded body data could not be written to disk because a file already exists at the provided `fileURL`. + case outputStreamFileAlreadyExists(at: URL) + /// The `fileURL` provided for writing the encoded body data to disk is not a file `URL`. + case outputStreamURLInvalid(url: URL) + /// The attempt to write the encoded body data to disk failed with an underlying error. + case outputStreamWriteFailed(error: Error) + /// The attempt to read an encoded body part `InputStream` failed with underlying system error. + case inputStreamReadFailed(error: Error) + } + + /// Represents unexpected input stream length that occur when encoding the `MultipartFormData`. Instances will be + /// embedded within an `AFError.multipartEncodingFailed` `.inputStreamReadFailed` case. + public struct UnexpectedInputStreamLength: Error { + /// The expected byte count to read. + public var bytesExpected: UInt64 + /// The actual byte count read. + public var bytesRead: UInt64 + } + + /// The underlying reason the `.parameterEncodingFailed` error occurred. + public enum ParameterEncodingFailureReason { + /// The `URLRequest` did not have a `URL` to encode. + case missingURL + /// JSON serialization failed with an underlying system error during the encoding process. + case jsonEncodingFailed(error: Error) + /// Custom parameter encoding failed due to the associated `Error`. + case customEncodingFailed(error: Error) + } + + /// The underlying reason the `.parameterEncoderFailed` error occurred. + public enum ParameterEncoderFailureReason { + /// Possible missing components. + public enum RequiredComponent { + /// The `URL` was missing or unable to be extracted from the passed `URLRequest` or during encoding. + case url + /// The `HTTPMethod` could not be extracted from the passed `URLRequest`. + case httpMethod(rawValue: String) + } + + /// A `RequiredComponent` was missing during encoding. + case missingRequiredComponent(RequiredComponent) + /// The underlying encoder failed with the associated error. + case encoderFailed(error: Error) + } + + /// The underlying reason the `.responseValidationFailed` error occurred. + public enum ResponseValidationFailureReason { + /// The data file containing the server response did not exist. + case dataFileNil + /// The data file containing the server response at the associated `URL` could not be read. + case dataFileReadFailed(at: URL) + /// The response did not contain a `Content-Type` and the `acceptableContentTypes` provided did not contain a + /// wildcard type. + case missingContentType(acceptableContentTypes: [String]) + /// The response `Content-Type` did not match any type in the provided `acceptableContentTypes`. + case unacceptableContentType(acceptableContentTypes: [String], responseContentType: String) + /// The response status code was not acceptable. + case unacceptableStatusCode(code: Int) + /// Custom response validation failed due to the associated `Error`. + case customValidationFailed(error: Error) + } + + /// The underlying reason the response serialization error occurred. + public enum ResponseSerializationFailureReason { + /// The server response contained no data or the data was zero length. + case inputDataNilOrZeroLength + /// The file containing the server response did not exist. + case inputFileNil + /// The file containing the server response could not be read from the associated `URL`. + case inputFileReadFailed(at: URL) + /// String serialization failed using the provided `String.Encoding`. + case stringSerializationFailed(encoding: String.Encoding) + /// JSON serialization failed with an underlying system error. + case jsonSerializationFailed(error: Error) + /// A `DataDecoder` failed to decode the response due to the associated `Error`. + case decodingFailed(error: Error) + /// A custom response serializer failed due to the associated `Error`. + case customSerializationFailed(error: Error) + /// Generic serialization failed for an empty response that wasn't type `Empty` but instead the associated type. + case invalidEmptyResponse(type: String) + } + + #if !(os(Linux) || os(Windows)) + /// Underlying reason a server trust evaluation error occurred. + public enum ServerTrustFailureReason { + /// The output of a server trust evaluation. + public struct Output { + /// The host for which the evaluation was performed. + public let host: String + /// The `SecTrust` value which was evaluated. + public let trust: SecTrust + /// The `OSStatus` of evaluation operation. + public let status: OSStatus + /// The result of the evaluation operation. + public let result: SecTrustResultType + + /// Creates an `Output` value from the provided values. + init(_ host: String, _ trust: SecTrust, _ status: OSStatus, _ result: SecTrustResultType) { + self.host = host + self.trust = trust + self.status = status + self.result = result + } + } + + /// No `ServerTrustEvaluator` was found for the associated host. + case noRequiredEvaluator(host: String) + /// No certificates were found with which to perform the trust evaluation. + case noCertificatesFound + /// No public keys were found with which to perform the trust evaluation. + case noPublicKeysFound + /// During evaluation, application of the associated `SecPolicy` failed. + case policyApplicationFailed(trust: SecTrust, policy: SecPolicy, status: OSStatus) + /// During evaluation, setting the associated anchor certificates failed. + case settingAnchorCertificatesFailed(status: OSStatus, certificates: [SecCertificate]) + /// During evaluation, creation of the revocation policy failed. + case revocationPolicyCreationFailed + /// `SecTrust` evaluation failed with the associated `Error`, if one was produced. + case trustEvaluationFailed(error: Error?) + /// Default evaluation failed with the associated `Output`. + case defaultEvaluationFailed(output: Output) + /// Host validation failed with the associated `Output`. + case hostValidationFailed(output: Output) + /// Revocation check failed with the associated `Output` and options. + case revocationCheckFailed(output: Output, options: RevocationTrustEvaluator.Options) + /// Certificate pinning failed. + case certificatePinningFailed(host: String, trust: SecTrust, pinnedCertificates: [SecCertificate], serverCertificates: [SecCertificate]) + /// Public key pinning failed. + case publicKeyPinningFailed(host: String, trust: SecTrust, pinnedKeys: [SecKey], serverKeys: [SecKey]) + /// Custom server trust evaluation failed due to the associated `Error`. + case customEvaluationFailed(error: Error) + } + #endif + + /// The underlying reason the `.urlRequestValidationFailed` + public enum URLRequestValidationFailureReason { + /// URLRequest with GET method had body data. + case bodyDataInGETRequest(Data) + } + + /// `UploadableConvertible` threw an error in `createUploadable()`. + case createUploadableFailed(error: Error) + /// `URLRequestConvertible` threw an error in `asURLRequest()`. + case createURLRequestFailed(error: Error) + /// `SessionDelegate` threw an error while attempting to move downloaded file to destination URL. + case downloadedFileMoveFailed(error: Error, source: URL, destination: URL) + /// `Request` was explicitly cancelled. + case explicitlyCancelled + /// `URLConvertible` type failed to create a valid `URL`. + case invalidURL(url: URLConvertible) + /// Multipart form encoding failed. + case multipartEncodingFailed(reason: MultipartEncodingFailureReason) + /// `ParameterEncoding` threw an error during the encoding process. + case parameterEncodingFailed(reason: ParameterEncodingFailureReason) + /// `ParameterEncoder` threw an error while running the encoder. + case parameterEncoderFailed(reason: ParameterEncoderFailureReason) + /// `RequestAdapter` threw an error during adaptation. + case requestAdaptationFailed(error: Error) + /// `RequestRetrier` threw an error during the request retry process. + case requestRetryFailed(retryError: Error, originalError: Error) + /// Response validation failed. + case responseValidationFailed(reason: ResponseValidationFailureReason) + /// Response serialization failed. + case responseSerializationFailed(reason: ResponseSerializationFailureReason) + #if !(os(Linux) || os(Windows)) + /// `ServerTrustEvaluating` instance threw an error during trust evaluation. + case serverTrustEvaluationFailed(reason: ServerTrustFailureReason) + #endif + /// `Session` which issued the `Request` was deinitialized, most likely because its reference went out of scope. + case sessionDeinitialized + /// `Session` was explicitly invalidated, possibly with the `Error` produced by the underlying `URLSession`. + case sessionInvalidated(error: Error?) + /// `URLSessionTask` completed with error. + case sessionTaskFailed(error: Error) + /// `URLRequest` failed validation. + case urlRequestValidationFailed(reason: URLRequestValidationFailureReason) +} + +extension Error { + /// Returns the instance cast as an `AFError`. + public var asAFError: AFError? { + self as? AFError + } + + /// Returns the instance cast as an `AFError`. If casting fails, a `fatalError` with the specified `message` is thrown. + public func asAFError(orFailWith message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) -> AFError { + guard let afError = self as? AFError else { + fatalError(message(), file: file, line: line) + } + return afError + } + + /// Casts the instance as `AFError` or returns `defaultAFError` + func asAFError(or defaultAFError: @autoclosure () -> AFError) -> AFError { + self as? AFError ?? defaultAFError() + } +} + +// MARK: - Error Booleans + +extension AFError { + /// Returns whether the instance is `.sessionDeinitialized`. + public var isSessionDeinitializedError: Bool { + if case .sessionDeinitialized = self { return true } + return false + } + + /// Returns whether the instance is `.sessionInvalidated`. + public var isSessionInvalidatedError: Bool { + if case .sessionInvalidated = self { return true } + return false + } + + /// Returns whether the instance is `.explicitlyCancelled`. + public var isExplicitlyCancelledError: Bool { + if case .explicitlyCancelled = self { return true } + return false + } + + /// Returns whether the instance is `.invalidURL`. + public var isInvalidURLError: Bool { + if case .invalidURL = self { return true } + return false + } + + /// Returns whether the instance is `.parameterEncodingFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isParameterEncodingError: Bool { + if case .parameterEncodingFailed = self { return true } + return false + } + + /// Returns whether the instance is `.parameterEncoderFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isParameterEncoderError: Bool { + if case .parameterEncoderFailed = self { return true } + return false + } + + /// Returns whether the instance is `.multipartEncodingFailed`. When `true`, the `url` and `underlyingError` + /// properties will contain the associated values. + public var isMultipartEncodingError: Bool { + if case .multipartEncodingFailed = self { return true } + return false + } + + /// Returns whether the instance is `.requestAdaptationFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isRequestAdaptationError: Bool { + if case .requestAdaptationFailed = self { return true } + return false + } + + /// Returns whether the instance is `.responseValidationFailed`. When `true`, the `acceptableContentTypes`, + /// `responseContentType`, `responseCode`, and `underlyingError` properties will contain the associated values. + public var isResponseValidationError: Bool { + if case .responseValidationFailed = self { return true } + return false + } + + /// Returns whether the instance is `.responseSerializationFailed`. When `true`, the `failedStringEncoding` and + /// `underlyingError` properties will contain the associated values. + public var isResponseSerializationError: Bool { + if case .responseSerializationFailed = self { return true } + return false + } + + #if !(os(Linux) || os(Windows)) + /// Returns whether the instance is `.serverTrustEvaluationFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isServerTrustEvaluationError: Bool { + if case .serverTrustEvaluationFailed = self { return true } + return false + } + #endif + + /// Returns whether the instance is `requestRetryFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isRequestRetryError: Bool { + if case .requestRetryFailed = self { return true } + return false + } + + /// Returns whether the instance is `createUploadableFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isCreateUploadableError: Bool { + if case .createUploadableFailed = self { return true } + return false + } + + /// Returns whether the instance is `createURLRequestFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isCreateURLRequestError: Bool { + if case .createURLRequestFailed = self { return true } + return false + } + + /// Returns whether the instance is `downloadedFileMoveFailed`. When `true`, the `destination` and `underlyingError` properties will + /// contain the associated values. + public var isDownloadedFileMoveError: Bool { + if case .downloadedFileMoveFailed = self { return true } + return false + } + + /// Returns whether the instance is `createURLRequestFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isSessionTaskError: Bool { + if case .sessionTaskFailed = self { return true } + return false + } +} + +// MARK: - Convenience Properties + +extension AFError { + /// The `URLConvertible` associated with the error. + public var urlConvertible: URLConvertible? { + guard case let .invalidURL(url) = self else { return nil } + return url + } + + /// The `URL` associated with the error. + public var url: URL? { + guard case let .multipartEncodingFailed(reason) = self else { return nil } + return reason.url + } + + /// The underlying `Error` responsible for generating the failure associated with `.sessionInvalidated`, + /// `.parameterEncodingFailed`, `.parameterEncoderFailed`, `.multipartEncodingFailed`, `.requestAdaptationFailed`, + /// `.responseSerializationFailed`, `.requestRetryFailed` errors. + public var underlyingError: Error? { + switch self { + case let .multipartEncodingFailed(reason): + return reason.underlyingError + case let .parameterEncodingFailed(reason): + return reason.underlyingError + case let .parameterEncoderFailed(reason): + return reason.underlyingError + case let .requestAdaptationFailed(error): + return error + case let .requestRetryFailed(retryError, _): + return retryError + case let .responseValidationFailed(reason): + return reason.underlyingError + case let .responseSerializationFailed(reason): + return reason.underlyingError + #if !(os(Linux) || os(Windows)) + case let .serverTrustEvaluationFailed(reason): + return reason.underlyingError + #endif + case let .sessionInvalidated(error): + return error + case let .createUploadableFailed(error): + return error + case let .createURLRequestFailed(error): + return error + case let .downloadedFileMoveFailed(error, _, _): + return error + case let .sessionTaskFailed(error): + return error + case .explicitlyCancelled, + .invalidURL, + .sessionDeinitialized, + .urlRequestValidationFailed: + return nil + } + } + + /// The acceptable `Content-Type`s of a `.responseValidationFailed` error. + public var acceptableContentTypes: [String]? { + guard case let .responseValidationFailed(reason) = self else { return nil } + return reason.acceptableContentTypes + } + + /// The response `Content-Type` of a `.responseValidationFailed` error. + public var responseContentType: String? { + guard case let .responseValidationFailed(reason) = self else { return nil } + return reason.responseContentType + } + + /// The response code of a `.responseValidationFailed` error. + public var responseCode: Int? { + guard case let .responseValidationFailed(reason) = self else { return nil } + return reason.responseCode + } + + /// The `String.Encoding` associated with a failed `.stringResponse()` call. + public var failedStringEncoding: String.Encoding? { + guard case let .responseSerializationFailed(reason) = self else { return nil } + return reason.failedStringEncoding + } + + /// The `source` URL of a `.downloadedFileMoveFailed` error. + public var sourceURL: URL? { + guard case let .downloadedFileMoveFailed(_, source, _) = self else { return nil } + return source + } + + /// The `destination` URL of a `.downloadedFileMoveFailed` error. + public var destinationURL: URL? { + guard case let .downloadedFileMoveFailed(_, _, destination) = self else { return nil } + return destination + } + + #if !(os(Linux) || os(Windows)) + /// The download resume data of any underlying network error. Only produced by `DownloadRequest`s. + public var downloadResumeData: Data? { + (underlyingError as? URLError)?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data + } + #endif +} + +extension AFError.ParameterEncodingFailureReason { + var underlyingError: Error? { + switch self { + case let .jsonEncodingFailed(error), + let .customEncodingFailed(error): + return error + case .missingURL: + return nil + } + } +} + +extension AFError.ParameterEncoderFailureReason { + var underlyingError: Error? { + switch self { + case let .encoderFailed(error): + return error + case .missingRequiredComponent: + return nil + } + } +} + +extension AFError.MultipartEncodingFailureReason { + var url: URL? { + switch self { + case let .bodyPartURLInvalid(url), + let .bodyPartFilenameInvalid(url), + let .bodyPartFileNotReachable(url), + let .bodyPartFileIsDirectory(url), + let .bodyPartFileSizeNotAvailable(url), + let .bodyPartInputStreamCreationFailed(url), + let .outputStreamCreationFailed(url), + let .outputStreamFileAlreadyExists(url), + let .outputStreamURLInvalid(url), + let .bodyPartFileNotReachableWithError(url, _), + let .bodyPartFileSizeQueryFailedWithError(url, _): + return url + case .outputStreamWriteFailed, + .inputStreamReadFailed: + return nil + } + } + + var underlyingError: Error? { + switch self { + case let .bodyPartFileNotReachableWithError(_, error), + let .bodyPartFileSizeQueryFailedWithError(_, error), + let .outputStreamWriteFailed(error), + let .inputStreamReadFailed(error): + return error + case .bodyPartURLInvalid, + .bodyPartFilenameInvalid, + .bodyPartFileNotReachable, + .bodyPartFileIsDirectory, + .bodyPartFileSizeNotAvailable, + .bodyPartInputStreamCreationFailed, + .outputStreamCreationFailed, + .outputStreamFileAlreadyExists, + .outputStreamURLInvalid: + return nil + } + } +} + +extension AFError.ResponseValidationFailureReason { + var acceptableContentTypes: [String]? { + switch self { + case let .missingContentType(types), + let .unacceptableContentType(types, _): + return types + case .dataFileNil, + .dataFileReadFailed, + .unacceptableStatusCode, + .customValidationFailed: + return nil + } + } + + var responseContentType: String? { + switch self { + case let .unacceptableContentType(_, responseType): + return responseType + case .dataFileNil, + .dataFileReadFailed, + .missingContentType, + .unacceptableStatusCode, + .customValidationFailed: + return nil + } + } + + var responseCode: Int? { + switch self { + case let .unacceptableStatusCode(code): + return code + case .dataFileNil, + .dataFileReadFailed, + .missingContentType, + .unacceptableContentType, + .customValidationFailed: + return nil + } + } + + var underlyingError: Error? { + switch self { + case let .customValidationFailed(error): + return error + case .dataFileNil, + .dataFileReadFailed, + .missingContentType, + .unacceptableContentType, + .unacceptableStatusCode: + return nil + } + } +} + +extension AFError.ResponseSerializationFailureReason { + var failedStringEncoding: String.Encoding? { + switch self { + case let .stringSerializationFailed(encoding): + return encoding + case .inputDataNilOrZeroLength, + .inputFileNil, + .inputFileReadFailed(_), + .jsonSerializationFailed(_), + .decodingFailed(_), + .customSerializationFailed(_), + .invalidEmptyResponse: + return nil + } + } + + var underlyingError: Error? { + switch self { + case let .jsonSerializationFailed(error), + let .decodingFailed(error), + let .customSerializationFailed(error): + return error + case .inputDataNilOrZeroLength, + .inputFileNil, + .inputFileReadFailed, + .stringSerializationFailed, + .invalidEmptyResponse: + return nil + } + } +} + +#if !(os(Linux) || os(Windows)) +extension AFError.ServerTrustFailureReason { + var output: AFError.ServerTrustFailureReason.Output? { + switch self { + case let .defaultEvaluationFailed(output), + let .hostValidationFailed(output), + let .revocationCheckFailed(output, _): + return output + case .noRequiredEvaluator, + .noCertificatesFound, + .noPublicKeysFound, + .policyApplicationFailed, + .settingAnchorCertificatesFailed, + .revocationPolicyCreationFailed, + .trustEvaluationFailed, + .certificatePinningFailed, + .publicKeyPinningFailed, + .customEvaluationFailed: + return nil + } + } + + var underlyingError: Error? { + switch self { + case let .customEvaluationFailed(error): + return error + case let .trustEvaluationFailed(error): + return error + case .noRequiredEvaluator, + .noCertificatesFound, + .noPublicKeysFound, + .policyApplicationFailed, + .settingAnchorCertificatesFailed, + .revocationPolicyCreationFailed, + .defaultEvaluationFailed, + .hostValidationFailed, + .revocationCheckFailed, + .certificatePinningFailed, + .publicKeyPinningFailed: + return nil + } + } +} +#endif + +// MARK: - Error Descriptions + +extension AFError: LocalizedError { + public var errorDescription: String? { + switch self { + case .explicitlyCancelled: + return "Request explicitly cancelled." + case let .invalidURL(url): + return "URL is not valid: \(url)" + case let .parameterEncodingFailed(reason): + return reason.localizedDescription + case let .parameterEncoderFailed(reason): + return reason.localizedDescription + case let .multipartEncodingFailed(reason): + return reason.localizedDescription + case let .requestAdaptationFailed(error): + return "Request adaption failed with error: \(error.localizedDescription)" + case let .responseValidationFailed(reason): + return reason.localizedDescription + case let .responseSerializationFailed(reason): + return reason.localizedDescription + case let .requestRetryFailed(retryError, originalError): + return """ + Request retry failed with retry error: \(retryError.localizedDescription), \ + original error: \(originalError.localizedDescription) + """ + case .sessionDeinitialized: + return """ + Session was invalidated without error, so it was likely deinitialized unexpectedly. \ + Be sure to retain a reference to your Session for the duration of your requests. + """ + case let .sessionInvalidated(error): + return "Session was invalidated with error: \(error?.localizedDescription ?? "No description.")" + #if !(os(Linux) || os(Windows)) + case let .serverTrustEvaluationFailed(reason): + return "Server trust evaluation failed due to reason: \(reason.localizedDescription)" + #endif + case let .urlRequestValidationFailed(reason): + return "URLRequest validation failed due to reason: \(reason.localizedDescription)" + case let .createUploadableFailed(error): + return "Uploadable creation failed with error: \(error.localizedDescription)" + case let .createURLRequestFailed(error): + return "URLRequest creation failed with error: \(error.localizedDescription)" + case let .downloadedFileMoveFailed(error, source, destination): + return "Moving downloaded file from: \(source) to: \(destination) failed with error: \(error.localizedDescription)" + case let .sessionTaskFailed(error): + return "URLSessionTask failed with error: \(error.localizedDescription)" + } + } +} + +extension AFError.ParameterEncodingFailureReason { + var localizedDescription: String { + switch self { + case .missingURL: + return "URL request to encode was missing a URL" + case let .jsonEncodingFailed(error): + return "JSON could not be encoded because of error:\n\(error.localizedDescription)" + case let .customEncodingFailed(error): + return "Custom parameter encoder failed with error: \(error.localizedDescription)" + } + } +} + +extension AFError.ParameterEncoderFailureReason { + var localizedDescription: String { + switch self { + case let .missingRequiredComponent(component): + return "Encoding failed due to a missing request component: \(component)" + case let .encoderFailed(error): + return "The underlying encoder failed with the error: \(error)" + } + } +} + +extension AFError.MultipartEncodingFailureReason { + var localizedDescription: String { + switch self { + case let .bodyPartURLInvalid(url): + return "The URL provided is not a file URL: \(url)" + case let .bodyPartFilenameInvalid(url): + return "The URL provided does not have a valid filename: \(url)" + case let .bodyPartFileNotReachable(url): + return "The URL provided is not reachable: \(url)" + case let .bodyPartFileNotReachableWithError(url, error): + return """ + The system returned an error while checking the provided URL for reachability. + URL: \(url) + Error: \(error) + """ + case let .bodyPartFileIsDirectory(url): + return "The URL provided is a directory: \(url)" + case let .bodyPartFileSizeNotAvailable(url): + return "Could not fetch the file size from the provided URL: \(url)" + case let .bodyPartFileSizeQueryFailedWithError(url, error): + return """ + The system returned an error while attempting to fetch the file size from the provided URL. + URL: \(url) + Error: \(error) + """ + case let .bodyPartInputStreamCreationFailed(url): + return "Failed to create an InputStream for the provided URL: \(url)" + case let .outputStreamCreationFailed(url): + return "Failed to create an OutputStream for URL: \(url)" + case let .outputStreamFileAlreadyExists(url): + return "A file already exists at the provided URL: \(url)" + case let .outputStreamURLInvalid(url): + return "The provided OutputStream URL is invalid: \(url)" + case let .outputStreamWriteFailed(error): + return "OutputStream write failed with error: \(error)" + case let .inputStreamReadFailed(error): + return "InputStream read failed with error: \(error)" + } + } +} + +extension AFError.ResponseSerializationFailureReason { + var localizedDescription: String { + switch self { + case .inputDataNilOrZeroLength: + return "Response could not be serialized, input data was nil or zero length." + case .inputFileNil: + return "Response could not be serialized, input file was nil." + case let .inputFileReadFailed(url): + return "Response could not be serialized, input file could not be read: \(url)." + case let .stringSerializationFailed(encoding): + return "String could not be serialized with encoding: \(encoding)." + case let .jsonSerializationFailed(error): + return "JSON could not be serialized because of error:\n\(error.localizedDescription)" + case let .invalidEmptyResponse(type): + return """ + Empty response could not be serialized to type: \(type). \ + Use Empty as the expected type for such responses. + """ + case let .decodingFailed(error): + return "Response could not be decoded because of error:\n\(error.localizedDescription)" + case let .customSerializationFailed(error): + return "Custom response serializer failed with error:\n\(error.localizedDescription)" + } + } +} + +extension AFError.ResponseValidationFailureReason { + var localizedDescription: String { + switch self { + case .dataFileNil: + return "Response could not be validated, data file was nil." + case let .dataFileReadFailed(url): + return "Response could not be validated, data file could not be read: \(url)." + case let .missingContentType(types): + return """ + Response Content-Type was missing and acceptable content types \ + (\(types.joined(separator: ","))) do not match "*/*". + """ + case let .unacceptableContentType(acceptableTypes, responseType): + return """ + Response Content-Type "\(responseType)" does not match any acceptable types: \ + \(acceptableTypes.joined(separator: ",")). + """ + case let .unacceptableStatusCode(code): + return "Response status code was unacceptable: \(code)." + case let .customValidationFailed(error): + return "Custom response validation failed with error: \(error.localizedDescription)" + } + } +} + +#if !(os(Linux) || os(Windows)) +extension AFError.ServerTrustFailureReason { + var localizedDescription: String { + switch self { + case let .noRequiredEvaluator(host): + return "A ServerTrustEvaluating value is required for host \(host) but none was found." + case .noCertificatesFound: + return "No certificates were found or provided for evaluation." + case .noPublicKeysFound: + return "No public keys were found or provided for evaluation." + case .policyApplicationFailed: + return "Attempting to set a SecPolicy failed." + case .settingAnchorCertificatesFailed: + return "Attempting to set the provided certificates as anchor certificates failed." + case .revocationPolicyCreationFailed: + return "Attempting to create a revocation policy failed." + case let .trustEvaluationFailed(error): + return "SecTrust evaluation failed with error: \(error?.localizedDescription ?? "None")" + case let .defaultEvaluationFailed(output): + return "Default evaluation failed for host \(output.host)." + case let .hostValidationFailed(output): + return "Host validation failed for host \(output.host)." + case let .revocationCheckFailed(output, _): + return "Revocation check failed for host \(output.host)." + case let .certificatePinningFailed(host, _, _, _): + return "Certificate pinning failed for host \(host)." + case let .publicKeyPinningFailed(host, _, _, _): + return "Public key pinning failed for host \(host)." + case let .customEvaluationFailed(error): + return "Custom trust evaluation failed with error: \(error.localizedDescription)" + } + } +} +#endif + +extension AFError.URLRequestValidationFailureReason { + var localizedDescription: String { + switch self { + case let .bodyDataInGETRequest(data): + return """ + Invalid URLRequest: Requests with GET method cannot have body data: + \(String(decoding: data, as: UTF8.self)) + """ + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/Alamofire.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/Alamofire.swift new file mode 100644 index 0000000..d6fc39e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/Alamofire.swift @@ -0,0 +1,40 @@ +// +// Alamofire.swift +// +// Copyright (c) 2014-2021 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Dispatch +import Foundation +#if canImport(FoundationNetworking) +@_exported import FoundationNetworking +#endif + +// Enforce minimum Swift version for all platforms and build systems. +#if swift(<5.3) +#error("Alamofire doesn't support Swift versions below 5.3.") +#endif + +/// Reference to `Session.default` for quick bootstrapping and examples. +public let AF = Session.default + +/// Current Alamofire version. Necessary since SPM doesn't use dynamic libraries. Plus this will be more accurate. +let version = "5.6.1" diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/AlamofireExtended.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/AlamofireExtended.swift new file mode 100644 index 0000000..280c6de --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/AlamofireExtended.swift @@ -0,0 +1,61 @@ +// +// AlamofireExtended.swift +// +// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +/// Type that acts as a generic extension point for all `AlamofireExtended` types. +public struct AlamofireExtension { + /// Stores the type or meta-type of any extended type. + public private(set) var type: ExtendedType + + /// Create an instance from the provided value. + /// + /// - Parameter type: Instance being extended. + public init(_ type: ExtendedType) { + self.type = type + } +} + +/// Protocol describing the `af` extension points for Alamofire extended types. +public protocol AlamofireExtended { + /// Type being extended. + associatedtype ExtendedType + + /// Static Alamofire extension point. + static var af: AlamofireExtension.Type { get set } + /// Instance Alamofire extension point. + var af: AlamofireExtension { get set } +} + +extension AlamofireExtended { + /// Static Alamofire extension point. + public static var af: AlamofireExtension.Type { + get { AlamofireExtension.self } + set {} + } + + /// Instance Alamofire extension point. + public var af: AlamofireExtension { + get { AlamofireExtension(self) } + set {} + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/AuthenticationInterceptor.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/AuthenticationInterceptor.swift new file mode 100644 index 0000000..c3a3f31 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/AuthenticationInterceptor.swift @@ -0,0 +1,403 @@ +// +// AuthenticationInterceptor.swift +// +// Copyright (c) 2020 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Types adopting the `AuthenticationCredential` protocol can be used to authenticate `URLRequest`s. +/// +/// One common example of an `AuthenticationCredential` is an OAuth2 credential containing an access token used to +/// authenticate all requests on behalf of a user. The access token generally has an expiration window of 60 minutes +/// which will then require a refresh of the credential using the refresh token to generate a new access token. +public protocol AuthenticationCredential { + /// Whether the credential requires a refresh. This property should always return `true` when the credential is + /// expired. It is also wise to consider returning `true` when the credential will expire in several seconds or + /// minutes depending on the expiration window of the credential. + /// + /// For example, if the credential is valid for 60 minutes, then it would be wise to return `true` when the + /// credential is only valid for 5 minutes or less. That ensures the credential will not expire as it is passed + /// around backend services. + var requiresRefresh: Bool { get } +} + +// MARK: - + +/// Types adopting the `Authenticator` protocol can be used to authenticate `URLRequest`s with an +/// `AuthenticationCredential` as well as refresh the `AuthenticationCredential` when required. +public protocol Authenticator: AnyObject { + /// The type of credential associated with the `Authenticator` instance. + associatedtype Credential: AuthenticationCredential + + /// Applies the `Credential` to the `URLRequest`. + /// + /// In the case of OAuth2, the access token of the `Credential` would be added to the `URLRequest` as a Bearer + /// token to the `Authorization` header. + /// + /// - Parameters: + /// - credential: The `Credential`. + /// - urlRequest: The `URLRequest`. + func apply(_ credential: Credential, to urlRequest: inout URLRequest) + + /// Refreshes the `Credential` and executes the `completion` closure with the `Result` once complete. + /// + /// Refresh can be called in one of two ways. It can be called before the `Request` is actually executed due to + /// a `requiresRefresh` returning `true` during the adapt portion of the `Request` creation process. It can also + /// be triggered by a failed `Request` where the authentication server denied access due to an expired or + /// invalidated access token. + /// + /// In the case of OAuth2, this method would use the refresh token of the `Credential` to generate a new + /// `Credential` using the authentication service. Once complete, the `completion` closure should be called with + /// the new `Credential`, or the error that occurred. + /// + /// In general, if the refresh call fails with certain status codes from the authentication server (commonly a 401), + /// the refresh token in the `Credential` can no longer be used to generate a valid `Credential`. In these cases, + /// you will need to reauthenticate the user with their username / password. + /// + /// Please note, these are just general examples of common use cases. They are not meant to solve your specific + /// authentication server challenges. Please work with your authentication server team to ensure your + /// `Authenticator` logic matches their expectations. + /// + /// - Parameters: + /// - credential: The `Credential` to refresh. + /// - session: The `Session` requiring the refresh. + /// - completion: The closure to be executed once the refresh is complete. + func refresh(_ credential: Credential, for session: Session, completion: @escaping (Result) -> Void) + + /// Determines whether the `URLRequest` failed due to an authentication error based on the `HTTPURLResponse`. + /// + /// If the authentication server **CANNOT** invalidate credentials after they are issued, then simply return `false` + /// for this method. If the authentication server **CAN** invalidate credentials due to security breaches, then you + /// will need to work with your authentication server team to understand how to identify when this occurs. + /// + /// In the case of OAuth2, where an authentication server can invalidate credentials, you will need to inspect the + /// `HTTPURLResponse` or possibly the `Error` for when this occurs. This is commonly handled by the authentication + /// server returning a 401 status code and some additional header to indicate an OAuth2 failure occurred. + /// + /// It is very important to understand how your authentication server works to be able to implement this correctly. + /// For example, if your authentication server returns a 401 when an OAuth2 error occurs, and your downstream + /// service also returns a 401 when you are not authorized to perform that operation, how do you know which layer + /// of the backend returned you a 401? You do not want to trigger a refresh unless you know your authentication + /// server is actually the layer rejecting the request. Again, work with your authentication server team to understand + /// how to identify an OAuth2 401 error vs. a downstream 401 error to avoid endless refresh loops. + /// + /// - Parameters: + /// - urlRequest: The `URLRequest`. + /// - response: The `HTTPURLResponse`. + /// - error: The `Error`. + /// + /// - Returns: `true` if the `URLRequest` failed due to an authentication error, `false` otherwise. + func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool + + /// Determines whether the `URLRequest` is authenticated with the `Credential`. + /// + /// If the authentication server **CANNOT** invalidate credentials after they are issued, then simply return `true` + /// for this method. If the authentication server **CAN** invalidate credentials due to security breaches, then + /// read on. + /// + /// When an authentication server can invalidate credentials, it means that you may have a non-expired credential + /// that appears to be valid, but will be rejected by the authentication server when used. Generally when this + /// happens, a number of requests are all sent when the application is foregrounded, and all of them will be + /// rejected by the authentication server in the order they are received. The first failed request will trigger a + /// refresh internally, which will update the credential, and then retry all the queued requests with the new + /// credential. However, it is possible that some of the original requests will not return from the authentication + /// server until the refresh has completed. This is where this method comes in. + /// + /// When the authentication server rejects a credential, we need to check to make sure we haven't refreshed the + /// credential while the request was in flight. If it has already refreshed, then we don't need to trigger an + /// additional refresh. If it hasn't refreshed, then we need to refresh. + /// + /// Now that it is understood how the result of this method is used in the refresh lifecyle, let's walk through how + /// to implement it. You should return `true` in this method if the `URLRequest` is authenticated in a way that + /// matches the values in the `Credential`. In the case of OAuth2, this would mean that the Bearer token in the + /// `Authorization` header of the `URLRequest` matches the access token in the `Credential`. If it matches, then we + /// know the `Credential` was used to authenticate the `URLRequest` and should return `true`. If the Bearer token + /// did not match the access token, then you should return `false`. + /// + /// - Parameters: + /// - urlRequest: The `URLRequest`. + /// - credential: The `Credential`. + /// + /// - Returns: `true` if the `URLRequest` is authenticated with the `Credential`, `false` otherwise. + func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: Credential) -> Bool +} + +// MARK: - + +/// Represents various authentication failures that occur when using the `AuthenticationInterceptor`. All errors are +/// still vended from Alamofire as `AFError` types. The `AuthenticationError` instances will be embedded within +/// `AFError` `.requestAdaptationFailed` or `.requestRetryFailed` cases. +public enum AuthenticationError: Error { + /// The credential was missing so the request could not be authenticated. + case missingCredential + /// The credential was refreshed too many times within the `RefreshWindow`. + case excessiveRefresh +} + +// MARK: - + +/// The `AuthenticationInterceptor` class manages the queuing and threading complexity of authenticating requests. +/// It relies on an `Authenticator` type to handle the actual `URLRequest` authentication and `Credential` refresh. +public class AuthenticationInterceptor: RequestInterceptor where AuthenticatorType: Authenticator { + // MARK: Typealiases + + /// Type of credential used to authenticate requests. + public typealias Credential = AuthenticatorType.Credential + + // MARK: Helper Types + + /// Type that defines a time window used to identify excessive refresh calls. When enabled, prior to executing a + /// refresh, the `AuthenticationInterceptor` compares the timestamp history of previous refresh calls against the + /// `RefreshWindow`. If more refreshes have occurred within the refresh window than allowed, the refresh is + /// cancelled and an `AuthorizationError.excessiveRefresh` error is thrown. + public struct RefreshWindow { + /// `TimeInterval` defining the duration of the time window before the current time in which the number of + /// refresh attempts is compared against `maximumAttempts`. For example, if `interval` is 30 seconds, then the + /// `RefreshWindow` represents the past 30 seconds. If more attempts occurred in the past 30 seconds than + /// `maximumAttempts`, an `.excessiveRefresh` error will be thrown. + public let interval: TimeInterval + + /// Total refresh attempts allowed within `interval` before throwing an `.excessiveRefresh` error. + public let maximumAttempts: Int + + /// Creates a `RefreshWindow` instance from the specified `interval` and `maximumAttempts`. + /// + /// - Parameters: + /// - interval: `TimeInterval` defining the duration of the time window before the current time. + /// - maximumAttempts: The maximum attempts allowed within the `TimeInterval`. + public init(interval: TimeInterval = 30.0, maximumAttempts: Int = 5) { + self.interval = interval + self.maximumAttempts = maximumAttempts + } + } + + private struct AdaptOperation { + let urlRequest: URLRequest + let session: Session + let completion: (Result) -> Void + } + + private enum AdaptResult { + case adapt(Credential) + case doNotAdapt(AuthenticationError) + case adaptDeferred + } + + private struct MutableState { + var credential: Credential? + + var isRefreshing = false + var refreshTimestamps: [TimeInterval] = [] + var refreshWindow: RefreshWindow? + + var adaptOperations: [AdaptOperation] = [] + var requestsToRetry: [(RetryResult) -> Void] = [] + } + + // MARK: Properties + + /// The `Credential` used to authenticate requests. + public var credential: Credential? { + get { $mutableState.credential } + set { $mutableState.credential = newValue } + } + + let authenticator: AuthenticatorType + let queue = DispatchQueue(label: "org.alamofire.authentication.inspector") + + @Protected + private var mutableState: MutableState + + // MARK: Initialization + + /// Creates an `AuthenticationInterceptor` instance from the specified parameters. + /// + /// A `nil` `RefreshWindow` will result in the `AuthenticationInterceptor` not checking for excessive refresh calls. + /// It is recommended to always use a `RefreshWindow` to avoid endless refresh cycles. + /// + /// - Parameters: + /// - authenticator: The `Authenticator` type. + /// - credential: The `Credential` if it exists. `nil` by default. + /// - refreshWindow: The `RefreshWindow` used to identify excessive refresh calls. `RefreshWindow()` by default. + public init(authenticator: AuthenticatorType, + credential: Credential? = nil, + refreshWindow: RefreshWindow? = RefreshWindow()) { + self.authenticator = authenticator + mutableState = MutableState(credential: credential, refreshWindow: refreshWindow) + } + + // MARK: Adapt + + public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + let adaptResult: AdaptResult = $mutableState.write { mutableState in + // Queue the adapt operation if a refresh is already in place. + guard !mutableState.isRefreshing else { + let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion) + mutableState.adaptOperations.append(operation) + return .adaptDeferred + } + + // Throw missing credential error is the credential is missing. + guard let credential = mutableState.credential else { + let error = AuthenticationError.missingCredential + return .doNotAdapt(error) + } + + // Queue the adapt operation and trigger refresh operation if credential requires refresh. + guard !credential.requiresRefresh else { + let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion) + mutableState.adaptOperations.append(operation) + refresh(credential, for: session, insideLock: &mutableState) + return .adaptDeferred + } + + return .adapt(credential) + } + + switch adaptResult { + case let .adapt(credential): + var authenticatedRequest = urlRequest + authenticator.apply(credential, to: &authenticatedRequest) + completion(.success(authenticatedRequest)) + + case let .doNotAdapt(adaptError): + completion(.failure(adaptError)) + + case .adaptDeferred: + // No-op: adapt operation captured during refresh. + break + } + } + + // MARK: Retry + + public func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) { + // Do not attempt retry if there was not an original request and response from the server. + guard let urlRequest = request.request, let response = request.response else { + completion(.doNotRetry) + return + } + + // Do not attempt retry unless the `Authenticator` verifies failure was due to authentication error (i.e. 401 status code). + guard authenticator.didRequest(urlRequest, with: response, failDueToAuthenticationError: error) else { + completion(.doNotRetry) + return + } + + // Do not attempt retry if there is no credential. + guard let credential = credential else { + let error = AuthenticationError.missingCredential + completion(.doNotRetryWithError(error)) + return + } + + // Retry the request if the `Authenticator` verifies it was authenticated with a previous credential. + guard authenticator.isRequest(urlRequest, authenticatedWith: credential) else { + completion(.retry) + return + } + + $mutableState.write { mutableState in + mutableState.requestsToRetry.append(completion) + + guard !mutableState.isRefreshing else { return } + + refresh(credential, for: session, insideLock: &mutableState) + } + } + + // MARK: Refresh + + private func refresh(_ credential: Credential, for session: Session, insideLock mutableState: inout MutableState) { + guard !isRefreshExcessive(insideLock: &mutableState) else { + let error = AuthenticationError.excessiveRefresh + handleRefreshFailure(error, insideLock: &mutableState) + return + } + + mutableState.refreshTimestamps.append(ProcessInfo.processInfo.systemUptime) + mutableState.isRefreshing = true + + // Dispatch to queue to hop out of the lock in case authenticator.refresh is implemented synchronously. + queue.async { + self.authenticator.refresh(credential, for: session) { result in + self.$mutableState.write { mutableState in + switch result { + case let .success(credential): + self.handleRefreshSuccess(credential, insideLock: &mutableState) + case let .failure(error): + self.handleRefreshFailure(error, insideLock: &mutableState) + } + } + } + } + } + + private func isRefreshExcessive(insideLock mutableState: inout MutableState) -> Bool { + guard let refreshWindow = mutableState.refreshWindow else { return false } + + let refreshWindowMin = ProcessInfo.processInfo.systemUptime - refreshWindow.interval + + let refreshAttemptsWithinWindow = mutableState.refreshTimestamps.reduce(into: 0) { attempts, refreshTimestamp in + guard refreshWindowMin <= refreshTimestamp else { return } + attempts += 1 + } + + let isRefreshExcessive = refreshAttemptsWithinWindow >= refreshWindow.maximumAttempts + + return isRefreshExcessive + } + + private func handleRefreshSuccess(_ credential: Credential, insideLock mutableState: inout MutableState) { + mutableState.credential = credential + + let adaptOperations = mutableState.adaptOperations + let requestsToRetry = mutableState.requestsToRetry + + mutableState.adaptOperations.removeAll() + mutableState.requestsToRetry.removeAll() + + mutableState.isRefreshing = false + + // Dispatch to queue to hop out of the mutable state lock + queue.async { + adaptOperations.forEach { self.adapt($0.urlRequest, for: $0.session, completion: $0.completion) } + requestsToRetry.forEach { $0(.retry) } + } + } + + private func handleRefreshFailure(_ error: Error, insideLock mutableState: inout MutableState) { + let adaptOperations = mutableState.adaptOperations + let requestsToRetry = mutableState.requestsToRetry + + mutableState.adaptOperations.removeAll() + mutableState.requestsToRetry.removeAll() + + mutableState.isRefreshing = false + + // Dispatch to queue to hop out of the mutable state lock + queue.async { + adaptOperations.forEach { $0.completion(.failure(error)) } + requestsToRetry.forEach { $0(.doNotRetryWithError(error)) } + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/CachedResponseHandler.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/CachedResponseHandler.swift new file mode 100644 index 0000000..e7d0060 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/CachedResponseHandler.swift @@ -0,0 +1,109 @@ +// +// CachedResponseHandler.swift +// +// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A type that handles whether the data task should store the HTTP response in the cache. +public protocol CachedResponseHandler { + /// Determines whether the HTTP response should be stored in the cache. + /// + /// The `completion` closure should be passed one of three possible options: + /// + /// 1. The cached response provided by the server (this is the most common use case). + /// 2. A modified version of the cached response (you may want to modify it in some way before caching). + /// 3. A `nil` value to prevent the cached response from being stored in the cache. + /// + /// - Parameters: + /// - task: The data task whose request resulted in the cached response. + /// - response: The cached response to potentially store in the cache. + /// - completion: The closure to execute containing cached response, a modified response, or `nil`. + func dataTask(_ task: URLSessionDataTask, + willCacheResponse response: CachedURLResponse, + completion: @escaping (CachedURLResponse?) -> Void) +} + +// MARK: - + +/// `ResponseCacher` is a convenience `CachedResponseHandler` making it easy to cache, not cache, or modify a cached +/// response. +public struct ResponseCacher { + /// Defines the behavior of the `ResponseCacher` type. + public enum Behavior { + /// Stores the cached response in the cache. + case cache + /// Prevents the cached response from being stored in the cache. + case doNotCache + /// Modifies the cached response before storing it in the cache. + case modify((URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?) + } + + /// Returns a `ResponseCacher` with a `.cache` `Behavior`. + public static let cache = ResponseCacher(behavior: .cache) + /// Returns a `ResponseCacher` with a `.doNotCache` `Behavior`. + public static let doNotCache = ResponseCacher(behavior: .doNotCache) + + /// The `Behavior` of the `ResponseCacher`. + public let behavior: Behavior + + /// Creates a `ResponseCacher` instance from the `Behavior`. + /// + /// - Parameter behavior: The `Behavior`. + public init(behavior: Behavior) { + self.behavior = behavior + } +} + +extension ResponseCacher: CachedResponseHandler { + public func dataTask(_ task: URLSessionDataTask, + willCacheResponse response: CachedURLResponse, + completion: @escaping (CachedURLResponse?) -> Void) { + switch behavior { + case .cache: + completion(response) + case .doNotCache: + completion(nil) + case let .modify(closure): + let response = closure(task, response) + completion(response) + } + } +} + +#if swift(>=5.5) +extension CachedResponseHandler where Self == ResponseCacher { + /// Provides a `ResponseCacher` which caches the response, if allowed. Equivalent to `ResponseCacher.cache`. + public static var cache: ResponseCacher { .cache } + + /// Provides a `ResponseCacher` which does not cache the response. Equivalent to `ResponseCacher.doNotCache`. + public static var doNotCache: ResponseCacher { .doNotCache } + + /// Creates a `ResponseCacher` which modifies the proposed `CachedURLResponse` using the provided closure. + /// + /// - Parameter closure: Closure used to modify the `CachedURLResponse`. + /// - Returns: The `ResponseCacher`. + public static func modify(using closure: @escaping ((URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)) -> ResponseCacher { + ResponseCacher(behavior: .modify(closure)) + } +} +#endif diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/Combine.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/Combine.swift new file mode 100644 index 0000000..066ba47 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/Combine.swift @@ -0,0 +1,655 @@ +// +// Combine.swift +// +// Copyright (c) 2020 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if !((os(iOS) && (arch(i386) || arch(arm))) || os(Windows) || os(Linux)) + +import Combine +import Dispatch +import Foundation + +// MARK: - DataRequest / UploadRequest + +/// A Combine `Publisher` that publishes the `DataResponse` of the provided `DataRequest`. +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +public struct DataResponsePublisher: Publisher { + public typealias Output = DataResponse + public typealias Failure = Never + + private typealias Handler = (@escaping (_ response: DataResponse) -> Void) -> DataRequest + + private let request: DataRequest + private let responseHandler: Handler + + /// Creates an instance which will serialize responses using the provided `ResponseSerializer`. + /// + /// - Parameters: + /// - request: `DataRequest` for which to publish the response. + /// - queue: `DispatchQueue` on which the `DataResponse` value will be published. `.main` by default. + /// - serializer: `ResponseSerializer` used to produce the published `DataResponse`. + public init(_ request: DataRequest, queue: DispatchQueue, serializer: Serializer) + where Value == Serializer.SerializedObject { + self.request = request + responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) } + } + + /// Creates an instance which will serialize responses using the provided `DataResponseSerializerProtocol`. + /// + /// - Parameters: + /// - request: `DataRequest` for which to publish the response. + /// - queue: `DispatchQueue` on which the `DataResponse` value will be published. `.main` by default. + /// - serializer: `DataResponseSerializerProtocol` used to produce the published `DataResponse`. + public init(_ request: DataRequest, + queue: DispatchQueue, + serializer: Serializer) + where Value == Serializer.SerializedObject { + self.request = request + responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) } + } + + /// Publishes only the `Result` of the `DataResponse` value. + /// + /// - Returns: The `AnyPublisher` publishing the `Result` value. + public func result() -> AnyPublisher, Never> { + map(\.result).eraseToAnyPublisher() + } + + /// Publishes the `Result` of the `DataResponse` as a single `Value` or fail with the `AFError` instance. + /// + /// - Returns: The `AnyPublisher` publishing the stream. + public func value() -> AnyPublisher { + setFailureType(to: AFError.self).flatMap(\.result.publisher).eraseToAnyPublisher() + } + + public func receive(subscriber: S) where S: Subscriber, DataResponsePublisher.Failure == S.Failure, DataResponsePublisher.Output == S.Input { + subscriber.receive(subscription: Inner(request: request, + responseHandler: responseHandler, + downstream: subscriber)) + } + + private final class Inner: Subscription, Cancellable + where Downstream.Input == Output { + typealias Failure = Downstream.Failure + + @Protected + private var downstream: Downstream? + private let request: DataRequest + private let responseHandler: Handler + + init(request: DataRequest, responseHandler: @escaping Handler, downstream: Downstream) { + self.request = request + self.responseHandler = responseHandler + self.downstream = downstream + } + + func request(_ demand: Subscribers.Demand) { + assert(demand > 0) + + guard let downstream = downstream else { return } + + self.downstream = nil + responseHandler { response in + _ = downstream.receive(response) + downstream.receive(completion: .finished) + }.resume() + } + + func cancel() { + request.cancel() + downstream = nil + } + } +} + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +extension DataResponsePublisher where Value == Data? { + /// Creates an instance which publishes a `DataResponse` value without serialization. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public init(_ request: DataRequest, queue: DispatchQueue) { + self.request = request + responseHandler = { request.response(queue: queue, completionHandler: $0) } + } +} + +extension DataRequest { + /// Creates a `DataResponsePublisher` for this instance using the given `ResponseSerializer` and `DispatchQueue`. + /// + /// - Parameters: + /// - serializer: `ResponseSerializer` used to serialize response `Data`. + /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. + /// + /// - Returns: The `DataResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishResponse(using serializer: Serializer, on queue: DispatchQueue = .main) -> DataResponsePublisher + where Serializer.SerializedObject == T { + DataResponsePublisher(self, queue: queue, serializer: serializer) + } + + /// Creates a `DataResponsePublisher` for this instance and uses a `DataResponseSerializer` to serialize the + /// response. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. + /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()` + /// by default. + /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by + /// default. + /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless of + /// status code. `[.head]` by default. + /// - Returns: The `DataResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishData(queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) -> DataResponsePublisher { + publishResponse(using: DataResponseSerializer(dataPreprocessor: preprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + on: queue) + } + + /// Creates a `DataResponsePublisher` for this instance and uses a `StringResponseSerializer` to serialize the + /// response. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. + /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()` + /// by default. + /// - encoding: `String.Encoding` to parse the response. `nil` by default, in which case the encoding + /// will be determined by the server response, falling back to the default HTTP character + /// set, `ISO-8859-1`. + /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by + /// default. + /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless of + /// status code. `[.head]` by default. + /// + /// - Returns: The `DataResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishString(queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) -> DataResponsePublisher { + publishResponse(using: StringResponseSerializer(dataPreprocessor: preprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + on: queue) + } + + @_disfavoredOverload + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + @available(*, deprecated, message: "Renamed publishDecodable(type:queue:preprocessor:decoder:emptyResponseCodes:emptyRequestMethods).") + public func publishDecodable(type: T.Type = T.self, + queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DataResponsePublisher { + publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyResponseMethods), + on: queue) + } + + /// Creates a `DataResponsePublisher` for this instance and uses a `DecodableResponseSerializer` to serialize the + /// response. + /// + /// - Parameters: + /// - type: `Decodable` type to which to decode response `Data`. Inferred from the context by + /// default. + /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. + /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. + /// `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` instance used to decode response `Data`. `JSONDecoder()` by default. + /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by + /// default. + /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless of + /// status code. `[.head]` by default. + /// + /// - Returns: The `DataResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishDecodable(type: T.Type = T.self, + queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DataResponsePublisher { + publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + on: queue) + } + + /// Creates a `DataResponsePublisher` for this instance which does not serialize the response before publishing. + /// + /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. + /// + /// - Returns: The `DataResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishUnserialized(queue: DispatchQueue = .main) -> DataResponsePublisher { + DataResponsePublisher(self, queue: queue) + } +} + +// A Combine `Publisher` that publishes a sequence of `Stream` values received by the provided `DataStreamRequest`. +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +public struct DataStreamPublisher: Publisher { + public typealias Output = DataStreamRequest.Stream + public typealias Failure = Never + + private typealias Handler = (@escaping DataStreamRequest.Handler) -> DataStreamRequest + + private let request: DataStreamRequest + private let streamHandler: Handler + + /// Creates an instance which will serialize responses using the provided `DataStreamSerializer`. + /// + /// - Parameters: + /// - request: `DataStreamRequest` for which to publish the response. + /// - queue: `DispatchQueue` on which the `Stream` values will be published. `.main` by + /// default. + /// - serializer: `DataStreamSerializer` used to produce the published `Stream` values. + public init(_ request: DataStreamRequest, queue: DispatchQueue, serializer: Serializer) + where Value == Serializer.SerializedObject { + self.request = request + streamHandler = { request.responseStream(using: serializer, on: queue, stream: $0) } + } + + /// Publishes only the `Result` of the `DataStreamRequest.Stream`'s `Event`s. + /// + /// - Returns: The `AnyPublisher` publishing the `Result` value. + public func result() -> AnyPublisher, Never> { + compactMap { stream in + switch stream.event { + case let .stream(result): + return result + // If the stream has completed with an error, send the error value downstream as a `.failure`. + case let .complete(completion): + return completion.error.map(Result.failure) + } + } + .eraseToAnyPublisher() + } + + /// Publishes the streamed values of the `DataStreamRequest.Stream` as a sequence of `Value` or fail with the + /// `AFError` instance. + /// + /// - Returns: The `AnyPublisher` publishing the stream. + public func value() -> AnyPublisher { + result().setFailureType(to: AFError.self).flatMap(\.publisher).eraseToAnyPublisher() + } + + public func receive(subscriber: S) where S: Subscriber, DataStreamPublisher.Failure == S.Failure, DataStreamPublisher.Output == S.Input { + subscriber.receive(subscription: Inner(request: request, + streamHandler: streamHandler, + downstream: subscriber)) + } + + private final class Inner: Subscription, Cancellable + where Downstream.Input == Output { + typealias Failure = Downstream.Failure + + @Protected + private var downstream: Downstream? + private let request: DataStreamRequest + private let streamHandler: Handler + + init(request: DataStreamRequest, streamHandler: @escaping Handler, downstream: Downstream) { + self.request = request + self.streamHandler = streamHandler + self.downstream = downstream + } + + func request(_ demand: Subscribers.Demand) { + assert(demand > 0) + + guard let downstream = downstream else { return } + + self.downstream = nil + streamHandler { stream in + _ = downstream.receive(stream) + if case .complete = stream.event { + downstream.receive(completion: .finished) + } + }.resume() + } + + func cancel() { + request.cancel() + downstream = nil + } + } +} + +extension DataStreamRequest { + /// Creates a `DataStreamPublisher` for this instance using the given `DataStreamSerializer` and `DispatchQueue`. + /// + /// - Parameters: + /// - serializer: `DataStreamSerializer` used to serialize the streamed `Data`. + /// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default. + /// - Returns: The `DataStreamPublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishStream(using serializer: Serializer, + on queue: DispatchQueue = .main) -> DataStreamPublisher { + DataStreamPublisher(self, queue: queue, serializer: serializer) + } + + /// Creates a `DataStreamPublisher` for this instance which uses a `PassthroughStreamSerializer` to stream `Data` + /// unserialized. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default. + /// - Returns: The `DataStreamPublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishData(queue: DispatchQueue = .main) -> DataStreamPublisher { + publishStream(using: PassthroughStreamSerializer(), on: queue) + } + + /// Creates a `DataStreamPublisher` for this instance which uses a `StringStreamSerializer` to serialize stream + /// `Data` values into `String` values. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default. + /// - Returns: The `DataStreamPublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishString(queue: DispatchQueue = .main) -> DataStreamPublisher { + publishStream(using: StringStreamSerializer(), on: queue) + } + + /// Creates a `DataStreamPublisher` for this instance which uses a `DecodableStreamSerializer` with the provided + /// parameters to serialize stream `Data` values into the provided type. + /// + /// - Parameters: + /// - type: `Decodable` type to which to decode stream `Data`. Inferred from the context by default. + /// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default. + /// - decoder: `DataDecoder` instance used to decode stream `Data`. `JSONDecoder()` by default. + /// - preprocessor: `DataPreprocessor` which filters incoming stream `Data` before serialization. + /// `PassthroughPreprocessor()` by default. + /// - Returns: The `DataStreamPublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishDecodable(type: T.Type = T.self, + queue: DispatchQueue = .main, + decoder: DataDecoder = JSONDecoder(), + preprocessor: DataPreprocessor = PassthroughPreprocessor()) -> DataStreamPublisher { + publishStream(using: DecodableStreamSerializer(decoder: decoder, + dataPreprocessor: preprocessor), + on: queue) + } +} + +/// A Combine `Publisher` that publishes the `DownloadResponse` of the provided `DownloadRequest`. +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +public struct DownloadResponsePublisher: Publisher { + public typealias Output = DownloadResponse + public typealias Failure = Never + + private typealias Handler = (@escaping (_ response: DownloadResponse) -> Void) -> DownloadRequest + + private let request: DownloadRequest + private let responseHandler: Handler + + /// Creates an instance which will serialize responses using the provided `ResponseSerializer`. + /// + /// - Parameters: + /// - request: `DownloadRequest` for which to publish the response. + /// - queue: `DispatchQueue` on which the `DownloadResponse` value will be published. `.main` by default. + /// - serializer: `ResponseSerializer` used to produce the published `DownloadResponse`. + public init(_ request: DownloadRequest, queue: DispatchQueue, serializer: Serializer) + where Value == Serializer.SerializedObject { + self.request = request + responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) } + } + + /// Creates an instance which will serialize responses using the provided `DownloadResponseSerializerProtocol` value. + /// + /// - Parameters: + /// - request: `DownloadRequest` for which to publish the response. + /// - queue: `DispatchQueue` on which the `DataResponse` value will be published. `.main` by default. + /// - serializer: `DownloadResponseSerializerProtocol` used to produce the published `DownloadResponse`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public init(_ request: DownloadRequest, + queue: DispatchQueue, + serializer: Serializer) + where Value == Serializer.SerializedObject { + self.request = request + responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) } + } + + /// Publishes only the `Result` of the `DownloadResponse` value. + /// + /// - Returns: The `AnyPublisher` publishing the `Result` value. + public func result() -> AnyPublisher, Never> { + map(\.result).eraseToAnyPublisher() + } + + /// Publishes the `Result` of the `DownloadResponse` as a single `Value` or fail with the `AFError` instance. + /// + /// - Returns: The `AnyPublisher` publishing the stream. + public func value() -> AnyPublisher { + setFailureType(to: AFError.self).flatMap(\.result.publisher).eraseToAnyPublisher() + } + + public func receive(subscriber: S) where S: Subscriber, DownloadResponsePublisher.Failure == S.Failure, DownloadResponsePublisher.Output == S.Input { + subscriber.receive(subscription: Inner(request: request, + responseHandler: responseHandler, + downstream: subscriber)) + } + + private final class Inner: Subscription, Cancellable + where Downstream.Input == Output { + typealias Failure = Downstream.Failure + + @Protected + private var downstream: Downstream? + private let request: DownloadRequest + private let responseHandler: Handler + + init(request: DownloadRequest, responseHandler: @escaping Handler, downstream: Downstream) { + self.request = request + self.responseHandler = responseHandler + self.downstream = downstream + } + + func request(_ demand: Subscribers.Demand) { + assert(demand > 0) + + guard let downstream = downstream else { return } + + self.downstream = nil + responseHandler { response in + _ = downstream.receive(response) + downstream.receive(completion: .finished) + }.resume() + } + + func cancel() { + request.cancel() + downstream = nil + } + } +} + +extension DownloadRequest { + /// Creates a `DownloadResponsePublisher` for this instance using the given `ResponseSerializer` and `DispatchQueue`. + /// + /// - Parameters: + /// - serializer: `ResponseSerializer` used to serialize the response `Data` from disk. + /// - queue: `DispatchQueue` on which the `DownloadResponse` will be published.`.main` by default. + /// + /// - Returns: The `DownloadResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishResponse(using serializer: Serializer, on queue: DispatchQueue = .main) -> DownloadResponsePublisher + where Serializer.SerializedObject == T { + DownloadResponsePublisher(self, queue: queue, serializer: serializer) + } + + /// Creates a `DownloadResponsePublisher` for this instance using the given `DownloadResponseSerializerProtocol` and + /// `DispatchQueue`. + /// + /// - Parameters: + /// - serializer: `DownloadResponseSerializer` used to serialize the response `Data` from disk. + /// - queue: `DispatchQueue` on which the `DownloadResponse` will be published.`.main` by default. + /// + /// - Returns: The `DownloadResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishResponse(using serializer: Serializer, on queue: DispatchQueue = .main) -> DownloadResponsePublisher + where Serializer.SerializedObject == T { + DownloadResponsePublisher(self, queue: queue, serializer: serializer) + } + + /// Creates a `DownloadResponsePublisher` for this instance and uses a `URLResponseSerializer` to serialize the + /// response. + /// + /// - Parameter queue: `DispatchQueue` on which the `DownloadResponse` will be published. `.main` by default. + /// + /// - Returns: The `DownloadResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishURL(queue: DispatchQueue = .main) -> DownloadResponsePublisher { + publishResponse(using: URLResponseSerializer(), on: queue) + } + + /// Creates a `DownloadResponsePublisher` for this instance and uses a `DataResponseSerializer` to serialize the + /// response. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the `DownloadResponse` will be published. `.main` by default. + /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()` + /// by default. + /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by + /// default. + /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless of + /// status code. `[.head]` by default. + /// + /// - Returns: The `DownloadResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishData(queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) -> DownloadResponsePublisher { + publishResponse(using: DataResponseSerializer(dataPreprocessor: preprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + on: queue) + } + + /// Creates a `DownloadResponsePublisher` for this instance and uses a `StringResponseSerializer` to serialize the + /// response. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. + /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()` + /// by default. + /// - encoding: `String.Encoding` to parse the response. `nil` by default, in which case the encoding + /// will be determined by the server response, falling back to the default HTTP character + /// set, `ISO-8859-1`. + /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by + /// default. + /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless of + /// status code. `[.head]` by default. + /// + /// - Returns: The `DownloadResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishString(queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) -> DownloadResponsePublisher { + publishResponse(using: StringResponseSerializer(dataPreprocessor: preprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + on: queue) + } + + @_disfavoredOverload + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + @available(*, deprecated, message: "Renamed publishDecodable(type:queue:preprocessor:decoder:emptyResponseCodes:emptyRequestMethods).") + public func publishDecodable(type: T.Type = T.self, + queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DownloadResponsePublisher { + publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyResponseMethods), + on: queue) + } + + /// Creates a `DownloadResponsePublisher` for this instance and uses a `DecodableResponseSerializer` to serialize + /// the response. + /// + /// - Parameters: + /// - type: `Decodable` type to which to decode response `Data`. Inferred from the context by default. + /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. + /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. + /// `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` instance used to decode response `Data`. `JSONDecoder()` by default. + /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by + /// default. + /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless + /// of status code. `[.head]` by default. + /// + /// - Returns: The `DownloadResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishDecodable(type: T.Type = T.self, + queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DownloadResponsePublisher { + publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + on: queue) + } +} + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +extension DownloadResponsePublisher where Value == URL? { + /// Creates an instance which publishes a `DownloadResponse` value without serialization. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public init(_ request: DownloadRequest, queue: DispatchQueue) { + self.request = request + responseHandler = { request.response(queue: queue, completionHandler: $0) } + } +} + +extension DownloadRequest { + /// Creates a `DownloadResponsePublisher` for this instance which does not serialize the response before publishing. + /// + /// - Parameter queue: `DispatchQueue` on which the `DownloadResponse` will be published. `.main` by default. + /// + /// - Returns: The `DownloadResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishUnserialized(on queue: DispatchQueue = .main) -> DownloadResponsePublisher { + DownloadResponsePublisher(self, queue: queue) + } +} + +#endif diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/Concurrency.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/Concurrency.swift new file mode 100644 index 0000000..a5621f3 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/Concurrency.swift @@ -0,0 +1,704 @@ +// +// Concurrency.swift +// +// Copyright (c) 2021 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if compiler(>=5.6.0) && canImport(_Concurrency) + +import Foundation + +// MARK: - Request Event Streams + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Request { + /// Creates a `StreamOf` for the instance's upload progress. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func uploadProgress(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + uploadProgress(queue: .singleEventQueue) { progress in + continuation.yield(progress) + } + } + } + + /// Creates a `StreamOf` for the instance's download progress. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func downloadProgress(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + downloadProgress(queue: .singleEventQueue) { progress in + continuation.yield(progress) + } + } + } + + /// Creates a `StreamOf` for the `URLRequest`s produced for the instance. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func urlRequests(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + onURLRequestCreation(on: .singleEventQueue) { request in + continuation.yield(request) + } + } + } + + /// Creates a `StreamOf` for the `URLSessionTask`s produced for the instance. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func urlSessionTasks(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + onURLSessionTaskCreation(on: .singleEventQueue) { task in + continuation.yield(task) + } + } + } + + /// Creates a `StreamOf` for the cURL descriptions produced for the instance. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func cURLDescriptions(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + cURLDescription(on: .singleEventQueue) { description in + continuation.yield(description) + } + } + } + + private func stream(of type: T.Type = T.self, + bufferingPolicy: StreamOf.BufferingPolicy = .unbounded, + yielder: @escaping (StreamOf.Continuation) -> Void) -> StreamOf { + StreamOf(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + yielder(continuation) + // Must come after serializers run in order to catch retry progress. + onFinish { + continuation.finish() + } + } + } +} + +// MARK: - DataTask + +/// Value used to `await` a `DataResponse` and associated values. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct DataTask { + /// `DataResponse` produced by the `DataRequest` and its response handler. + public var response: DataResponse { + get async { + if shouldAutomaticallyCancel { + return await withTaskCancellationHandler { + self.cancel() + } operation: { + await task.value + } + } else { + return await task.value + } + } + } + + /// `Result` of any response serialization performed for the `response`. + public var result: Result { + get async { await response.result } + } + + /// `Value` returned by the `response`. + public var value: Value { + get async throws { + try await result.get() + } + } + + private let request: DataRequest + private let task: Task, Never> + private let shouldAutomaticallyCancel: Bool + + fileprivate init(request: DataRequest, task: Task, Never>, shouldAutomaticallyCancel: Bool) { + self.request = request + self.task = task + self.shouldAutomaticallyCancel = shouldAutomaticallyCancel + } + + /// Cancel the underlying `DataRequest` and `Task`. + public func cancel() { + task.cancel() + } + + /// Resume the underlying `DataRequest`. + public func resume() { + request.resume() + } + + /// Suspend the underlying `DataRequest`. + public func suspend() { + request.suspend() + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension DataRequest { + /// Creates a `DataTask` to `await` a `Data` value. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before completion. + /// - emptyResponseCodes: HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DataTask`. + public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) -> DataTask { + serializingResponse(using: DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DataTask` to `await` serialization of a `Decodable` value. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer. + /// `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DataTask`. + public func serializingDecodable(_ type: Value.Type = Value.self, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DataTask { + serializingResponse(using: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DataTask` to `await` serialization of a `String` value. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer. + /// `PassthroughPreprocessor()` by default. + /// - encoding: `String.Encoding` to use during serialization. Defaults to `nil`, in which case + /// the encoding will be determined from the server response, falling back to the + /// default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DataTask`. + public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) -> DataTask { + serializingResponse(using: StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DataTask` to `await` serialization using the provided `ResponseSerializer` instance. + /// + /// - Parameters: + /// - serializer: `ResponseSerializer` responsible for serializing the request, response, and data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// + /// - Returns: The `DataTask`. + public func serializingResponse(using serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false) + -> DataTask { + dataTask(automaticallyCancelling: shouldAutomaticallyCancel) { + self.response(queue: .singleEventQueue, + responseSerializer: serializer, + completionHandler: $0) + } + } + + /// Creates a `DataTask` to `await` serialization using the provided `DataResponseSerializerProtocol` instance. + /// + /// - Parameters: + /// - serializer: `DataResponseSerializerProtocol` responsible for serializing the request, + /// response, and data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// + /// - Returns: The `DataTask`. + public func serializingResponse(using serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false) + -> DataTask { + dataTask(automaticallyCancelling: shouldAutomaticallyCancel) { + self.response(queue: .singleEventQueue, + responseSerializer: serializer, + completionHandler: $0) + } + } + + private func dataTask(automaticallyCancelling shouldAutomaticallyCancel: Bool, + forResponse onResponse: @escaping (@escaping (DataResponse) -> Void) -> Void) + -> DataTask { + let task = Task { + await withTaskCancellationHandler { + self.cancel() + } operation: { + await withCheckedContinuation { continuation in + onResponse { + continuation.resume(returning: $0) + } + } + } + } + + return DataTask(request: self, task: task, shouldAutomaticallyCancel: shouldAutomaticallyCancel) + } +} + +// MARK: - DownloadTask + +/// Value used to `await` a `DownloadResponse` and associated values. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct DownloadTask { + /// `DownloadResponse` produced by the `DownloadRequest` and its response handler. + public var response: DownloadResponse { + get async { + if shouldAutomaticallyCancel { + return await withTaskCancellationHandler { + self.cancel() + } operation: { + await task.value + } + } else { + return await task.value + } + } + } + + /// `Result` of any response serialization performed for the `response`. + public var result: Result { + get async { await response.result } + } + + /// `Value` returned by the `response`. + public var value: Value { + get async throws { + try await result.get() + } + } + + private let task: Task, Never> + private let request: DownloadRequest + private let shouldAutomaticallyCancel: Bool + + fileprivate init(request: DownloadRequest, task: Task, Never>, shouldAutomaticallyCancel: Bool) { + self.request = request + self.task = task + self.shouldAutomaticallyCancel = shouldAutomaticallyCancel + } + + /// Cancel the underlying `DownloadRequest` and `Task`. + public func cancel() { + task.cancel() + } + + /// Resume the underlying `DownloadRequest`. + public func resume() { + request.resume() + } + + /// Suspend the underlying `DownloadRequest`. + public func suspend() { + request.suspend() + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension DownloadRequest { + /// Creates a `DownloadTask` to `await` a `Data` value. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before completion. + /// - emptyResponseCodes: HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask { + serializingDownload(using: DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DownloadTask` to `await` serialization of a `Decodable` value. + /// + /// - Note: This serializer reads the entire response into memory before parsing. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer. + /// `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingDecodable(_ type: Value.Type = Value.self, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask { + serializingDownload(using: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DownloadTask` to `await` serialization of the downloaded file's `URL` on disk. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingDownloadedFileURL(automaticallyCancelling shouldAutomaticallyCancel: Bool = false) -> DownloadTask { + serializingDownload(using: URLResponseSerializer(), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DownloadTask` to `await` serialization of a `String` value. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// serializer. `PassthroughPreprocessor()` by default. + /// - encoding: `String.Encoding` to use during serialization. Defaults to `nil`, in which case + /// the encoding will be determined from the server response, falling back to the + /// default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask { + serializingDownload(using: StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DownloadTask` to `await` serialization using the provided `ResponseSerializer` instance. + /// + /// - Parameters: + /// - serializer: `ResponseSerializer` responsible for serializing the request, response, and data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingDownload(using serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false) + -> DownloadTask { + downloadTask(automaticallyCancelling: shouldAutomaticallyCancel) { + self.response(queue: .singleEventQueue, + responseSerializer: serializer, + completionHandler: $0) + } + } + + /// Creates a `DownloadTask` to `await` serialization using the provided `DownloadResponseSerializerProtocol` + /// instance. + /// + /// - Parameters: + /// - serializer: `DownloadResponseSerializerProtocol` responsible for serializing the request, + /// response, and data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingDownload(using serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false) + -> DownloadTask { + downloadTask(automaticallyCancelling: shouldAutomaticallyCancel) { + self.response(queue: .singleEventQueue, + responseSerializer: serializer, + completionHandler: $0) + } + } + + private func downloadTask(automaticallyCancelling shouldAutomaticallyCancel: Bool, + forResponse onResponse: @escaping (@escaping (DownloadResponse) -> Void) -> Void) + -> DownloadTask { + let task = Task { + await withTaskCancellationHandler { + self.cancel() + } operation: { + await withCheckedContinuation { continuation in + onResponse { + continuation.resume(returning: $0) + } + } + } + } + + return DownloadTask(request: self, task: task, shouldAutomaticallyCancel: shouldAutomaticallyCancel) + } +} + +// MARK: - DataStreamTask + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct DataStreamTask { + // Type of created streams. + public typealias Stream = StreamOf> + + private let request: DataStreamRequest + + fileprivate init(request: DataStreamRequest) { + self.request = request + } + + /// Creates a `Stream` of `Data` values from the underlying `DataStreamRequest`. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled + /// which observation of the stream stops. `true` by default. + /// - bufferingPolicy: ` BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `Stream`. + public func streamingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, bufferingPolicy: Stream.BufferingPolicy = .unbounded) -> Stream { + createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in + self.request.responseStream(on: .streamCompletionQueue(forRequestID: request.id), stream: onStream) + } + } + + /// Creates a `Stream` of `UTF-8` `String`s from the underlying `DataStreamRequest`. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled + /// which observation of the stream stops. `true` by default. + /// - bufferingPolicy: ` BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// - Returns: + public func streamingStrings(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, bufferingPolicy: Stream.BufferingPolicy = .unbounded) -> Stream { + createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in + self.request.responseStreamString(on: .streamCompletionQueue(forRequestID: request.id), stream: onStream) + } + } + + /// Creates a `Stream` of `Decodable` values from the underlying `DataStreamRequest`. + /// + /// - Parameters: + /// - type: `Decodable` type to be serialized from stream payloads. + /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled + /// which observation of the stream stops. `true` by default. + /// - bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `Stream`. + public func streamingDecodables(_ type: T.Type = T.self, + automaticallyCancelling shouldAutomaticallyCancel: Bool = true, + bufferingPolicy: Stream.BufferingPolicy = .unbounded) + -> Stream where T: Decodable { + streamingResponses(serializedUsing: DecodableStreamSerializer(), + automaticallyCancelling: shouldAutomaticallyCancel, + bufferingPolicy: bufferingPolicy) + } + + /// Creates a `Stream` of values using the provided `DataStreamSerializer` from the underlying `DataStreamRequest`. + /// + /// - Parameters: + /// - serializer: `DataStreamSerializer` to use to serialize incoming `Data`. + /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled + /// which observation of the stream stops. `true` by default. + /// - bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `Stream`. + public func streamingResponses(serializedUsing serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = true, + bufferingPolicy: Stream.BufferingPolicy = .unbounded) + -> Stream { + createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in + self.request.responseStream(using: serializer, + on: .streamCompletionQueue(forRequestID: request.id), + stream: onStream) + } + } + + private func createStream(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, + bufferingPolicy: Stream.BufferingPolicy = .unbounded, + forResponse onResponse: @escaping (@escaping (DataStreamRequest.Stream) -> Void) -> Void) + -> Stream { + StreamOf(bufferingPolicy: bufferingPolicy) { + guard shouldAutomaticallyCancel, + request.isInitialized || request.isResumed || request.isSuspended else { return } + + cancel() + } builder: { continuation in + onResponse { stream in + continuation.yield(stream) + if case .complete = stream.event { + continuation.finish() + } + } + } + } + + /// Cancel the underlying `DataStreamRequest`. + public func cancel() { + request.cancel() + } + + /// Resume the underlying `DataStreamRequest`. + public func resume() { + request.resume() + } + + /// Suspend the underlying `DataStreamRequest`. + public func suspend() { + request.suspend() + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension DataStreamRequest { + /// Creates a `DataStreamTask` used to `await` streams of serialized values. + /// + /// - Returns: The `DataStreamTask`. + public func streamTask() -> DataStreamTask { + DataStreamTask(request: self) + } +} + +extension DispatchQueue { + fileprivate static let singleEventQueue = DispatchQueue(label: "org.alamofire.concurrencySingleEventQueue", + attributes: .concurrent) + + fileprivate static func streamCompletionQueue(forRequestID id: UUID) -> DispatchQueue { + DispatchQueue(label: "org.alamofire.concurrencyStreamCompletionQueue-\(id)", target: .singleEventQueue) + } +} + +/// An asynchronous sequence generated from an underlying `AsyncStream`. Only produced by Alamofire. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct StreamOf: AsyncSequence { + public typealias AsyncIterator = Iterator + public typealias BufferingPolicy = AsyncStream.Continuation.BufferingPolicy + fileprivate typealias Continuation = AsyncStream.Continuation + + private let bufferingPolicy: BufferingPolicy + private let onTermination: (() -> Void)? + private let builder: (Continuation) -> Void + + fileprivate init(bufferingPolicy: BufferingPolicy = .unbounded, + onTermination: (() -> Void)? = nil, + builder: @escaping (Continuation) -> Void) { + self.bufferingPolicy = bufferingPolicy + self.onTermination = onTermination + self.builder = builder + } + + public func makeAsyncIterator() -> Iterator { + var continuation: AsyncStream.Continuation? + let stream = AsyncStream { innerContinuation in + continuation = innerContinuation + builder(innerContinuation) + } + + return Iterator(iterator: stream.makeAsyncIterator()) { + continuation?.finish() + self.onTermination?() + } + } + + public struct Iterator: AsyncIteratorProtocol { + private final class Token { + private let onDeinit: () -> Void + + init(onDeinit: @escaping () -> Void) { + self.onDeinit = onDeinit + } + + deinit { + onDeinit() + } + } + + private var iterator: AsyncStream.AsyncIterator + private let token: Token + + init(iterator: AsyncStream.AsyncIterator, onCancellation: @escaping () -> Void) { + self.iterator = iterator + token = Token(onDeinit: onCancellation) + } + + public mutating func next() async -> Element? { + await iterator.next() + } + } +} + +#endif diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/DispatchQueue+Alamofire.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/DispatchQueue+Alamofire.swift new file mode 100644 index 0000000..10cd273 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/DispatchQueue+Alamofire.swift @@ -0,0 +1,37 @@ +// +// DispatchQueue+Alamofire.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Dispatch +import Foundation + +extension DispatchQueue { + /// Execute the provided closure after a `TimeInterval`. + /// + /// - Parameters: + /// - delay: `TimeInterval` to delay execution. + /// - closure: Closure to execute. + func after(_ delay: TimeInterval, execute closure: @escaping () -> Void) { + asyncAfter(deadline: .now() + delay, execute: closure) + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/EventMonitor.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/EventMonitor.swift new file mode 100644 index 0000000..3b09671 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/EventMonitor.swift @@ -0,0 +1,892 @@ +// +// EventMonitor.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Protocol outlining the lifetime events inside Alamofire. It includes both events received from the various +/// `URLSession` delegate protocols as well as various events from the lifetime of `Request` and its subclasses. +public protocol EventMonitor { + /// The `DispatchQueue` onto which Alamofire's root `CompositeEventMonitor` will dispatch events. `.main` by default. + var queue: DispatchQueue { get } + + // MARK: - URLSession Events + + // MARK: URLSessionDelegate Events + + /// Event called during `URLSessionDelegate`'s `urlSession(_:didBecomeInvalidWithError:)` method. + func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) + + // MARK: URLSessionTaskDelegate Events + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didReceive:completionHandler:)` method. + func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` method. + func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:needNewBodyStream:)` method. + func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` method. + func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didFinishCollecting:)` method. + func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didCompleteWithError:)` method. + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:taskIsWaitingForConnectivity:)` method. + @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) + func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) + + // MARK: URLSessionDataDelegate Events + + /// Event called during `URLSessionDataDelegate`'s `urlSession(_:dataTask:didReceive:)` method. + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) + + /// Event called during `URLSessionDataDelegate`'s `urlSession(_:dataTask:willCacheResponse:completionHandler:)` method. + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse) + + // MARK: URLSessionDownloadDelegate Events + + /// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)` method. + func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) + + /// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)` method. + func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) + + /// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didFinishDownloadingTo:)` method. + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) + + // MARK: - Request Events + + /// Event called when a `URLRequest` is first created for a `Request`. If a `RequestAdapter` is active, the + /// `URLRequest` will be adapted before being issued. + func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) + + /// Event called when the attempt to create a `URLRequest` from a `Request`'s original `URLRequestConvertible` value fails. + func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) + + /// Event called when a `RequestAdapter` adapts the `Request`'s initial `URLRequest`. + func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) + + /// Event called when a `RequestAdapter` fails to adapt the `Request`'s initial `URLRequest`. + func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: AFError) + + /// Event called when a final `URLRequest` is created for a `Request`. + func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) + + /// Event called when a `URLSessionTask` subclass instance is created for a `Request`. + func request(_ request: Request, didCreateTask task: URLSessionTask) + + /// Event called when a `Request` receives a `URLSessionTaskMetrics` value. + func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) + + /// Event called when a `Request` fails due to an error created by Alamofire. e.g. When certificate pinning fails. + func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) + + /// Event called when a `Request`'s task completes, possibly with an error. A `Request` may receive this event + /// multiple times if it is retried. + func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) + + /// Event called when a `Request` is about to be retried. + func requestIsRetrying(_ request: Request) + + /// Event called when a `Request` finishes and response serializers are being called. + func requestDidFinish(_ request: Request) + + /// Event called when a `Request` receives a `resume` call. + func requestDidResume(_ request: Request) + + /// Event called when a `Request`'s associated `URLSessionTask` is resumed. + func request(_ request: Request, didResumeTask task: URLSessionTask) + + /// Event called when a `Request` receives a `suspend` call. + func requestDidSuspend(_ request: Request) + + /// Event called when a `Request`'s associated `URLSessionTask` is suspended. + func request(_ request: Request, didSuspendTask task: URLSessionTask) + + /// Event called when a `Request` receives a `cancel` call. + func requestDidCancel(_ request: Request) + + /// Event called when a `Request`'s associated `URLSessionTask` is cancelled. + func request(_ request: Request, didCancelTask task: URLSessionTask) + + // MARK: DataRequest Events + + /// Event called when a `DataRequest` calls a `Validation`. + func request(_ request: DataRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + data: Data?, + withResult result: Request.ValidationResult) + + /// Event called when a `DataRequest` creates a `DataResponse` value without calling a `ResponseSerializer`. + func request(_ request: DataRequest, didParseResponse response: DataResponse) + + /// Event called when a `DataRequest` calls a `ResponseSerializer` and creates a generic `DataResponse`. + func request(_ request: DataRequest, didParseResponse response: DataResponse) + + // MARK: DataStreamRequest Events + + /// Event called when a `DataStreamRequest` calls a `Validation` closure. + /// + /// - Parameters: + /// - request: `DataStreamRequest` which is calling the `Validation`. + /// - urlRequest: `URLRequest` of the request being validated. + /// - response: `HTTPURLResponse` of the request being validated. + /// - result: Produced `ValidationResult`. + func request(_ request: DataStreamRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + withResult result: Request.ValidationResult) + + /// Event called when a `DataStreamSerializer` produces a value from streamed `Data`. + /// + /// - Parameters: + /// - request: `DataStreamRequest` for which the value was serialized. + /// - result: `Result` of the serialization attempt. + func request(_ request: DataStreamRequest, didParseStream result: Result) + + // MARK: UploadRequest Events + + /// Event called when an `UploadRequest` creates its `Uploadable` value, indicating the type of upload it represents. + func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) + + /// Event called when an `UploadRequest` failed to create its `Uploadable` value due to an error. + func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) + + /// Event called when an `UploadRequest` provides the `InputStream` from its `Uploadable` value. This only occurs if + /// the `InputStream` does not wrap a `Data` value or file `URL`. + func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) + + // MARK: DownloadRequest Events + + /// Event called when a `DownloadRequest`'s `URLSessionDownloadTask` finishes and the temporary file has been moved. + func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result) + + /// Event called when a `DownloadRequest`'s `Destination` closure is called and creates the destination URL the + /// downloaded file will be moved to. + func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) + + /// Event called when a `DownloadRequest` calls a `Validation`. + func request(_ request: DownloadRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + fileURL: URL?, + withResult result: Request.ValidationResult) + + /// Event called when a `DownloadRequest` creates a `DownloadResponse` without calling a `ResponseSerializer`. + func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) + + /// Event called when a `DownloadRequest` calls a `DownloadResponseSerializer` and creates a generic `DownloadResponse` + func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) +} + +extension EventMonitor { + /// The default queue on which `CompositeEventMonitor`s will call the `EventMonitor` methods. `.main` by default. + public var queue: DispatchQueue { .main } + + // MARK: Default Implementations + + public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {} + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge) {} + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) {} + public func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) {} + public func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest) {} + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didFinishCollecting metrics: URLSessionTaskMetrics) {} + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {} + public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {} + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {} + public func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + willCacheResponse proposedResponse: CachedURLResponse) {} + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) {} + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) {} + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didFinishDownloadingTo location: URL) {} + public func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) {} + public func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) {} + public func request(_ request: Request, + didAdaptInitialRequest initialRequest: URLRequest, + to adaptedRequest: URLRequest) {} + public func request(_ request: Request, + didFailToAdaptURLRequest initialRequest: URLRequest, + withError error: AFError) {} + public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) {} + public func request(_ request: Request, didCreateTask task: URLSessionTask) {} + public func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) {} + public func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) {} + public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) {} + public func requestIsRetrying(_ request: Request) {} + public func requestDidFinish(_ request: Request) {} + public func requestDidResume(_ request: Request) {} + public func request(_ request: Request, didResumeTask task: URLSessionTask) {} + public func requestDidSuspend(_ request: Request) {} + public func request(_ request: Request, didSuspendTask task: URLSessionTask) {} + public func requestDidCancel(_ request: Request) {} + public func request(_ request: Request, didCancelTask task: URLSessionTask) {} + public func request(_ request: DataRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + data: Data?, + withResult result: Request.ValidationResult) {} + public func request(_ request: DataRequest, didParseResponse response: DataResponse) {} + public func request(_ request: DataRequest, didParseResponse response: DataResponse) {} + public func request(_ request: DataStreamRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + withResult result: Request.ValidationResult) {} + public func request(_ request: DataStreamRequest, didParseStream result: Result) {} + public func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) {} + public func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) {} + public func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) {} + public func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result) {} + public func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) {} + public func request(_ request: DownloadRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + fileURL: URL?, + withResult result: Request.ValidationResult) {} + public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) {} + public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) {} +} + +/// An `EventMonitor` which can contain multiple `EventMonitor`s and calls their methods on their queues. +public final class CompositeEventMonitor: EventMonitor { + public let queue = DispatchQueue(label: "org.alamofire.compositeEventMonitor", qos: .utility) + + let monitors: [EventMonitor] + + init(monitors: [EventMonitor]) { + self.monitors = monitors + } + + func performEvent(_ event: @escaping (EventMonitor) -> Void) { + queue.async { + for monitor in self.monitors { + monitor.queue.async { event(monitor) } + } + } + } + + public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + performEvent { $0.urlSession(session, didBecomeInvalidWithError: error) } + } + + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge) { + performEvent { $0.urlSession(session, task: task, didReceive: challenge) } + } + + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) { + performEvent { + $0.urlSession(session, + task: task, + didSendBodyData: bytesSent, + totalBytesSent: totalBytesSent, + totalBytesExpectedToSend: totalBytesExpectedToSend) + } + } + + public func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) { + performEvent { + $0.urlSession(session, taskNeedsNewBodyStream: task) + } + } + + public func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest) { + performEvent { + $0.urlSession(session, + task: task, + willPerformHTTPRedirection: response, + newRequest: request) + } + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + performEvent { $0.urlSession(session, task: task, didFinishCollecting: metrics) } + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + performEvent { $0.urlSession(session, task: task, didCompleteWithError: error) } + } + + @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) + public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { + performEvent { $0.urlSession(session, taskIsWaitingForConnectivity: task) } + } + + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + performEvent { $0.urlSession(session, dataTask: dataTask, didReceive: data) } + } + + public func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + willCacheResponse proposedResponse: CachedURLResponse) { + performEvent { $0.urlSession(session, dataTask: dataTask, willCacheResponse: proposedResponse) } + } + + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) { + performEvent { + $0.urlSession(session, + downloadTask: downloadTask, + didResumeAtOffset: fileOffset, + expectedTotalBytes: expectedTotalBytes) + } + } + + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) { + performEvent { + $0.urlSession(session, + downloadTask: downloadTask, + didWriteData: bytesWritten, + totalBytesWritten: totalBytesWritten, + totalBytesExpectedToWrite: totalBytesExpectedToWrite) + } + } + + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didFinishDownloadingTo location: URL) { + performEvent { $0.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location) } + } + + public func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) { + performEvent { $0.request(request, didCreateInitialURLRequest: urlRequest) } + } + + public func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) { + performEvent { $0.request(request, didFailToCreateURLRequestWithError: error) } + } + + public func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) { + performEvent { $0.request(request, didAdaptInitialRequest: initialRequest, to: adaptedRequest) } + } + + public func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: AFError) { + performEvent { $0.request(request, didFailToAdaptURLRequest: initialRequest, withError: error) } + } + + public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) { + performEvent { $0.request(request, didCreateURLRequest: urlRequest) } + } + + public func request(_ request: Request, didCreateTask task: URLSessionTask) { + performEvent { $0.request(request, didCreateTask: task) } + } + + public func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) { + performEvent { $0.request(request, didGatherMetrics: metrics) } + } + + public func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) { + performEvent { $0.request(request, didFailTask: task, earlyWithError: error) } + } + + public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) { + performEvent { $0.request(request, didCompleteTask: task, with: error) } + } + + public func requestIsRetrying(_ request: Request) { + performEvent { $0.requestIsRetrying(request) } + } + + public func requestDidFinish(_ request: Request) { + performEvent { $0.requestDidFinish(request) } + } + + public func requestDidResume(_ request: Request) { + performEvent { $0.requestDidResume(request) } + } + + public func request(_ request: Request, didResumeTask task: URLSessionTask) { + performEvent { $0.request(request, didResumeTask: task) } + } + + public func requestDidSuspend(_ request: Request) { + performEvent { $0.requestDidSuspend(request) } + } + + public func request(_ request: Request, didSuspendTask task: URLSessionTask) { + performEvent { $0.request(request, didSuspendTask: task) } + } + + public func requestDidCancel(_ request: Request) { + performEvent { $0.requestDidCancel(request) } + } + + public func request(_ request: Request, didCancelTask task: URLSessionTask) { + performEvent { $0.request(request, didCancelTask: task) } + } + + public func request(_ request: DataRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + data: Data?, + withResult result: Request.ValidationResult) { + performEvent { $0.request(request, + didValidateRequest: urlRequest, + response: response, + data: data, + withResult: result) + } + } + + public func request(_ request: DataRequest, didParseResponse response: DataResponse) { + performEvent { $0.request(request, didParseResponse: response) } + } + + public func request(_ request: DataRequest, didParseResponse response: DataResponse) { + performEvent { $0.request(request, didParseResponse: response) } + } + + public func request(_ request: DataStreamRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + withResult result: Request.ValidationResult) { + performEvent { $0.request(request, + didValidateRequest: urlRequest, + response: response, + withResult: result) + } + } + + public func request(_ request: DataStreamRequest, didParseStream result: Result) { + performEvent { $0.request(request, didParseStream: result) } + } + + public func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) { + performEvent { $0.request(request, didCreateUploadable: uploadable) } + } + + public func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) { + performEvent { $0.request(request, didFailToCreateUploadableWithError: error) } + } + + public func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) { + performEvent { $0.request(request, didProvideInputStream: stream) } + } + + public func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result) { + performEvent { $0.request(request, didFinishDownloadingUsing: task, with: result) } + } + + public func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) { + performEvent { $0.request(request, didCreateDestinationURL: url) } + } + + public func request(_ request: DownloadRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + fileURL: URL?, + withResult result: Request.ValidationResult) { + performEvent { $0.request(request, + didValidateRequest: urlRequest, + response: response, + fileURL: fileURL, + withResult: result) } + } + + public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { + performEvent { $0.request(request, didParseResponse: response) } + } + + public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { + performEvent { $0.request(request, didParseResponse: response) } + } +} + +/// `EventMonitor` that allows optional closures to be set to receive events. +open class ClosureEventMonitor: EventMonitor { + /// Closure called on the `urlSession(_:didBecomeInvalidWithError:)` event. + open var sessionDidBecomeInvalidWithError: ((URLSession, Error?) -> Void)? + + /// Closure called on the `urlSession(_:task:didReceive:completionHandler:)`. + open var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> Void)? + + /// Closure that receives `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` event. + open var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)? + + /// Closure called on the `urlSession(_:task:needNewBodyStream:)` event. + open var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> Void)? + + /// Closure called on the `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` event. + open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> Void)? + + /// Closure called on the `urlSession(_:task:didFinishCollecting:)` event. + open var taskDidFinishCollectingMetrics: ((URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void)? + + /// Closure called on the `urlSession(_:task:didCompleteWithError:)` event. + open var taskDidComplete: ((URLSession, URLSessionTask, Error?) -> Void)? + + /// Closure called on the `urlSession(_:taskIsWaitingForConnectivity:)` event. + open var taskIsWaitingForConnectivity: ((URLSession, URLSessionTask) -> Void)? + + /// Closure that receives the `urlSession(_:dataTask:didReceive:)` event. + open var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)? + + /// Closure called on the `urlSession(_:dataTask:willCacheResponse:completionHandler:)` event. + open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> Void)? + + /// Closure called on the `urlSession(_:downloadTask:didFinishDownloadingTo:)` event. + open var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> Void)? + + /// Closure called on the `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)` + /// event. + open var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? + + /// Closure called on the `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)` event. + open var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)? + + // MARK: - Request Events + + /// Closure called on the `request(_:didCreateInitialURLRequest:)` event. + open var requestDidCreateInitialURLRequest: ((Request, URLRequest) -> Void)? + + /// Closure called on the `request(_:didFailToCreateURLRequestWithError:)` event. + open var requestDidFailToCreateURLRequestWithError: ((Request, AFError) -> Void)? + + /// Closure called on the `request(_:didAdaptInitialRequest:to:)` event. + open var requestDidAdaptInitialRequestToAdaptedRequest: ((Request, URLRequest, URLRequest) -> Void)? + + /// Closure called on the `request(_:didFailToAdaptURLRequest:withError:)` event. + open var requestDidFailToAdaptURLRequestWithError: ((Request, URLRequest, AFError) -> Void)? + + /// Closure called on the `request(_:didCreateURLRequest:)` event. + open var requestDidCreateURLRequest: ((Request, URLRequest) -> Void)? + + /// Closure called on the `request(_:didCreateTask:)` event. + open var requestDidCreateTask: ((Request, URLSessionTask) -> Void)? + + /// Closure called on the `request(_:didGatherMetrics:)` event. + open var requestDidGatherMetrics: ((Request, URLSessionTaskMetrics) -> Void)? + + /// Closure called on the `request(_:didFailTask:earlyWithError:)` event. + open var requestDidFailTaskEarlyWithError: ((Request, URLSessionTask, AFError) -> Void)? + + /// Closure called on the `request(_:didCompleteTask:with:)` event. + open var requestDidCompleteTaskWithError: ((Request, URLSessionTask, AFError?) -> Void)? + + /// Closure called on the `requestIsRetrying(_:)` event. + open var requestIsRetrying: ((Request) -> Void)? + + /// Closure called on the `requestDidFinish(_:)` event. + open var requestDidFinish: ((Request) -> Void)? + + /// Closure called on the `requestDidResume(_:)` event. + open var requestDidResume: ((Request) -> Void)? + + /// Closure called on the `request(_:didResumeTask:)` event. + open var requestDidResumeTask: ((Request, URLSessionTask) -> Void)? + + /// Closure called on the `requestDidSuspend(_:)` event. + open var requestDidSuspend: ((Request) -> Void)? + + /// Closure called on the `request(_:didSuspendTask:)` event. + open var requestDidSuspendTask: ((Request, URLSessionTask) -> Void)? + + /// Closure called on the `requestDidCancel(_:)` event. + open var requestDidCancel: ((Request) -> Void)? + + /// Closure called on the `request(_:didCancelTask:)` event. + open var requestDidCancelTask: ((Request, URLSessionTask) -> Void)? + + /// Closure called on the `request(_:didValidateRequest:response:data:withResult:)` event. + open var requestDidValidateRequestResponseDataWithResult: ((DataRequest, URLRequest?, HTTPURLResponse, Data?, Request.ValidationResult) -> Void)? + + /// Closure called on the `request(_:didParseResponse:)` event. + open var requestDidParseResponse: ((DataRequest, DataResponse) -> Void)? + + /// Closure called on the `request(_:didValidateRequest:response:withResult:)` event. + open var requestDidValidateRequestResponseWithResult: ((DataStreamRequest, URLRequest?, HTTPURLResponse, Request.ValidationResult) -> Void)? + + /// Closure called on the `request(_:didCreateUploadable:)` event. + open var requestDidCreateUploadable: ((UploadRequest, UploadRequest.Uploadable) -> Void)? + + /// Closure called on the `request(_:didFailToCreateUploadableWithError:)` event. + open var requestDidFailToCreateUploadableWithError: ((UploadRequest, AFError) -> Void)? + + /// Closure called on the `request(_:didProvideInputStream:)` event. + open var requestDidProvideInputStream: ((UploadRequest, InputStream) -> Void)? + + /// Closure called on the `request(_:didFinishDownloadingUsing:with:)` event. + open var requestDidFinishDownloadingUsingTaskWithResult: ((DownloadRequest, URLSessionTask, Result) -> Void)? + + /// Closure called on the `request(_:didCreateDestinationURL:)` event. + open var requestDidCreateDestinationURL: ((DownloadRequest, URL) -> Void)? + + /// Closure called on the `request(_:didValidateRequest:response:temporaryURL:destinationURL:withResult:)` event. + open var requestDidValidateRequestResponseFileURLWithResult: ((DownloadRequest, URLRequest?, HTTPURLResponse, URL?, Request.ValidationResult) -> Void)? + + /// Closure called on the `request(_:didParseResponse:)` event. + open var requestDidParseDownloadResponse: ((DownloadRequest, DownloadResponse) -> Void)? + + public let queue: DispatchQueue + + /// Creates an instance using the provided queue. + /// + /// - Parameter queue: `DispatchQueue` on which events will fired. `.main` by default. + public init(queue: DispatchQueue = .main) { + self.queue = queue + } + + open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + sessionDidBecomeInvalidWithError?(session, error) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) { + taskDidReceiveChallenge?(session, task, challenge) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) { + taskDidSendBodyData?(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend) + } + + open func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) { + taskNeedNewBodyStream?(session, task) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest) { + taskWillPerformHTTPRedirection?(session, task, response, request) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + taskDidFinishCollectingMetrics?(session, task, metrics) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + taskDidComplete?(session, task, error) + } + + open func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { + taskIsWaitingForConnectivity?(session, task) + } + + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + dataTaskDidReceiveData?(session, dataTask, data) + } + + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse) { + dataTaskWillCacheResponse?(session, dataTask, proposedResponse) + } + + open func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) { + downloadTaskDidResumeAtOffset?(session, downloadTask, fileOffset, expectedTotalBytes) + } + + open func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) { + downloadTaskDidWriteData?(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) + } + + open func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + downloadTaskDidFinishDownloadingToURL?(session, downloadTask, location) + } + + // MARK: Request Events + + open func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) { + requestDidCreateInitialURLRequest?(request, urlRequest) + } + + open func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) { + requestDidFailToCreateURLRequestWithError?(request, error) + } + + open func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) { + requestDidAdaptInitialRequestToAdaptedRequest?(request, initialRequest, adaptedRequest) + } + + open func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: AFError) { + requestDidFailToAdaptURLRequestWithError?(request, initialRequest, error) + } + + open func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) { + requestDidCreateURLRequest?(request, urlRequest) + } + + open func request(_ request: Request, didCreateTask task: URLSessionTask) { + requestDidCreateTask?(request, task) + } + + open func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) { + requestDidGatherMetrics?(request, metrics) + } + + open func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) { + requestDidFailTaskEarlyWithError?(request, task, error) + } + + open func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) { + requestDidCompleteTaskWithError?(request, task, error) + } + + open func requestIsRetrying(_ request: Request) { + requestIsRetrying?(request) + } + + open func requestDidFinish(_ request: Request) { + requestDidFinish?(request) + } + + open func requestDidResume(_ request: Request) { + requestDidResume?(request) + } + + public func request(_ request: Request, didResumeTask task: URLSessionTask) { + requestDidResumeTask?(request, task) + } + + open func requestDidSuspend(_ request: Request) { + requestDidSuspend?(request) + } + + public func request(_ request: Request, didSuspendTask task: URLSessionTask) { + requestDidSuspendTask?(request, task) + } + + open func requestDidCancel(_ request: Request) { + requestDidCancel?(request) + } + + public func request(_ request: Request, didCancelTask task: URLSessionTask) { + requestDidCancelTask?(request, task) + } + + open func request(_ request: DataRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + data: Data?, + withResult result: Request.ValidationResult) { + requestDidValidateRequestResponseDataWithResult?(request, urlRequest, response, data, result) + } + + open func request(_ request: DataRequest, didParseResponse response: DataResponse) { + requestDidParseResponse?(request, response) + } + + public func request(_ request: DataStreamRequest, didValidateRequest urlRequest: URLRequest?, response: HTTPURLResponse, withResult result: Request.ValidationResult) { + requestDidValidateRequestResponseWithResult?(request, urlRequest, response, result) + } + + open func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) { + requestDidCreateUploadable?(request, uploadable) + } + + open func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) { + requestDidFailToCreateUploadableWithError?(request, error) + } + + open func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) { + requestDidProvideInputStream?(request, stream) + } + + open func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result) { + requestDidFinishDownloadingUsingTaskWithResult?(request, task, result) + } + + open func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) { + requestDidCreateDestinationURL?(request, url) + } + + open func request(_ request: DownloadRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + fileURL: URL?, + withResult result: Request.ValidationResult) { + requestDidValidateRequestResponseFileURLWithResult?(request, + urlRequest, + response, + fileURL, + result) + } + + open func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { + requestDidParseDownloadResponse?(request, response) + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/HTTPHeaders.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/HTTPHeaders.swift new file mode 100644 index 0000000..cdbdbc6 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/HTTPHeaders.swift @@ -0,0 +1,447 @@ +// +// HTTPHeaders.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// An order-preserving and case-insensitive representation of HTTP headers. +public struct HTTPHeaders { + private var headers: [HTTPHeader] = [] + + /// Creates an empty instance. + public init() {} + + /// Creates an instance from an array of `HTTPHeader`s. Duplicate case-insensitive names are collapsed into the last + /// name and value encountered. + public init(_ headers: [HTTPHeader]) { + self.init() + + headers.forEach { update($0) } + } + + /// Creates an instance from a `[String: String]`. Duplicate case-insensitive names are collapsed into the last name + /// and value encountered. + public init(_ dictionary: [String: String]) { + self.init() + + dictionary.forEach { update(HTTPHeader(name: $0.key, value: $0.value)) } + } + + /// Case-insensitively updates or appends an `HTTPHeader` into the instance using the provided `name` and `value`. + /// + /// - Parameters: + /// - name: The `HTTPHeader` name. + /// - value: The `HTTPHeader value. + public mutating func add(name: String, value: String) { + update(HTTPHeader(name: name, value: value)) + } + + /// Case-insensitively updates or appends the provided `HTTPHeader` into the instance. + /// + /// - Parameter header: The `HTTPHeader` to update or append. + public mutating func add(_ header: HTTPHeader) { + update(header) + } + + /// Case-insensitively updates or appends an `HTTPHeader` into the instance using the provided `name` and `value`. + /// + /// - Parameters: + /// - name: The `HTTPHeader` name. + /// - value: The `HTTPHeader value. + public mutating func update(name: String, value: String) { + update(HTTPHeader(name: name, value: value)) + } + + /// Case-insensitively updates or appends the provided `HTTPHeader` into the instance. + /// + /// - Parameter header: The `HTTPHeader` to update or append. + public mutating func update(_ header: HTTPHeader) { + guard let index = headers.index(of: header.name) else { + headers.append(header) + return + } + + headers.replaceSubrange(index...index, with: [header]) + } + + /// Case-insensitively removes an `HTTPHeader`, if it exists, from the instance. + /// + /// - Parameter name: The name of the `HTTPHeader` to remove. + public mutating func remove(name: String) { + guard let index = headers.index(of: name) else { return } + + headers.remove(at: index) + } + + /// Sort the current instance by header name, case insensitively. + public mutating func sort() { + headers.sort { $0.name.lowercased() < $1.name.lowercased() } + } + + /// Returns an instance sorted by header name. + /// + /// - Returns: A copy of the current instance sorted by name. + public func sorted() -> HTTPHeaders { + var headers = self + headers.sort() + + return headers + } + + /// Case-insensitively find a header's value by name. + /// + /// - Parameter name: The name of the header to search for, case-insensitively. + /// + /// - Returns: The value of header, if it exists. + public func value(for name: String) -> String? { + guard let index = headers.index(of: name) else { return nil } + + return headers[index].value + } + + /// Case-insensitively access the header with the given name. + /// + /// - Parameter name: The name of the header. + public subscript(_ name: String) -> String? { + get { value(for: name) } + set { + if let value = newValue { + update(name: name, value: value) + } else { + remove(name: name) + } + } + } + + /// The dictionary representation of all headers. + /// + /// This representation does not preserve the current order of the instance. + public var dictionary: [String: String] { + let namesAndValues = headers.map { ($0.name, $0.value) } + + return Dictionary(namesAndValues, uniquingKeysWith: { _, last in last }) + } +} + +extension HTTPHeaders: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, String)...) { + self.init() + + elements.forEach { update(name: $0.0, value: $0.1) } + } +} + +extension HTTPHeaders: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: HTTPHeader...) { + self.init(elements) + } +} + +extension HTTPHeaders: Sequence { + public func makeIterator() -> IndexingIterator<[HTTPHeader]> { + headers.makeIterator() + } +} + +extension HTTPHeaders: Collection { + public var startIndex: Int { + headers.startIndex + } + + public var endIndex: Int { + headers.endIndex + } + + public subscript(position: Int) -> HTTPHeader { + headers[position] + } + + public func index(after i: Int) -> Int { + headers.index(after: i) + } +} + +extension HTTPHeaders: CustomStringConvertible { + public var description: String { + headers.map(\.description) + .joined(separator: "\n") + } +} + +// MARK: - HTTPHeader + +/// A representation of a single HTTP header's name / value pair. +public struct HTTPHeader: Hashable { + /// Name of the header. + public let name: String + + /// Value of the header. + public let value: String + + /// Creates an instance from the given `name` and `value`. + /// + /// - Parameters: + /// - name: The name of the header. + /// - value: The value of the header. + public init(name: String, value: String) { + self.name = name + self.value = value + } +} + +extension HTTPHeader: CustomStringConvertible { + public var description: String { + "\(name): \(value)" + } +} + +extension HTTPHeader { + /// Returns an `Accept` header. + /// + /// - Parameter value: The `Accept` value. + /// - Returns: The header. + public static func accept(_ value: String) -> HTTPHeader { + HTTPHeader(name: "Accept", value: value) + } + + /// Returns an `Accept-Charset` header. + /// + /// - Parameter value: The `Accept-Charset` value. + /// - Returns: The header. + public static func acceptCharset(_ value: String) -> HTTPHeader { + HTTPHeader(name: "Accept-Charset", value: value) + } + + /// Returns an `Accept-Language` header. + /// + /// Alamofire offers a default Accept-Language header that accumulates and encodes the system's preferred languages. + /// Use `HTTPHeader.defaultAcceptLanguage`. + /// + /// - Parameter value: The `Accept-Language` value. + /// + /// - Returns: The header. + public static func acceptLanguage(_ value: String) -> HTTPHeader { + HTTPHeader(name: "Accept-Language", value: value) + } + + /// Returns an `Accept-Encoding` header. + /// + /// Alamofire offers a default accept encoding value that provides the most common values. Use + /// `HTTPHeader.defaultAcceptEncoding`. + /// + /// - Parameter value: The `Accept-Encoding` value. + /// + /// - Returns: The header + public static func acceptEncoding(_ value: String) -> HTTPHeader { + HTTPHeader(name: "Accept-Encoding", value: value) + } + + /// Returns a `Basic` `Authorization` header using the `username` and `password` provided. + /// + /// - Parameters: + /// - username: The username of the header. + /// - password: The password of the header. + /// + /// - Returns: The header. + public static func authorization(username: String, password: String) -> HTTPHeader { + let credential = Data("\(username):\(password)".utf8).base64EncodedString() + + return authorization("Basic \(credential)") + } + + /// Returns a `Bearer` `Authorization` header using the `bearerToken` provided + /// + /// - Parameter bearerToken: The bearer token. + /// + /// - Returns: The header. + public static func authorization(bearerToken: String) -> HTTPHeader { + authorization("Bearer \(bearerToken)") + } + + /// Returns an `Authorization` header. + /// + /// Alamofire provides built-in methods to produce `Authorization` headers. For a Basic `Authorization` header use + /// `HTTPHeader.authorization(username:password:)`. For a Bearer `Authorization` header, use + /// `HTTPHeader.authorization(bearerToken:)`. + /// + /// - Parameter value: The `Authorization` value. + /// + /// - Returns: The header. + public static func authorization(_ value: String) -> HTTPHeader { + HTTPHeader(name: "Authorization", value: value) + } + + /// Returns a `Content-Disposition` header. + /// + /// - Parameter value: The `Content-Disposition` value. + /// + /// - Returns: The header. + public static func contentDisposition(_ value: String) -> HTTPHeader { + HTTPHeader(name: "Content-Disposition", value: value) + } + + /// Returns a `Content-Type` header. + /// + /// All Alamofire `ParameterEncoding`s and `ParameterEncoder`s set the `Content-Type` of the request, so it may not be necessary to manually + /// set this value. + /// + /// - Parameter value: The `Content-Type` value. + /// + /// - Returns: The header. + public static func contentType(_ value: String) -> HTTPHeader { + HTTPHeader(name: "Content-Type", value: value) + } + + /// Returns a `User-Agent` header. + /// + /// - Parameter value: The `User-Agent` value. + /// + /// - Returns: The header. + public static func userAgent(_ value: String) -> HTTPHeader { + HTTPHeader(name: "User-Agent", value: value) + } +} + +extension Array where Element == HTTPHeader { + /// Case-insensitively finds the index of an `HTTPHeader` with the provided name, if it exists. + func index(of name: String) -> Int? { + let lowercasedName = name.lowercased() + return firstIndex { $0.name.lowercased() == lowercasedName } + } +} + +// MARK: - Defaults + +extension HTTPHeaders { + /// The default set of `HTTPHeaders` used by Alamofire. Includes `Accept-Encoding`, `Accept-Language`, and + /// `User-Agent`. + public static let `default`: HTTPHeaders = [.defaultAcceptEncoding, + .defaultAcceptLanguage, + .defaultUserAgent] +} + +extension HTTPHeader { + /// Returns Alamofire's default `Accept-Encoding` header, appropriate for the encodings supported by particular OS + /// versions. + /// + /// See the [Accept-Encoding HTTP header documentation](https://tools.ietf.org/html/rfc7230#section-4.2.3) . + public static let defaultAcceptEncoding: HTTPHeader = { + let encodings: [String] + if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) { + encodings = ["br", "gzip", "deflate"] + } else { + encodings = ["gzip", "deflate"] + } + + return .acceptEncoding(encodings.qualityEncoded()) + }() + + /// Returns Alamofire's default `Accept-Language` header, generated by querying `Locale` for the user's + /// `preferredLanguages`. + /// + /// See the [Accept-Language HTTP header documentation](https://tools.ietf.org/html/rfc7231#section-5.3.5). + public static let defaultAcceptLanguage: HTTPHeader = .acceptLanguage(Locale.preferredLanguages.prefix(6).qualityEncoded()) + + /// Returns Alamofire's default `User-Agent` header. + /// + /// See the [User-Agent header documentation](https://tools.ietf.org/html/rfc7231#section-5.5.3). + /// + /// Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 13.0.0) Alamofire/5.0.0` + public static let defaultUserAgent: HTTPHeader = { + let info = Bundle.main.infoDictionary + let executable = (info?["CFBundleExecutable"] as? String) ?? + (ProcessInfo.processInfo.arguments.first?.split(separator: "/").last.map(String.init)) ?? + "Unknown" + let bundle = info?["CFBundleIdentifier"] as? String ?? "Unknown" + let appVersion = info?["CFBundleShortVersionString"] as? String ?? "Unknown" + let appBuild = info?["CFBundleVersion"] as? String ?? "Unknown" + + let osNameVersion: String = { + let version = ProcessInfo.processInfo.operatingSystemVersion + let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" + let osName: String = { + #if os(iOS) + #if targetEnvironment(macCatalyst) + return "macOS(Catalyst)" + #else + return "iOS" + #endif + #elseif os(watchOS) + return "watchOS" + #elseif os(tvOS) + return "tvOS" + #elseif os(macOS) + return "macOS" + #elseif os(Linux) + return "Linux" + #elseif os(Windows) + return "Windows" + #else + return "Unknown" + #endif + }() + + return "\(osName) \(versionString)" + }() + + let alamofireVersion = "Alamofire/\(version)" + + let userAgent = "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)" + + return .userAgent(userAgent) + }() +} + +extension Collection where Element == String { + func qualityEncoded() -> String { + enumerated().map { index, encoding in + let quality = 1.0 - (Double(index) * 0.1) + return "\(encoding);q=\(quality)" + }.joined(separator: ", ") + } +} + +// MARK: - System Type Extensions + +extension URLRequest { + /// Returns `allHTTPHeaderFields` as `HTTPHeaders`. + public var headers: HTTPHeaders { + get { allHTTPHeaderFields.map(HTTPHeaders.init) ?? HTTPHeaders() } + set { allHTTPHeaderFields = newValue.dictionary } + } +} + +extension HTTPURLResponse { + /// Returns `allHeaderFields` as `HTTPHeaders`. + public var headers: HTTPHeaders { + (allHeaderFields as? [String: String]).map(HTTPHeaders.init) ?? HTTPHeaders() + } +} + +extension URLSessionConfiguration { + /// Returns `httpAdditionalHeaders` as `HTTPHeaders`. + public var headers: HTTPHeaders { + get { (httpAdditionalHeaders as? [String: String]).map(HTTPHeaders.init) ?? HTTPHeaders() } + set { httpAdditionalHeaders = newValue.dictionary } + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/HTTPMethod.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/HTTPMethod.swift new file mode 100644 index 0000000..539d214 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/HTTPMethod.swift @@ -0,0 +1,56 @@ +// +// HTTPMethod.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +/// Type representing HTTP methods. Raw `String` value is stored and compared case-sensitively, so +/// `HTTPMethod.get != HTTPMethod(rawValue: "get")`. +/// +/// See https://tools.ietf.org/html/rfc7231#section-4.3 +public struct HTTPMethod: RawRepresentable, Equatable, Hashable { + /// `CONNECT` method. + public static let connect = HTTPMethod(rawValue: "CONNECT") + /// `DELETE` method. + public static let delete = HTTPMethod(rawValue: "DELETE") + /// `GET` method. + public static let get = HTTPMethod(rawValue: "GET") + /// `HEAD` method. + public static let head = HTTPMethod(rawValue: "HEAD") + /// `OPTIONS` method. + public static let options = HTTPMethod(rawValue: "OPTIONS") + /// `PATCH` method. + public static let patch = HTTPMethod(rawValue: "PATCH") + /// `POST` method. + public static let post = HTTPMethod(rawValue: "POST") + /// `PUT` method. + public static let put = HTTPMethod(rawValue: "PUT") + /// `QUERY` method. + public static let query = HTTPMethod(rawValue: "QUERY") + /// `TRACE` method. + public static let trace = HTTPMethod(rawValue: "TRACE") + + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/MultipartFormData.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/MultipartFormData.swift new file mode 100644 index 0000000..364b614 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/MultipartFormData.swift @@ -0,0 +1,584 @@ +// +// MultipartFormData.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +#if os(iOS) || os(watchOS) || os(tvOS) +import MobileCoreServices +#elseif os(macOS) +import CoreServices +#endif + +/// Constructs `multipart/form-data` for uploads within an HTTP or HTTPS body. There are currently two ways to encode +/// multipart form data. The first way is to encode the data directly in memory. This is very efficient, but can lead +/// to memory issues if the dataset is too large. The second way is designed for larger datasets and will write all the +/// data to a single file on disk with all the proper boundary segmentation. The second approach MUST be used for +/// larger datasets such as video content, otherwise your app may run out of memory when trying to encode the dataset. +/// +/// For more information on `multipart/form-data` in general, please refer to the RFC-2388 and RFC-2045 specs as well +/// and the w3 form documentation. +/// +/// - https://www.ietf.org/rfc/rfc2388.txt +/// - https://www.ietf.org/rfc/rfc2045.txt +/// - https://www.w3.org/TR/html401/interact/forms.html#h-17.13 +open class MultipartFormData { + // MARK: - Helper Types + + enum EncodingCharacters { + static let crlf = "\r\n" + } + + enum BoundaryGenerator { + enum BoundaryType { + case initial, encapsulated, final + } + + static func randomBoundary() -> String { + let first = UInt32.random(in: UInt32.min...UInt32.max) + let second = UInt32.random(in: UInt32.min...UInt32.max) + + return String(format: "alamofire.boundary.%08x%08x", first, second) + } + + static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data { + let boundaryText: String + + switch boundaryType { + case .initial: + boundaryText = "--\(boundary)\(EncodingCharacters.crlf)" + case .encapsulated: + boundaryText = "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)" + case .final: + boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)" + } + + return Data(boundaryText.utf8) + } + } + + class BodyPart { + let headers: HTTPHeaders + let bodyStream: InputStream + let bodyContentLength: UInt64 + var hasInitialBoundary = false + var hasFinalBoundary = false + + init(headers: HTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64) { + self.headers = headers + self.bodyStream = bodyStream + self.bodyContentLength = bodyContentLength + } + } + + // MARK: - Properties + + /// Default memory threshold used when encoding `MultipartFormData`, in bytes. + public static let encodingMemoryThreshold: UInt64 = 10_000_000 + + /// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`. + open lazy var contentType: String = "multipart/form-data; boundary=\(self.boundary)" + + /// The content length of all body parts used to generate the `multipart/form-data` not including the boundaries. + public var contentLength: UInt64 { bodyParts.reduce(0) { $0 + $1.bodyContentLength } } + + /// The boundary used to separate the body parts in the encoded form data. + public let boundary: String + + let fileManager: FileManager + + private var bodyParts: [BodyPart] + private var bodyPartError: AFError? + private let streamBufferSize: Int + + // MARK: - Lifecycle + + /// Creates an instance. + /// + /// - Parameters: + /// - fileManager: `FileManager` to use for file operations, if needed. + /// - boundary: Boundary `String` used to separate body parts. + public init(fileManager: FileManager = .default, boundary: String? = nil) { + self.fileManager = fileManager + self.boundary = boundary ?? BoundaryGenerator.randomBoundary() + bodyParts = [] + + // + // The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more + // information, please refer to the following article: + // - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html + // + streamBufferSize = 1024 + } + + // MARK: - Body Parts + + /// Creates a body part from the data and appends it to the instance. + /// + /// The body part data will be encoded using the following format: + /// + /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header) + /// - `Content-Type: #{mimeType}` (HTTP Header) + /// - Encoded file data + /// - Multipart form boundary + /// + /// - Parameters: + /// - data: `Data` to encoding into the instance. + /// - name: Name to associate with the `Data` in the `Content-Disposition` HTTP header. + /// - fileName: Filename to associate with the `Data` in the `Content-Disposition` HTTP header. + /// - mimeType: MIME type to associate with the data in the `Content-Type` HTTP header. + public func append(_ data: Data, withName name: String, fileName: String? = nil, mimeType: String? = nil) { + let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) + let stream = InputStream(data: data) + let length = UInt64(data.count) + + append(stream, withLength: length, headers: headers) + } + + /// Creates a body part from the file and appends it to the instance. + /// + /// The body part data will be encoded using the following format: + /// + /// - `Content-Disposition: form-data; name=#{name}; filename=#{generated filename}` (HTTP Header) + /// - `Content-Type: #{generated mimeType}` (HTTP Header) + /// - Encoded file data + /// - Multipart form boundary + /// + /// The filename in the `Content-Disposition` HTTP header is generated from the last path component of the + /// `fileURL`. The `Content-Type` HTTP header MIME type is generated by mapping the `fileURL` extension to the + /// system associated MIME type. + /// + /// - Parameters: + /// - fileURL: `URL` of the file whose content will be encoded into the instance. + /// - name: Name to associate with the file content in the `Content-Disposition` HTTP header. + public func append(_ fileURL: URL, withName name: String) { + let fileName = fileURL.lastPathComponent + let pathExtension = fileURL.pathExtension + + if !fileName.isEmpty && !pathExtension.isEmpty { + let mime = mimeType(forPathExtension: pathExtension) + append(fileURL, withName: name, fileName: fileName, mimeType: mime) + } else { + setBodyPartError(withReason: .bodyPartFilenameInvalid(in: fileURL)) + } + } + + /// Creates a body part from the file and appends it to the instance. + /// + /// The body part data will be encoded using the following format: + /// + /// - Content-Disposition: form-data; name=#{name}; filename=#{filename} (HTTP Header) + /// - Content-Type: #{mimeType} (HTTP Header) + /// - Encoded file data + /// - Multipart form boundary + /// + /// - Parameters: + /// - fileURL: `URL` of the file whose content will be encoded into the instance. + /// - name: Name to associate with the file content in the `Content-Disposition` HTTP header. + /// - fileName: Filename to associate with the file content in the `Content-Disposition` HTTP header. + /// - mimeType: MIME type to associate with the file content in the `Content-Type` HTTP header. + public func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String) { + let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) + + //============================================================ + // Check 1 - is file URL? + //============================================================ + + guard fileURL.isFileURL else { + setBodyPartError(withReason: .bodyPartURLInvalid(url: fileURL)) + return + } + + //============================================================ + // Check 2 - is file URL reachable? + //============================================================ + + #if !(os(Linux) || os(Windows)) + do { + let isReachable = try fileURL.checkPromisedItemIsReachable() + guard isReachable else { + setBodyPartError(withReason: .bodyPartFileNotReachable(at: fileURL)) + return + } + } catch { + setBodyPartError(withReason: .bodyPartFileNotReachableWithError(atURL: fileURL, error: error)) + return + } + #endif + + //============================================================ + // Check 3 - is file URL a directory? + //============================================================ + + var isDirectory: ObjCBool = false + let path = fileURL.path + + guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory.boolValue else { + setBodyPartError(withReason: .bodyPartFileIsDirectory(at: fileURL)) + return + } + + //============================================================ + // Check 4 - can the file size be extracted? + //============================================================ + + let bodyContentLength: UInt64 + + do { + guard let fileSize = try fileManager.attributesOfItem(atPath: path)[.size] as? NSNumber else { + setBodyPartError(withReason: .bodyPartFileSizeNotAvailable(at: fileURL)) + return + } + + bodyContentLength = fileSize.uint64Value + } catch { + setBodyPartError(withReason: .bodyPartFileSizeQueryFailedWithError(forURL: fileURL, error: error)) + return + } + + //============================================================ + // Check 5 - can a stream be created from file URL? + //============================================================ + + guard let stream = InputStream(url: fileURL) else { + setBodyPartError(withReason: .bodyPartInputStreamCreationFailed(for: fileURL)) + return + } + + append(stream, withLength: bodyContentLength, headers: headers) + } + + /// Creates a body part from the stream and appends it to the instance. + /// + /// The body part data will be encoded using the following format: + /// + /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header) + /// - `Content-Type: #{mimeType}` (HTTP Header) + /// - Encoded stream data + /// - Multipart form boundary + /// + /// - Parameters: + /// - stream: `InputStream` to encode into the instance. + /// - length: Length, in bytes, of the stream. + /// - name: Name to associate with the stream content in the `Content-Disposition` HTTP header. + /// - fileName: Filename to associate with the stream content in the `Content-Disposition` HTTP header. + /// - mimeType: MIME type to associate with the stream content in the `Content-Type` HTTP header. + public func append(_ stream: InputStream, + withLength length: UInt64, + name: String, + fileName: String, + mimeType: String) { + let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) + append(stream, withLength: length, headers: headers) + } + + /// Creates a body part with the stream, length, and headers and appends it to the instance. + /// + /// The body part data will be encoded using the following format: + /// + /// - HTTP headers + /// - Encoded stream data + /// - Multipart form boundary + /// + /// - Parameters: + /// - stream: `InputStream` to encode into the instance. + /// - length: Length, in bytes, of the stream. + /// - headers: `HTTPHeaders` for the body part. + public func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPHeaders) { + let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length) + bodyParts.append(bodyPart) + } + + // MARK: - Data Encoding + + /// Encodes all appended body parts into a single `Data` value. + /// + /// - Note: This method will load all the appended body parts into memory all at the same time. This method should + /// only be used when the encoded data will have a small memory footprint. For large data cases, please use + /// the `writeEncodedData(to:))` method. + /// + /// - Returns: The encoded `Data`, if encoding is successful. + /// - Throws: An `AFError` if encoding encounters an error. + public func encode() throws -> Data { + if let bodyPartError = bodyPartError { + throw bodyPartError + } + + var encoded = Data() + + bodyParts.first?.hasInitialBoundary = true + bodyParts.last?.hasFinalBoundary = true + + for bodyPart in bodyParts { + let encodedData = try encode(bodyPart) + encoded.append(encodedData) + } + + return encoded + } + + /// Writes all appended body parts to the given file `URL`. + /// + /// This process is facilitated by reading and writing with input and output streams, respectively. Thus, + /// this approach is very memory efficient and should be used for large body part data. + /// + /// - Parameter fileURL: File `URL` to which to write the form data. + /// - Throws: An `AFError` if encoding encounters an error. + public func writeEncodedData(to fileURL: URL) throws { + if let bodyPartError = bodyPartError { + throw bodyPartError + } + + if fileManager.fileExists(atPath: fileURL.path) { + throw AFError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL)) + } else if !fileURL.isFileURL { + throw AFError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL)) + } + + guard let outputStream = OutputStream(url: fileURL, append: false) else { + throw AFError.multipartEncodingFailed(reason: .outputStreamCreationFailed(for: fileURL)) + } + + outputStream.open() + defer { outputStream.close() } + + bodyParts.first?.hasInitialBoundary = true + bodyParts.last?.hasFinalBoundary = true + + for bodyPart in bodyParts { + try write(bodyPart, to: outputStream) + } + } + + // MARK: - Private - Body Part Encoding + + private func encode(_ bodyPart: BodyPart) throws -> Data { + var encoded = Data() + + let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData() + encoded.append(initialData) + + let headerData = encodeHeaders(for: bodyPart) + encoded.append(headerData) + + let bodyStreamData = try encodeBodyStream(for: bodyPart) + encoded.append(bodyStreamData) + + if bodyPart.hasFinalBoundary { + encoded.append(finalBoundaryData()) + } + + return encoded + } + + private func encodeHeaders(for bodyPart: BodyPart) -> Data { + let headerText = bodyPart.headers.map { "\($0.name): \($0.value)\(EncodingCharacters.crlf)" } + .joined() + + EncodingCharacters.crlf + + return Data(headerText.utf8) + } + + private func encodeBodyStream(for bodyPart: BodyPart) throws -> Data { + let inputStream = bodyPart.bodyStream + inputStream.open() + defer { inputStream.close() } + + var encoded = Data() + + while inputStream.hasBytesAvailable { + var buffer = [UInt8](repeating: 0, count: streamBufferSize) + let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize) + + if let error = inputStream.streamError { + throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error)) + } + + if bytesRead > 0 { + encoded.append(buffer, count: bytesRead) + } else { + break + } + } + + guard UInt64(encoded.count) == bodyPart.bodyContentLength else { + let error = AFError.UnexpectedInputStreamLength(bytesExpected: bodyPart.bodyContentLength, + bytesRead: UInt64(encoded.count)) + throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error)) + } + + return encoded + } + + // MARK: - Private - Writing Body Part to Output Stream + + private func write(_ bodyPart: BodyPart, to outputStream: OutputStream) throws { + try writeInitialBoundaryData(for: bodyPart, to: outputStream) + try writeHeaderData(for: bodyPart, to: outputStream) + try writeBodyStream(for: bodyPart, to: outputStream) + try writeFinalBoundaryData(for: bodyPart, to: outputStream) + } + + private func writeInitialBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws { + let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData() + return try write(initialData, to: outputStream) + } + + private func writeHeaderData(for bodyPart: BodyPart, to outputStream: OutputStream) throws { + let headerData = encodeHeaders(for: bodyPart) + return try write(headerData, to: outputStream) + } + + private func writeBodyStream(for bodyPart: BodyPart, to outputStream: OutputStream) throws { + let inputStream = bodyPart.bodyStream + + inputStream.open() + defer { inputStream.close() } + + while inputStream.hasBytesAvailable { + var buffer = [UInt8](repeating: 0, count: streamBufferSize) + let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize) + + if let streamError = inputStream.streamError { + throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: streamError)) + } + + if bytesRead > 0 { + if buffer.count != bytesRead { + buffer = Array(buffer[0.. 0, outputStream.hasSpaceAvailable { + let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite) + + if let error = outputStream.streamError { + throw AFError.multipartEncodingFailed(reason: .outputStreamWriteFailed(error: error)) + } + + bytesToWrite -= bytesWritten + + if bytesToWrite > 0 { + buffer = Array(buffer[bytesWritten.. HTTPHeaders { + var disposition = "form-data; name=\"\(name)\"" + if let fileName = fileName { disposition += "; filename=\"\(fileName)\"" } + + var headers: HTTPHeaders = [.contentDisposition(disposition)] + if let mimeType = mimeType { headers.add(.contentType(mimeType)) } + + return headers + } + + // MARK: - Private - Boundary Encoding + + private func initialBoundaryData() -> Data { + BoundaryGenerator.boundaryData(forBoundaryType: .initial, boundary: boundary) + } + + private func encapsulatedBoundaryData() -> Data { + BoundaryGenerator.boundaryData(forBoundaryType: .encapsulated, boundary: boundary) + } + + private func finalBoundaryData() -> Data { + BoundaryGenerator.boundaryData(forBoundaryType: .final, boundary: boundary) + } + + // MARK: - Private - Errors + + private func setBodyPartError(withReason reason: AFError.MultipartEncodingFailureReason) { + guard bodyPartError == nil else { return } + bodyPartError = AFError.multipartEncodingFailed(reason: reason) + } +} + +#if canImport(UniformTypeIdentifiers) +import UniformTypeIdentifiers + +extension MultipartFormData { + // MARK: - Private - Mime Type + + private func mimeType(forPathExtension pathExtension: String) -> String { + if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) { + return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream" + } else { + if + let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(), + let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() { + return contentType as String + } + + return "application/octet-stream" + } + } +} + +#else + +extension MultipartFormData { + // MARK: - Private - Mime Type + + private func mimeType(forPathExtension pathExtension: String) -> String { + #if !(os(Linux) || os(Windows)) + if + let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(), + let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() { + return contentType as String + } + #endif + + return "application/octet-stream" + } +} + +#endif diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/MultipartUpload.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/MultipartUpload.swift new file mode 100644 index 0000000..ceda21f --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/MultipartUpload.swift @@ -0,0 +1,89 @@ +// +// MultipartUpload.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Internal type which encapsulates a `MultipartFormData` upload. +final class MultipartUpload { + lazy var result = Result { try build() } + + @Protected + private(set) var multipartFormData: MultipartFormData + let encodingMemoryThreshold: UInt64 + let request: URLRequestConvertible + let fileManager: FileManager + + init(encodingMemoryThreshold: UInt64, + request: URLRequestConvertible, + multipartFormData: MultipartFormData) { + self.encodingMemoryThreshold = encodingMemoryThreshold + self.request = request + fileManager = multipartFormData.fileManager + self.multipartFormData = multipartFormData + } + + func build() throws -> UploadRequest.Uploadable { + let uploadable: UploadRequest.Uploadable + if $multipartFormData.contentLength < encodingMemoryThreshold { + let data = try $multipartFormData.read { try $0.encode() } + + uploadable = .data(data) + } else { + let tempDirectoryURL = fileManager.temporaryDirectory + let directoryURL = tempDirectoryURL.appendingPathComponent("org.alamofire.manager/multipart.form.data") + let fileName = UUID().uuidString + let fileURL = directoryURL.appendingPathComponent(fileName) + + try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) + + do { + try $multipartFormData.read { try $0.writeEncodedData(to: fileURL) } + } catch { + // Cleanup after attempted write if it fails. + try? fileManager.removeItem(at: fileURL) + throw error + } + + uploadable = .file(fileURL, shouldRemove: true) + } + + return uploadable + } +} + +extension MultipartUpload: UploadConvertible { + func asURLRequest() throws -> URLRequest { + var urlRequest = try request.asURLRequest() + + $multipartFormData.read { multipartFormData in + urlRequest.headers.add(.contentType(multipartFormData.contentType)) + } + + return urlRequest + } + + func createUploadable() throws -> UploadRequest.Uploadable { + try result.get() + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/NetworkReachabilityManager.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/NetworkReachabilityManager.swift new file mode 100644 index 0000000..deeb3a4 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/NetworkReachabilityManager.swift @@ -0,0 +1,267 @@ +// +// NetworkReachabilityManager.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if !(os(watchOS) || os(Linux) || os(Windows)) + +import Foundation +import SystemConfiguration + +/// The `NetworkReachabilityManager` class listens for reachability changes of hosts and addresses for both cellular and +/// WiFi network interfaces. +/// +/// Reachability can be used to determine background information about why a network operation failed, or to retry +/// network requests when a connection is established. It should not be used to prevent a user from initiating a network +/// request, as it's possible that an initial request may be required to establish reachability. +open class NetworkReachabilityManager { + /// Defines the various states of network reachability. + public enum NetworkReachabilityStatus { + /// It is unknown whether the network is reachable. + case unknown + /// The network is not reachable. + case notReachable + /// The network is reachable on the associated `ConnectionType`. + case reachable(ConnectionType) + + init(_ flags: SCNetworkReachabilityFlags) { + guard flags.isActuallyReachable else { self = .notReachable; return } + + var networkStatus: NetworkReachabilityStatus = .reachable(.ethernetOrWiFi) + + if flags.isCellular { networkStatus = .reachable(.cellular) } + + self = networkStatus + } + + /// Defines the various connection types detected by reachability flags. + public enum ConnectionType { + /// The connection type is either over Ethernet or WiFi. + case ethernetOrWiFi + /// The connection type is a cellular connection. + case cellular + } + } + + /// A closure executed when the network reachability status changes. The closure takes a single argument: the + /// network reachability status. + public typealias Listener = (NetworkReachabilityStatus) -> Void + + /// Default `NetworkReachabilityManager` for the zero address and a `listenerQueue` of `.main`. + public static let `default` = NetworkReachabilityManager() + + // MARK: - Properties + + /// Whether the network is currently reachable. + open var isReachable: Bool { isReachableOnCellular || isReachableOnEthernetOrWiFi } + + /// Whether the network is currently reachable over the cellular interface. + /// + /// - Note: Using this property to decide whether to make a high or low bandwidth request is not recommended. + /// Instead, set the `allowsCellularAccess` on any `URLRequest`s being issued. + /// + open var isReachableOnCellular: Bool { status == .reachable(.cellular) } + + /// Whether the network is currently reachable over Ethernet or WiFi interface. + open var isReachableOnEthernetOrWiFi: Bool { status == .reachable(.ethernetOrWiFi) } + + /// `DispatchQueue` on which reachability will update. + public let reachabilityQueue = DispatchQueue(label: "org.alamofire.reachabilityQueue") + + /// Flags of the current reachability type, if any. + open var flags: SCNetworkReachabilityFlags? { + var flags = SCNetworkReachabilityFlags() + + return (SCNetworkReachabilityGetFlags(reachability, &flags)) ? flags : nil + } + + /// The current network reachability status. + open var status: NetworkReachabilityStatus { + flags.map(NetworkReachabilityStatus.init) ?? .unknown + } + + /// Mutable state storage. + struct MutableState { + /// A closure executed when the network reachability status changes. + var listener: Listener? + /// `DispatchQueue` on which listeners will be called. + var listenerQueue: DispatchQueue? + /// Previously calculated status. + var previousStatus: NetworkReachabilityStatus? + } + + /// `SCNetworkReachability` instance providing notifications. + private let reachability: SCNetworkReachability + + /// Protected storage for mutable state. + @Protected + private var mutableState = MutableState() + + // MARK: - Initialization + + /// Creates an instance with the specified host. + /// + /// - Note: The `host` value must *not* contain a scheme, just the hostname. + /// + /// - Parameters: + /// - host: Host used to evaluate network reachability. Must *not* include the scheme (e.g. `https`). + public convenience init?(host: String) { + guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil } + + self.init(reachability: reachability) + } + + /// Creates an instance that monitors the address 0.0.0.0. + /// + /// Reachability treats the 0.0.0.0 address as a special token that causes it to monitor the general routing + /// status of the device, both IPv4 and IPv6. + public convenience init?() { + var zero = sockaddr() + zero.sa_len = UInt8(MemoryLayout.size) + zero.sa_family = sa_family_t(AF_INET) + + guard let reachability = SCNetworkReachabilityCreateWithAddress(nil, &zero) else { return nil } + + self.init(reachability: reachability) + } + + private init(reachability: SCNetworkReachability) { + self.reachability = reachability + } + + deinit { + stopListening() + } + + // MARK: - Listening + + /// Starts listening for changes in network reachability status. + /// + /// - Note: Stops and removes any existing listener. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which to call the `listener` closure. `.main` by default. + /// - listener: `Listener` closure called when reachability changes. + /// + /// - Returns: `true` if listening was started successfully, `false` otherwise. + @discardableResult + open func startListening(onQueue queue: DispatchQueue = .main, + onUpdatePerforming listener: @escaping Listener) -> Bool { + stopListening() + + $mutableState.write { state in + state.listenerQueue = queue + state.listener = listener + } + + var context = SCNetworkReachabilityContext(version: 0, + info: Unmanaged.passUnretained(self).toOpaque(), + retain: nil, + release: nil, + copyDescription: nil) + let callback: SCNetworkReachabilityCallBack = { _, flags, info in + guard let info = info else { return } + + let instance = Unmanaged.fromOpaque(info).takeUnretainedValue() + instance.notifyListener(flags) + } + + let queueAdded = SCNetworkReachabilitySetDispatchQueue(reachability, reachabilityQueue) + let callbackAdded = SCNetworkReachabilitySetCallback(reachability, callback, &context) + + // Manually call listener to give initial state, since the framework may not. + if let currentFlags = flags { + reachabilityQueue.async { + self.notifyListener(currentFlags) + } + } + + return callbackAdded && queueAdded + } + + /// Stops listening for changes in network reachability status. + open func stopListening() { + SCNetworkReachabilitySetCallback(reachability, nil, nil) + SCNetworkReachabilitySetDispatchQueue(reachability, nil) + $mutableState.write { state in + state.listener = nil + state.listenerQueue = nil + state.previousStatus = nil + } + } + + // MARK: - Internal - Listener Notification + + /// Calls the `listener` closure of the `listenerQueue` if the computed status hasn't changed. + /// + /// - Note: Should only be called from the `reachabilityQueue`. + /// + /// - Parameter flags: `SCNetworkReachabilityFlags` to use to calculate the status. + func notifyListener(_ flags: SCNetworkReachabilityFlags) { + let newStatus = NetworkReachabilityStatus(flags) + + $mutableState.write { state in + guard state.previousStatus != newStatus else { return } + + state.previousStatus = newStatus + + let listener = state.listener + state.listenerQueue?.async { listener?(newStatus) } + } + } +} + +// MARK: - + +extension NetworkReachabilityManager.NetworkReachabilityStatus: Equatable {} + +extension SCNetworkReachabilityFlags { + var isReachable: Bool { contains(.reachable) } + var isConnectionRequired: Bool { contains(.connectionRequired) } + var canConnectAutomatically: Bool { contains(.connectionOnDemand) || contains(.connectionOnTraffic) } + var canConnectWithoutUserInteraction: Bool { canConnectAutomatically && !contains(.interventionRequired) } + var isActuallyReachable: Bool { isReachable && (!isConnectionRequired || canConnectWithoutUserInteraction) } + var isCellular: Bool { + #if os(iOS) || os(tvOS) + return contains(.isWWAN) + #else + return false + #endif + } + + /// Human readable `String` for all states, to help with debugging. + var readableDescription: String { + let W = isCellular ? "W" : "-" + let R = isReachable ? "R" : "-" + let c = isConnectionRequired ? "c" : "-" + let t = contains(.transientConnection) ? "t" : "-" + let i = contains(.interventionRequired) ? "i" : "-" + let C = contains(.connectionOnTraffic) ? "C" : "-" + let D = contains(.connectionOnDemand) ? "D" : "-" + let l = contains(.isLocalAddress) ? "l" : "-" + let d = contains(.isDirect) ? "d" : "-" + let a = contains(.connectionAutomatic) ? "a" : "-" + + return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)\(a)" + } +} +#endif diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/Notifications.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/Notifications.swift new file mode 100644 index 0000000..66434b6 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/Notifications.swift @@ -0,0 +1,115 @@ +// +// Notifications.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension Request { + /// Posted when a `Request` is resumed. The `Notification` contains the resumed `Request`. + public static let didResumeNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didResume") + /// Posted when a `Request` is suspended. The `Notification` contains the suspended `Request`. + public static let didSuspendNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didSuspend") + /// Posted when a `Request` is cancelled. The `Notification` contains the cancelled `Request`. + public static let didCancelNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCancel") + /// Posted when a `Request` is finished. The `Notification` contains the completed `Request`. + public static let didFinishNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didFinish") + + /// Posted when a `URLSessionTask` is resumed. The `Notification` contains the `Request` associated with the `URLSessionTask`. + public static let didResumeTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didResumeTask") + /// Posted when a `URLSessionTask` is suspended. The `Notification` contains the `Request` associated with the `URLSessionTask`. + public static let didSuspendTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didSuspendTask") + /// Posted when a `URLSessionTask` is cancelled. The `Notification` contains the `Request` associated with the `URLSessionTask`. + public static let didCancelTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCancelTask") + /// Posted when a `URLSessionTask` is completed. The `Notification` contains the `Request` associated with the `URLSessionTask`. + public static let didCompleteTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCompleteTask") +} + +// MARK: - + +extension Notification { + /// The `Request` contained by the instance's `userInfo`, `nil` otherwise. + public var request: Request? { + userInfo?[String.requestKey] as? Request + } + + /// Convenience initializer for a `Notification` containing a `Request` payload. + /// + /// - Parameters: + /// - name: The name of the notification. + /// - request: The `Request` payload. + init(name: Notification.Name, request: Request) { + self.init(name: name, object: nil, userInfo: [String.requestKey: request]) + } +} + +extension NotificationCenter { + /// Convenience function for posting notifications with `Request` payloads. + /// + /// - Parameters: + /// - name: The name of the notification. + /// - request: The `Request` payload. + func postNotification(named name: Notification.Name, with request: Request) { + let notification = Notification(name: name, request: request) + post(notification) + } +} + +extension String { + /// User info dictionary key representing the `Request` associated with the notification. + fileprivate static let requestKey = "org.alamofire.notification.key.request" +} + +/// `EventMonitor` that provides Alamofire's notifications. +public final class AlamofireNotifications: EventMonitor { + public func requestDidResume(_ request: Request) { + NotificationCenter.default.postNotification(named: Request.didResumeNotification, with: request) + } + + public func requestDidSuspend(_ request: Request) { + NotificationCenter.default.postNotification(named: Request.didSuspendNotification, with: request) + } + + public func requestDidCancel(_ request: Request) { + NotificationCenter.default.postNotification(named: Request.didCancelNotification, with: request) + } + + public func requestDidFinish(_ request: Request) { + NotificationCenter.default.postNotification(named: Request.didFinishNotification, with: request) + } + + public func request(_ request: Request, didResumeTask task: URLSessionTask) { + NotificationCenter.default.postNotification(named: Request.didResumeTaskNotification, with: request) + } + + public func request(_ request: Request, didSuspendTask task: URLSessionTask) { + NotificationCenter.default.postNotification(named: Request.didSuspendTaskNotification, with: request) + } + + public func request(_ request: Request, didCancelTask task: URLSessionTask) { + NotificationCenter.default.postNotification(named: Request.didCancelTaskNotification, with: request) + } + + public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) { + NotificationCenter.default.postNotification(named: Request.didCompleteTaskNotification, with: request) + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/OperationQueue+Alamofire.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/OperationQueue+Alamofire.swift new file mode 100644 index 0000000..b06a0cc --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/OperationQueue+Alamofire.swift @@ -0,0 +1,49 @@ +// +// OperationQueue+Alamofire.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension OperationQueue { + /// Creates an instance using the provided parameters. + /// + /// - Parameters: + /// - qualityOfService: `QualityOfService` to be applied to the queue. `.default` by default. + /// - maxConcurrentOperationCount: Maximum concurrent operations. + /// `OperationQueue.defaultMaxConcurrentOperationCount` by default. + /// - underlyingQueue: Underlying `DispatchQueue`. `nil` by default. + /// - name: Name for the queue. `nil` by default. + /// - startSuspended: Whether the queue starts suspended. `false` by default. + convenience init(qualityOfService: QualityOfService = .default, + maxConcurrentOperationCount: Int = OperationQueue.defaultMaxConcurrentOperationCount, + underlyingQueue: DispatchQueue? = nil, + name: String? = nil, + startSuspended: Bool = false) { + self.init() + self.qualityOfService = qualityOfService + self.maxConcurrentOperationCount = maxConcurrentOperationCount + self.underlyingQueue = underlyingQueue + self.name = name + isSuspended = startSuspended + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/ParameterEncoder.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/ParameterEncoder.swift new file mode 100644 index 0000000..2263660 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/ParameterEncoder.swift @@ -0,0 +1,217 @@ +// +// ParameterEncoder.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A type that can encode any `Encodable` type into a `URLRequest`. +public protocol ParameterEncoder { + /// Encode the provided `Encodable` parameters into `request`. + /// + /// - Parameters: + /// - parameters: The `Encodable` parameter value. + /// - request: The `URLRequest` into which to encode the parameters. + /// + /// - Returns: A `URLRequest` with the result of the encoding. + /// - Throws: An `Error` when encoding fails. For Alamofire provided encoders, this will be an instance of + /// `AFError.parameterEncoderFailed` with an associated `ParameterEncoderFailureReason`. + func encode(_ parameters: Parameters?, into request: URLRequest) throws -> URLRequest +} + +/// A `ParameterEncoder` that encodes types as JSON body data. +/// +/// If no `Content-Type` header is already set on the provided `URLRequest`s, it's set to `application/json`. +open class JSONParameterEncoder: ParameterEncoder { + /// Returns an encoder with default parameters. + public static var `default`: JSONParameterEncoder { JSONParameterEncoder() } + + /// Returns an encoder with `JSONEncoder.outputFormatting` set to `.prettyPrinted`. + public static var prettyPrinted: JSONParameterEncoder { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + return JSONParameterEncoder(encoder: encoder) + } + + /// Returns an encoder with `JSONEncoder.outputFormatting` set to `.sortedKeys`. + @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) + public static var sortedKeys: JSONParameterEncoder { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + + return JSONParameterEncoder(encoder: encoder) + } + + /// `JSONEncoder` used to encode parameters. + public let encoder: JSONEncoder + + /// Creates an instance with the provided `JSONEncoder`. + /// + /// - Parameter encoder: The `JSONEncoder`. `JSONEncoder()` by default. + public init(encoder: JSONEncoder = JSONEncoder()) { + self.encoder = encoder + } + + open func encode(_ parameters: Parameters?, + into request: URLRequest) throws -> URLRequest { + guard let parameters = parameters else { return request } + + var request = request + + do { + let data = try encoder.encode(parameters) + request.httpBody = data + if request.headers["Content-Type"] == nil { + request.headers.update(.contentType("application/json")) + } + } catch { + throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) + } + + return request + } +} + +#if swift(>=5.5) +extension ParameterEncoder where Self == JSONParameterEncoder { + /// Provides a default `JSONParameterEncoder` instance. + public static var json: JSONParameterEncoder { JSONParameterEncoder() } + + /// Creates a `JSONParameterEncoder` using the provided `JSONEncoder`. + /// + /// - Parameter encoder: `JSONEncoder` used to encode parameters. `JSONEncoder()` by default. + /// - Returns: The `JSONParameterEncoder`. + public static func json(encoder: JSONEncoder = JSONEncoder()) -> JSONParameterEncoder { + JSONParameterEncoder(encoder: encoder) + } +} +#endif + +/// A `ParameterEncoder` that encodes types as URL-encoded query strings to be set on the URL or as body data, depending +/// on the `Destination` set. +/// +/// If no `Content-Type` header is already set on the provided `URLRequest`s, it will be set to +/// `application/x-www-form-urlencoded; charset=utf-8`. +/// +/// Encoding behavior can be customized by passing an instance of `URLEncodedFormEncoder` to the initializer. +open class URLEncodedFormParameterEncoder: ParameterEncoder { + /// Defines where the URL-encoded string should be set for each `URLRequest`. + public enum Destination { + /// Applies the encoded query string to any existing query string for `.get`, `.head`, and `.delete` request. + /// Sets it to the `httpBody` for all other methods. + case methodDependent + /// Applies the encoded query string to any existing query string from the `URLRequest`. + case queryString + /// Applies the encoded query string to the `httpBody` of the `URLRequest`. + case httpBody + + /// Determines whether the URL-encoded string should be applied to the `URLRequest`'s `url`. + /// + /// - Parameter method: The `HTTPMethod`. + /// + /// - Returns: Whether the URL-encoded string should be applied to a `URL`. + func encodesParametersInURL(for method: HTTPMethod) -> Bool { + switch self { + case .methodDependent: return [.get, .head, .delete].contains(method) + case .queryString: return true + case .httpBody: return false + } + } + } + + /// Returns an encoder with default parameters. + public static var `default`: URLEncodedFormParameterEncoder { URLEncodedFormParameterEncoder() } + + /// The `URLEncodedFormEncoder` to use. + public let encoder: URLEncodedFormEncoder + + /// The `Destination` for the URL-encoded string. + public let destination: Destination + + /// Creates an instance with the provided `URLEncodedFormEncoder` instance and `Destination` value. + /// + /// - Parameters: + /// - encoder: The `URLEncodedFormEncoder`. `URLEncodedFormEncoder()` by default. + /// - destination: The `Destination`. `.methodDependent` by default. + public init(encoder: URLEncodedFormEncoder = URLEncodedFormEncoder(), destination: Destination = .methodDependent) { + self.encoder = encoder + self.destination = destination + } + + open func encode(_ parameters: Parameters?, + into request: URLRequest) throws -> URLRequest { + guard let parameters = parameters else { return request } + + var request = request + + guard let url = request.url else { + throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.url)) + } + + guard let method = request.method else { + let rawValue = request.method?.rawValue ?? "nil" + throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.httpMethod(rawValue: rawValue))) + } + + if destination.encodesParametersInURL(for: method), + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { + let query: String = try Result { try encoder.encode(parameters) } + .mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.get() + let newQueryString = [components.percentEncodedQuery, query].compactMap { $0 }.joinedWithAmpersands() + components.percentEncodedQuery = newQueryString.isEmpty ? nil : newQueryString + + guard let newURL = components.url else { + throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.url)) + } + + request.url = newURL + } else { + if request.headers["Content-Type"] == nil { + request.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8")) + } + + request.httpBody = try Result { try encoder.encode(parameters) } + .mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.get() + } + + return request + } +} + +#if swift(>=5.5) +extension ParameterEncoder where Self == URLEncodedFormParameterEncoder { + /// Provides a default `URLEncodedFormParameterEncoder` instance. + public static var urlEncodedForm: URLEncodedFormParameterEncoder { URLEncodedFormParameterEncoder() } + + /// Creates a `URLEncodedFormParameterEncoder` with the provided encoder and destination. + /// + /// - Parameters: + /// - encoder: `URLEncodedFormEncoder` used to encode the parameters. `URLEncodedFormEncoder()` by default. + /// - destination: `Destination` to which to encode the parameters. `.methodDependent` by default. + /// - Returns: The `URLEncodedFormParameterEncoder`. + public static func urlEncodedForm(encoder: URLEncodedFormEncoder = URLEncodedFormEncoder(), + destination: URLEncodedFormParameterEncoder.Destination = .methodDependent) -> URLEncodedFormParameterEncoder { + URLEncodedFormParameterEncoder(encoder: encoder, destination: destination) + } +} +#endif diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/ParameterEncoding.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/ParameterEncoding.swift new file mode 100644 index 0000000..e7fa452 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/ParameterEncoding.swift @@ -0,0 +1,321 @@ +// +// ParameterEncoding.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A dictionary of parameters to apply to a `URLRequest`. +public typealias Parameters = [String: Any] + +/// A type used to define how a set of parameters are applied to a `URLRequest`. +public protocol ParameterEncoding { + /// Creates a `URLRequest` by encoding parameters and applying them on the passed request. + /// + /// - Parameters: + /// - urlRequest: `URLRequestConvertible` value onto which parameters will be encoded. + /// - parameters: `Parameters` to encode onto the request. + /// + /// - Returns: The encoded `URLRequest`. + /// - Throws: Any `Error` produced during parameter encoding. + func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest +} + +// MARK: - + +/// Creates a url-encoded query string to be set as or appended to any existing URL query string or set as the HTTP +/// body of the URL request. Whether the query string is set or appended to any existing URL query string or set as +/// the HTTP body depends on the destination of the encoding. +/// +/// The `Content-Type` HTTP header field of an encoded request with HTTP body is set to +/// `application/x-www-form-urlencoded; charset=utf-8`. +/// +/// There is no published specification for how to encode collection types. By default the convention of appending +/// `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for +/// nested dictionary values (`foo[bar]=baz`) is used. Optionally, `ArrayEncoding` can be used to omit the +/// square brackets appended to array keys. +/// +/// `BoolEncoding` can be used to configure how boolean values are encoded. The default behavior is to encode +/// `true` as 1 and `false` as 0. +public struct URLEncoding: ParameterEncoding { + // MARK: Helper Types + + /// Defines whether the url-encoded query string is applied to the existing query string or HTTP body of the + /// resulting URL request. + public enum Destination { + /// Applies encoded query string result to existing query string for `GET`, `HEAD` and `DELETE` requests and + /// sets as the HTTP body for requests with any other HTTP method. + case methodDependent + /// Sets or appends encoded query string result to existing query string. + case queryString + /// Sets encoded query string result as the HTTP body of the URL request. + case httpBody + + func encodesParametersInURL(for method: HTTPMethod) -> Bool { + switch self { + case .methodDependent: return [.get, .head, .delete].contains(method) + case .queryString: return true + case .httpBody: return false + } + } + } + + /// Configures how `Array` parameters are encoded. + public enum ArrayEncoding { + /// An empty set of square brackets is appended to the key for every value. This is the default behavior. + case brackets + /// No brackets are appended. The key is encoded as is. + case noBrackets + /// Brackets containing the item index are appended. This matches the jQuery and Node.js behavior. + case indexInBrackets + + func encode(key: String, atIndex index: Int) -> String { + switch self { + case .brackets: + return "\(key)[]" + case .noBrackets: + return key + case .indexInBrackets: + return "\(key)[\(index)]" + } + } + } + + /// Configures how `Bool` parameters are encoded. + public enum BoolEncoding { + /// Encode `true` as `1` and `false` as `0`. This is the default behavior. + case numeric + /// Encode `true` and `false` as string literals. + case literal + + func encode(value: Bool) -> String { + switch self { + case .numeric: + return value ? "1" : "0" + case .literal: + return value ? "true" : "false" + } + } + } + + // MARK: Properties + + /// Returns a default `URLEncoding` instance with a `.methodDependent` destination. + public static var `default`: URLEncoding { URLEncoding() } + + /// Returns a `URLEncoding` instance with a `.queryString` destination. + public static var queryString: URLEncoding { URLEncoding(destination: .queryString) } + + /// Returns a `URLEncoding` instance with an `.httpBody` destination. + public static var httpBody: URLEncoding { URLEncoding(destination: .httpBody) } + + /// The destination defining where the encoded query string is to be applied to the URL request. + public let destination: Destination + + /// The encoding to use for `Array` parameters. + public let arrayEncoding: ArrayEncoding + + /// The encoding to use for `Bool` parameters. + public let boolEncoding: BoolEncoding + + // MARK: Initialization + + /// Creates an instance using the specified parameters. + /// + /// - Parameters: + /// - destination: `Destination` defining where the encoded query string will be applied. `.methodDependent` by + /// default. + /// - arrayEncoding: `ArrayEncoding` to use. `.brackets` by default. + /// - boolEncoding: `BoolEncoding` to use. `.numeric` by default. + public init(destination: Destination = .methodDependent, + arrayEncoding: ArrayEncoding = .brackets, + boolEncoding: BoolEncoding = .numeric) { + self.destination = destination + self.arrayEncoding = arrayEncoding + self.boolEncoding = boolEncoding + } + + // MARK: Encoding + + public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { + var urlRequest = try urlRequest.asURLRequest() + + guard let parameters = parameters else { return urlRequest } + + if let method = urlRequest.method, destination.encodesParametersInURL(for: method) { + guard let url = urlRequest.url else { + throw AFError.parameterEncodingFailed(reason: .missingURL) + } + + if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty { + let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters) + urlComponents.percentEncodedQuery = percentEncodedQuery + urlRequest.url = urlComponents.url + } + } else { + if urlRequest.headers["Content-Type"] == nil { + urlRequest.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8")) + } + + urlRequest.httpBody = Data(query(parameters).utf8) + } + + return urlRequest + } + + /// Creates a percent-escaped, URL encoded query string components from the given key-value pair recursively. + /// + /// - Parameters: + /// - key: Key of the query component. + /// - value: Value of the query component. + /// + /// - Returns: The percent-escaped, URL encoded query string components. + public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] { + var components: [(String, String)] = [] + switch value { + case let dictionary as [String: Any]: + for (nestedKey, value) in dictionary { + components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value) + } + case let array as [Any]: + for (index, value) in array.enumerated() { + components += queryComponents(fromKey: arrayEncoding.encode(key: key, atIndex: index), value: value) + } + case let number as NSNumber: + if number.isBool { + components.append((escape(key), escape(boolEncoding.encode(value: number.boolValue)))) + } else { + components.append((escape(key), escape("\(number)"))) + } + case let bool as Bool: + components.append((escape(key), escape(boolEncoding.encode(value: bool)))) + default: + components.append((escape(key), escape("\(value)"))) + } + return components + } + + /// Creates a percent-escaped string following RFC 3986 for a query string key or value. + /// + /// - Parameter string: `String` to be percent-escaped. + /// + /// - Returns: The percent-escaped `String`. + public func escape(_ string: String) -> String { + string.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? string + } + + private func query(_ parameters: [String: Any]) -> String { + var components: [(String, String)] = [] + + for key in parameters.keys.sorted(by: <) { + let value = parameters[key]! + components += queryComponents(fromKey: key, value: value) + } + return components.map { "\($0)=\($1)" }.joined(separator: "&") + } +} + +// MARK: - + +/// Uses `JSONSerialization` to create a JSON representation of the parameters object, which is set as the body of the +/// request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`. +public struct JSONEncoding: ParameterEncoding { + // MARK: Properties + + /// Returns a `JSONEncoding` instance with default writing options. + public static var `default`: JSONEncoding { JSONEncoding() } + + /// Returns a `JSONEncoding` instance with `.prettyPrinted` writing options. + public static var prettyPrinted: JSONEncoding { JSONEncoding(options: .prettyPrinted) } + + /// The options for writing the parameters as JSON data. + public let options: JSONSerialization.WritingOptions + + // MARK: Initialization + + /// Creates an instance using the specified `WritingOptions`. + /// + /// - Parameter options: `JSONSerialization.WritingOptions` to use. + public init(options: JSONSerialization.WritingOptions = []) { + self.options = options + } + + // MARK: Encoding + + public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { + var urlRequest = try urlRequest.asURLRequest() + + guard let parameters = parameters else { return urlRequest } + + do { + let data = try JSONSerialization.data(withJSONObject: parameters, options: options) + + if urlRequest.headers["Content-Type"] == nil { + urlRequest.headers.update(.contentType("application/json")) + } + + urlRequest.httpBody = data + } catch { + throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) + } + + return urlRequest + } + + /// Encodes any JSON compatible object into a `URLRequest`. + /// + /// - Parameters: + /// - urlRequest: `URLRequestConvertible` value into which the object will be encoded. + /// - jsonObject: `Any` value (must be JSON compatible` to be encoded into the `URLRequest`. `nil` by default. + /// + /// - Returns: The encoded `URLRequest`. + /// - Throws: Any `Error` produced during encoding. + public func encode(_ urlRequest: URLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest { + var urlRequest = try urlRequest.asURLRequest() + + guard let jsonObject = jsonObject else { return urlRequest } + + do { + let data = try JSONSerialization.data(withJSONObject: jsonObject, options: options) + + if urlRequest.headers["Content-Type"] == nil { + urlRequest.headers.update(.contentType("application/json")) + } + + urlRequest.httpBody = data + } catch { + throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) + } + + return urlRequest + } +} + +// MARK: - + +extension NSNumber { + fileprivate var isBool: Bool { + // Use Obj-C type encoding to check whether the underlying type is a `Bool`, as it's guaranteed as part of + // swift-corelibs-foundation, per [this discussion on the Swift forums](https://forums.swift.org/t/alamofire-on-linux-possible-but-not-release-ready/34553/22). + String(cString: objCType) == "c" + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/Protected.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/Protected.swift new file mode 100644 index 0000000..2c056fa --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/Protected.swift @@ -0,0 +1,161 @@ +// +// Protected.swift +// +// Copyright (c) 2014-2020 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +private protocol Lock { + func lock() + func unlock() +} + +extension Lock { + /// Executes a closure returning a value while acquiring the lock. + /// + /// - Parameter closure: The closure to run. + /// + /// - Returns: The value the closure generated. + func around(_ closure: () throws -> T) rethrows -> T { + lock(); defer { unlock() } + return try closure() + } + + /// Execute a closure while acquiring the lock. + /// + /// - Parameter closure: The closure to run. + func around(_ closure: () throws -> Void) rethrows { + lock(); defer { unlock() } + try closure() + } +} + +#if os(Linux) || os(Windows) + +extension NSLock: Lock {} + +#endif + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +/// An `os_unfair_lock` wrapper. +final class UnfairLock: Lock { + private let unfairLock: os_unfair_lock_t + + init() { + unfairLock = .allocate(capacity: 1) + unfairLock.initialize(to: os_unfair_lock()) + } + + deinit { + unfairLock.deinitialize(count: 1) + unfairLock.deallocate() + } + + fileprivate func lock() { + os_unfair_lock_lock(unfairLock) + } + + fileprivate func unlock() { + os_unfair_lock_unlock(unfairLock) + } +} +#endif + +/// A thread-safe wrapper around a value. +@propertyWrapper +@dynamicMemberLookup +final class Protected { + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + private let lock = UnfairLock() + #elseif os(Linux) || os(Windows) + private let lock = NSLock() + #endif + private var value: T + + init(_ value: T) { + self.value = value + } + + /// The contained value. Unsafe for anything more than direct read or write. + var wrappedValue: T { + get { lock.around { value } } + set { lock.around { value = newValue } } + } + + var projectedValue: Protected { self } + + init(wrappedValue: T) { + value = wrappedValue + } + + /// Synchronously read or transform the contained value. + /// + /// - Parameter closure: The closure to execute. + /// + /// - Returns: The return value of the closure passed. + func read(_ closure: (T) throws -> U) rethrows -> U { + try lock.around { try closure(self.value) } + } + + /// Synchronously modify the protected value. + /// + /// - Parameter closure: The closure to execute. + /// + /// - Returns: The modified value. + @discardableResult + func write(_ closure: (inout T) throws -> U) rethrows -> U { + try lock.around { try closure(&self.value) } + } + + subscript(dynamicMember keyPath: WritableKeyPath) -> Property { + get { lock.around { value[keyPath: keyPath] } } + set { lock.around { value[keyPath: keyPath] = newValue } } + } + + subscript(dynamicMember keyPath: KeyPath) -> Property { + lock.around { value[keyPath: keyPath] } + } +} + +extension Protected where T == Request.MutableState { + /// Attempts to transition to the passed `State`. + /// + /// - Parameter state: The `State` to attempt transition to. + /// + /// - Returns: Whether the transition occurred. + func attemptToTransitionTo(_ state: Request.State) -> Bool { + lock.around { + guard value.state.canTransitionTo(state) else { return false } + + value.state = state + + return true + } + } + + /// Perform a closure while locked with the provided `Request.State`. + /// + /// - Parameter perform: The closure to perform while locked. + func withState(perform: (Request.State) -> Void) { + lock.around { perform(value.state) } + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/RedirectHandler.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/RedirectHandler.swift new file mode 100644 index 0000000..5c232b8 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/RedirectHandler.swift @@ -0,0 +1,113 @@ +// +// RedirectHandler.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A type that handles how an HTTP redirect response from a remote server should be redirected to the new request. +public protocol RedirectHandler { + /// Determines how the HTTP redirect response should be redirected to the new request. + /// + /// The `completion` closure should be passed one of three possible options: + /// + /// 1. The new request specified by the redirect (this is the most common use case). + /// 2. A modified version of the new request (you may want to route it somewhere else). + /// 3. A `nil` value to deny the redirect request and return the body of the redirect response. + /// + /// - Parameters: + /// - task: The `URLSessionTask` whose request resulted in a redirect. + /// - request: The `URLRequest` to the new location specified by the redirect response. + /// - response: The `HTTPURLResponse` containing the server's response to the original request. + /// - completion: The closure to execute containing the new `URLRequest`, a modified `URLRequest`, or `nil`. + func task(_ task: URLSessionTask, + willBeRedirectedTo request: URLRequest, + for response: HTTPURLResponse, + completion: @escaping (URLRequest?) -> Void) +} + +// MARK: - + +/// `Redirector` is a convenience `RedirectHandler` making it easy to follow, not follow, or modify a redirect. +public struct Redirector { + /// Defines the behavior of the `Redirector` type. + public enum Behavior { + /// Follow the redirect as defined in the response. + case follow + /// Do not follow the redirect defined in the response. + case doNotFollow + /// Modify the redirect request defined in the response. + case modify((URLSessionTask, URLRequest, HTTPURLResponse) -> URLRequest?) + } + + /// Returns a `Redirector` with a `.follow` `Behavior`. + public static let follow = Redirector(behavior: .follow) + /// Returns a `Redirector` with a `.doNotFollow` `Behavior`. + public static let doNotFollow = Redirector(behavior: .doNotFollow) + + /// The `Behavior` of the `Redirector`. + public let behavior: Behavior + + /// Creates a `Redirector` instance from the `Behavior`. + /// + /// - Parameter behavior: The `Behavior`. + public init(behavior: Behavior) { + self.behavior = behavior + } +} + +// MARK: - + +extension Redirector: RedirectHandler { + public func task(_ task: URLSessionTask, + willBeRedirectedTo request: URLRequest, + for response: HTTPURLResponse, + completion: @escaping (URLRequest?) -> Void) { + switch behavior { + case .follow: + completion(request) + case .doNotFollow: + completion(nil) + case let .modify(closure): + let request = closure(task, request, response) + completion(request) + } + } +} + +#if swift(>=5.5) +extension RedirectHandler where Self == Redirector { + /// Provides a `Redirector` which follows redirects. Equivalent to `Redirector.follow`. + public static var follow: Redirector { .follow } + + /// Provides a `Redirector` which does not follow redirects. Equivalent to `Redirector.doNotFollow`. + public static var doNotFollow: Redirector { .doNotFollow } + + /// Creates a `Redirector` which modifies the redirected `URLRequest` using the provided closure. + /// + /// - Parameter closure: Closure used to modify the redirect. + /// - Returns: The `Redirector`. + public static func modify(using closure: @escaping (URLSessionTask, URLRequest, HTTPURLResponse) -> URLRequest?) -> Redirector { + Redirector(behavior: .modify(closure)) + } +} +#endif diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/Request.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/Request.swift new file mode 100644 index 0000000..7c6e5dc --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/Request.swift @@ -0,0 +1,1912 @@ +// +// Request.swift +// +// Copyright (c) 2014-2020 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// `Request` is the common superclass of all Alamofire request types and provides common state, delegate, and callback +/// handling. +public class Request { + /// State of the `Request`, with managed transitions between states set when calling `resume()`, `suspend()`, or + /// `cancel()` on the `Request`. + public enum State { + /// Initial state of the `Request`. + case initialized + /// `State` set when `resume()` is called. Any tasks created for the `Request` will have `resume()` called on + /// them in this state. + case resumed + /// `State` set when `suspend()` is called. Any tasks created for the `Request` will have `suspend()` called on + /// them in this state. + case suspended + /// `State` set when `cancel()` is called. Any tasks created for the `Request` will have `cancel()` called on + /// them. Unlike `resumed` or `suspended`, once in the `cancelled` state, the `Request` can no longer transition + /// to any other state. + case cancelled + /// `State` set when all response serialization completion closures have been cleared on the `Request` and + /// enqueued on their respective queues. + case finished + + /// Determines whether `self` can be transitioned to the provided `State`. + func canTransitionTo(_ state: State) -> Bool { + switch (self, state) { + case (.initialized, _): + return true + case (_, .initialized), (.cancelled, _), (.finished, _): + return false + case (.resumed, .cancelled), (.suspended, .cancelled), (.resumed, .suspended), (.suspended, .resumed): + return true + case (.suspended, .suspended), (.resumed, .resumed): + return false + case (_, .finished): + return true + } + } + } + + // MARK: - Initial State + + /// `UUID` providing a unique identifier for the `Request`, used in the `Hashable` and `Equatable` conformances. + public let id: UUID + /// The serial queue for all internal async actions. + public let underlyingQueue: DispatchQueue + /// The queue used for all serialization actions. By default it's a serial queue that targets `underlyingQueue`. + public let serializationQueue: DispatchQueue + /// `EventMonitor` used for event callbacks. + public let eventMonitor: EventMonitor? + /// The `Request`'s interceptor. + public let interceptor: RequestInterceptor? + /// The `Request`'s delegate. + public private(set) weak var delegate: RequestDelegate? + + // MARK: - Mutable State + + /// Type encapsulating all mutable state that may need to be accessed from anything other than the `underlyingQueue`. + struct MutableState { + /// State of the `Request`. + var state: State = .initialized + /// `ProgressHandler` and `DispatchQueue` provided for upload progress callbacks. + var uploadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? + /// `ProgressHandler` and `DispatchQueue` provided for download progress callbacks. + var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? + /// `RedirectHandler` provided for to handle request redirection. + var redirectHandler: RedirectHandler? + /// `CachedResponseHandler` provided to handle response caching. + var cachedResponseHandler: CachedResponseHandler? + /// Queue and closure called when the `Request` is able to create a cURL description of itself. + var cURLHandler: (queue: DispatchQueue, handler: (String) -> Void)? + /// Queue and closure called when the `Request` creates a `URLRequest`. + var urlRequestHandler: (queue: DispatchQueue, handler: (URLRequest) -> Void)? + /// Queue and closure called when the `Request` creates a `URLSessionTask`. + var urlSessionTaskHandler: (queue: DispatchQueue, handler: (URLSessionTask) -> Void)? + /// Response serialization closures that handle response parsing. + var responseSerializers: [() -> Void] = [] + /// Response serialization completion closures executed once all response serializers are complete. + var responseSerializerCompletions: [() -> Void] = [] + /// Whether response serializer processing is finished. + var responseSerializerProcessingFinished = false + /// `URLCredential` used for authentication challenges. + var credential: URLCredential? + /// All `URLRequest`s created by Alamofire on behalf of the `Request`. + var requests: [URLRequest] = [] + /// All `URLSessionTask`s created by Alamofire on behalf of the `Request`. + var tasks: [URLSessionTask] = [] + /// All `URLSessionTaskMetrics` values gathered by Alamofire on behalf of the `Request`. Should correspond + /// exactly the the `tasks` created. + var metrics: [URLSessionTaskMetrics] = [] + /// Number of times any retriers provided retried the `Request`. + var retryCount = 0 + /// Final `AFError` for the `Request`, whether from various internal Alamofire calls or as a result of a `task`. + var error: AFError? + /// Whether the instance has had `finish()` called and is running the serializers. Should be replaced with a + /// representation in the state machine in the future. + var isFinishing = false + /// Actions to run when requests are finished. Use for concurrency support. + var finishHandlers: [() -> Void] = [] + } + + /// Protected `MutableState` value that provides thread-safe access to state values. + @Protected + fileprivate var mutableState = MutableState() + + /// `State` of the `Request`. + public var state: State { $mutableState.state } + /// Returns whether `state` is `.initialized`. + public var isInitialized: Bool { state == .initialized } + /// Returns whether `state is `.resumed`. + public var isResumed: Bool { state == .resumed } + /// Returns whether `state` is `.suspended`. + public var isSuspended: Bool { state == .suspended } + /// Returns whether `state` is `.cancelled`. + public var isCancelled: Bool { state == .cancelled } + /// Returns whether `state` is `.finished`. + public var isFinished: Bool { state == .finished } + + // MARK: Progress + + /// Closure type executed when monitoring the upload or download progress of a request. + public typealias ProgressHandler = (Progress) -> Void + + /// `Progress` of the upload of the body of the executed `URLRequest`. Reset to `0` if the `Request` is retried. + public let uploadProgress = Progress(totalUnitCount: 0) + /// `Progress` of the download of any response data. Reset to `0` if the `Request` is retried. + public let downloadProgress = Progress(totalUnitCount: 0) + /// `ProgressHandler` called when `uploadProgress` is updated, on the provided `DispatchQueue`. + private var uploadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? { + get { $mutableState.uploadProgressHandler } + set { $mutableState.uploadProgressHandler = newValue } + } + + /// `ProgressHandler` called when `downloadProgress` is updated, on the provided `DispatchQueue`. + fileprivate var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? { + get { $mutableState.downloadProgressHandler } + set { $mutableState.downloadProgressHandler = newValue } + } + + // MARK: Redirect Handling + + /// `RedirectHandler` set on the instance. + public private(set) var redirectHandler: RedirectHandler? { + get { $mutableState.redirectHandler } + set { $mutableState.redirectHandler = newValue } + } + + // MARK: Cached Response Handling + + /// `CachedResponseHandler` set on the instance. + public private(set) var cachedResponseHandler: CachedResponseHandler? { + get { $mutableState.cachedResponseHandler } + set { $mutableState.cachedResponseHandler = newValue } + } + + // MARK: URLCredential + + /// `URLCredential` used for authentication challenges. Created by calling one of the `authenticate` methods. + public private(set) var credential: URLCredential? { + get { $mutableState.credential } + set { $mutableState.credential = newValue } + } + + // MARK: Validators + + /// `Validator` callback closures that store the validation calls enqueued. + @Protected + fileprivate var validators: [() -> Void] = [] + + // MARK: URLRequests + + /// All `URLRequests` created on behalf of the `Request`, including original and adapted requests. + public var requests: [URLRequest] { $mutableState.requests } + /// First `URLRequest` created on behalf of the `Request`. May not be the first one actually executed. + public var firstRequest: URLRequest? { requests.first } + /// Last `URLRequest` created on behalf of the `Request`. + public var lastRequest: URLRequest? { requests.last } + /// Current `URLRequest` created on behalf of the `Request`. + public var request: URLRequest? { lastRequest } + + /// `URLRequest`s from all of the `URLSessionTask`s executed on behalf of the `Request`. May be different from + /// `requests` due to `URLSession` manipulation. + public var performedRequests: [URLRequest] { $mutableState.read { $0.tasks.compactMap(\.currentRequest) } } + + // MARK: HTTPURLResponse + + /// `HTTPURLResponse` received from the server, if any. If the `Request` was retried, this is the response of the + /// last `URLSessionTask`. + public var response: HTTPURLResponse? { lastTask?.response as? HTTPURLResponse } + + // MARK: Tasks + + /// All `URLSessionTask`s created on behalf of the `Request`. + public var tasks: [URLSessionTask] { $mutableState.tasks } + /// First `URLSessionTask` created on behalf of the `Request`. + public var firstTask: URLSessionTask? { tasks.first } + /// Last `URLSessionTask` crated on behalf of the `Request`. + public var lastTask: URLSessionTask? { tasks.last } + /// Current `URLSessionTask` created on behalf of the `Request`. + public var task: URLSessionTask? { lastTask } + + // MARK: Metrics + + /// All `URLSessionTaskMetrics` gathered on behalf of the `Request`. Should correspond to the `tasks` created. + public var allMetrics: [URLSessionTaskMetrics] { $mutableState.metrics } + /// First `URLSessionTaskMetrics` gathered on behalf of the `Request`. + public var firstMetrics: URLSessionTaskMetrics? { allMetrics.first } + /// Last `URLSessionTaskMetrics` gathered on behalf of the `Request`. + public var lastMetrics: URLSessionTaskMetrics? { allMetrics.last } + /// Current `URLSessionTaskMetrics` gathered on behalf of the `Request`. + public var metrics: URLSessionTaskMetrics? { lastMetrics } + + // MARK: Retry Count + + /// Number of times the `Request` has been retried. + public var retryCount: Int { $mutableState.retryCount } + + // MARK: Error + + /// `Error` returned from Alamofire internally, from the network request directly, or any validators executed. + public fileprivate(set) var error: AFError? { + get { $mutableState.error } + set { $mutableState.error = newValue } + } + + /// Default initializer for the `Request` superclass. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. + init(id: UUID = UUID(), + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + delegate: RequestDelegate) { + self.id = id + self.underlyingQueue = underlyingQueue + self.serializationQueue = serializationQueue + self.eventMonitor = eventMonitor + self.interceptor = interceptor + self.delegate = delegate + } + + // MARK: - Internal Event API + + // All API must be called from underlyingQueue. + + /// Called when an initial `URLRequest` has been created on behalf of the instance. If a `RequestAdapter` is active, + /// the `URLRequest` will be adapted before being issued. + /// + /// - Parameter request: The `URLRequest` created. + func didCreateInitialURLRequest(_ request: URLRequest) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + $mutableState.write { $0.requests.append(request) } + + eventMonitor?.request(self, didCreateInitialURLRequest: request) + } + + /// Called when initial `URLRequest` creation has failed, typically through a `URLRequestConvertible`. + /// + /// - Note: Triggers retry. + /// + /// - Parameter error: `AFError` thrown from the failed creation. + func didFailToCreateURLRequest(with error: AFError) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + self.error = error + + eventMonitor?.request(self, didFailToCreateURLRequestWithError: error) + + callCURLHandlerIfNecessary() + + retryOrFinish(error: error) + } + + /// Called when a `RequestAdapter` has successfully adapted a `URLRequest`. + /// + /// - Parameters: + /// - initialRequest: The `URLRequest` that was adapted. + /// - adaptedRequest: The `URLRequest` returned by the `RequestAdapter`. + func didAdaptInitialRequest(_ initialRequest: URLRequest, to adaptedRequest: URLRequest) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + $mutableState.write { $0.requests.append(adaptedRequest) } + + eventMonitor?.request(self, didAdaptInitialRequest: initialRequest, to: adaptedRequest) + } + + /// Called when a `RequestAdapter` fails to adapt a `URLRequest`. + /// + /// - Note: Triggers retry. + /// + /// - Parameters: + /// - request: The `URLRequest` the adapter was called with. + /// - error: The `AFError` returned by the `RequestAdapter`. + func didFailToAdaptURLRequest(_ request: URLRequest, withError error: AFError) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + self.error = error + + eventMonitor?.request(self, didFailToAdaptURLRequest: request, withError: error) + + callCURLHandlerIfNecessary() + + retryOrFinish(error: error) + } + + /// Final `URLRequest` has been created for the instance. + /// + /// - Parameter request: The `URLRequest` created. + func didCreateURLRequest(_ request: URLRequest) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + $mutableState.read { state in + state.urlRequestHandler?.queue.async { state.urlRequestHandler?.handler(request) } + } + + eventMonitor?.request(self, didCreateURLRequest: request) + + callCURLHandlerIfNecessary() + } + + /// Asynchronously calls any stored `cURLHandler` and then removes it from `mutableState`. + private func callCURLHandlerIfNecessary() { + $mutableState.write { mutableState in + guard let cURLHandler = mutableState.cURLHandler else { return } + + cURLHandler.queue.async { cURLHandler.handler(self.cURLDescription()) } + + mutableState.cURLHandler = nil + } + } + + /// Called when a `URLSessionTask` is created on behalf of the instance. + /// + /// - Parameter task: The `URLSessionTask` created. + func didCreateTask(_ task: URLSessionTask) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + $mutableState.write { state in + state.tasks.append(task) + + guard let urlSessionTaskHandler = state.urlSessionTaskHandler else { return } + + urlSessionTaskHandler.queue.async { urlSessionTaskHandler.handler(task) } + } + + eventMonitor?.request(self, didCreateTask: task) + } + + /// Called when resumption is completed. + func didResume() { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.requestDidResume(self) + } + + /// Called when a `URLSessionTask` is resumed on behalf of the instance. + /// + /// - Parameter task: The `URLSessionTask` resumed. + func didResumeTask(_ task: URLSessionTask) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.request(self, didResumeTask: task) + } + + /// Called when suspension is completed. + func didSuspend() { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.requestDidSuspend(self) + } + + /// Called when a `URLSessionTask` is suspended on behalf of the instance. + /// + /// - Parameter task: The `URLSessionTask` suspended. + func didSuspendTask(_ task: URLSessionTask) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.request(self, didSuspendTask: task) + } + + /// Called when cancellation is completed, sets `error` to `AFError.explicitlyCancelled`. + func didCancel() { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + error = error ?? AFError.explicitlyCancelled + + eventMonitor?.requestDidCancel(self) + } + + /// Called when a `URLSessionTask` is cancelled on behalf of the instance. + /// + /// - Parameter task: The `URLSessionTask` cancelled. + func didCancelTask(_ task: URLSessionTask) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.request(self, didCancelTask: task) + } + + /// Called when a `URLSessionTaskMetrics` value is gathered on behalf of the instance. + /// + /// - Parameter metrics: The `URLSessionTaskMetrics` gathered. + func didGatherMetrics(_ metrics: URLSessionTaskMetrics) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + $mutableState.write { $0.metrics.append(metrics) } + + eventMonitor?.request(self, didGatherMetrics: metrics) + } + + /// Called when a `URLSessionTask` fails before it is finished, typically during certificate pinning. + /// + /// - Parameters: + /// - task: The `URLSessionTask` which failed. + /// - error: The early failure `AFError`. + func didFailTask(_ task: URLSessionTask, earlyWithError error: AFError) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + self.error = error + + // Task will still complete, so didCompleteTask(_:with:) will handle retry. + eventMonitor?.request(self, didFailTask: task, earlyWithError: error) + } + + /// Called when a `URLSessionTask` completes. All tasks will eventually call this method. + /// + /// - Note: Response validation is synchronously triggered in this step. + /// + /// - Parameters: + /// - task: The `URLSessionTask` which completed. + /// - error: The `AFError` `task` may have completed with. If `error` has already been set on the instance, this + /// value is ignored. + func didCompleteTask(_ task: URLSessionTask, with error: AFError?) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + self.error = self.error ?? error + + validators.forEach { $0() } + + eventMonitor?.request(self, didCompleteTask: task, with: error) + + retryOrFinish(error: self.error) + } + + /// Called when the `RequestDelegate` is going to retry this `Request`. Calls `reset()`. + func prepareForRetry() { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + $mutableState.write { $0.retryCount += 1 } + + reset() + + eventMonitor?.requestIsRetrying(self) + } + + /// Called to determine whether retry will be triggered for the particular error, or whether the instance should + /// call `finish()`. + /// + /// - Parameter error: The possible `AFError` which may trigger retry. + func retryOrFinish(error: AFError?) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + guard let error = error, let delegate = delegate else { finish(); return } + + delegate.retryResult(for: self, dueTo: error) { retryResult in + switch retryResult { + case .doNotRetry: + self.finish() + case let .doNotRetryWithError(retryError): + self.finish(error: retryError.asAFError(orFailWith: "Received retryError was not already AFError")) + case .retry, .retryWithDelay: + delegate.retryRequest(self, withDelay: retryResult.delay) + } + } + } + + /// Finishes this `Request` and starts the response serializers. + /// + /// - Parameter error: The possible `Error` with which the instance will finish. + func finish(error: AFError? = nil) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + guard !$mutableState.isFinishing else { return } + + $mutableState.isFinishing = true + + if let error = error { self.error = error } + + // Start response handlers + processNextResponseSerializer() + + eventMonitor?.requestDidFinish(self) + } + + /// Appends the response serialization closure to the instance. + /// + /// - Note: This method will also `resume` the instance if `delegate.startImmediately` returns `true`. + /// + /// - Parameter closure: The closure containing the response serialization call. + func appendResponseSerializer(_ closure: @escaping () -> Void) { + $mutableState.write { mutableState in + mutableState.responseSerializers.append(closure) + + if mutableState.state == .finished { + mutableState.state = .resumed + } + + if mutableState.responseSerializerProcessingFinished { + underlyingQueue.async { self.processNextResponseSerializer() } + } + + if mutableState.state.canTransitionTo(.resumed) { + underlyingQueue.async { if self.delegate?.startImmediately == true { self.resume() } } + } + } + } + + /// Returns the next response serializer closure to execute if there's one left. + /// + /// - Returns: The next response serialization closure, if there is one. + func nextResponseSerializer() -> (() -> Void)? { + var responseSerializer: (() -> Void)? + + $mutableState.write { mutableState in + let responseSerializerIndex = mutableState.responseSerializerCompletions.count + + if responseSerializerIndex < mutableState.responseSerializers.count { + responseSerializer = mutableState.responseSerializers[responseSerializerIndex] + } + } + + return responseSerializer + } + + /// Processes the next response serializer and calls all completions if response serialization is complete. + func processNextResponseSerializer() { + guard let responseSerializer = nextResponseSerializer() else { + // Execute all response serializer completions and clear them + var completions: [() -> Void] = [] + + $mutableState.write { mutableState in + completions = mutableState.responseSerializerCompletions + + // Clear out all response serializers and response serializer completions in mutable state since the + // request is complete. It's important to do this prior to calling the completion closures in case + // the completions call back into the request triggering a re-processing of the response serializers. + // An example of how this can happen is by calling cancel inside a response completion closure. + mutableState.responseSerializers.removeAll() + mutableState.responseSerializerCompletions.removeAll() + + if mutableState.state.canTransitionTo(.finished) { + mutableState.state = .finished + } + + mutableState.responseSerializerProcessingFinished = true + mutableState.isFinishing = false + } + + completions.forEach { $0() } + + // Cleanup the request + cleanup() + + return + } + + serializationQueue.async { responseSerializer() } + } + + /// Notifies the `Request` that the response serializer is complete. + /// + /// - Parameter completion: The completion handler provided with the response serializer, called when all serializers + /// are complete. + func responseSerializerDidComplete(completion: @escaping () -> Void) { + $mutableState.write { $0.responseSerializerCompletions.append(completion) } + processNextResponseSerializer() + } + + /// Resets all task and response serializer related state for retry. + func reset() { + error = nil + + uploadProgress.totalUnitCount = 0 + uploadProgress.completedUnitCount = 0 + downloadProgress.totalUnitCount = 0 + downloadProgress.completedUnitCount = 0 + + $mutableState.write { state in + state.isFinishing = false + state.responseSerializerCompletions = [] + } + } + + /// Called when updating the upload progress. + /// + /// - Parameters: + /// - totalBytesSent: Total bytes sent so far. + /// - totalBytesExpectedToSend: Total bytes expected to send. + func updateUploadProgress(totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + uploadProgress.totalUnitCount = totalBytesExpectedToSend + uploadProgress.completedUnitCount = totalBytesSent + + uploadProgressHandler?.queue.async { self.uploadProgressHandler?.handler(self.uploadProgress) } + } + + /// Perform a closure on the current `state` while locked. + /// + /// - Parameter perform: The closure to perform. + func withState(perform: (State) -> Void) { + $mutableState.withState(perform: perform) + } + + // MARK: Task Creation + + /// Called when creating a `URLSessionTask` for this `Request`. Subclasses must override. + /// + /// - Parameters: + /// - request: `URLRequest` to use to create the `URLSessionTask`. + /// - session: `URLSession` which creates the `URLSessionTask`. + /// + /// - Returns: The `URLSessionTask` created. + func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + fatalError("Subclasses must override.") + } + + // MARK: - Public API + + // These APIs are callable from any queue. + + // MARK: State + + /// Cancels the instance. Once cancelled, a `Request` can no longer be resumed or suspended. + /// + /// - Returns: The instance. + @discardableResult + public func cancel() -> Self { + $mutableState.write { mutableState in + guard mutableState.state.canTransitionTo(.cancelled) else { return } + + mutableState.state = .cancelled + + underlyingQueue.async { self.didCancel() } + + guard let task = mutableState.tasks.last, task.state != .completed else { + underlyingQueue.async { self.finish() } + return + } + + // Resume to ensure metrics are gathered. + task.resume() + task.cancel() + underlyingQueue.async { self.didCancelTask(task) } + } + + return self + } + + /// Suspends the instance. + /// + /// - Returns: The instance. + @discardableResult + public func suspend() -> Self { + $mutableState.write { mutableState in + guard mutableState.state.canTransitionTo(.suspended) else { return } + + mutableState.state = .suspended + + underlyingQueue.async { self.didSuspend() } + + guard let task = mutableState.tasks.last, task.state != .completed else { return } + + task.suspend() + underlyingQueue.async { self.didSuspendTask(task) } + } + + return self + } + + /// Resumes the instance. + /// + /// - Returns: The instance. + @discardableResult + public func resume() -> Self { + $mutableState.write { mutableState in + guard mutableState.state.canTransitionTo(.resumed) else { return } + + mutableState.state = .resumed + + underlyingQueue.async { self.didResume() } + + guard let task = mutableState.tasks.last, task.state != .completed else { return } + + task.resume() + underlyingQueue.async { self.didResumeTask(task) } + } + + return self + } + + // MARK: - Closure API + + /// Associates a credential using the provided values with the instance. + /// + /// - Parameters: + /// - username: The username. + /// - password: The password. + /// - persistence: The `URLCredential.Persistence` for the created `URLCredential`. `.forSession` by default. + /// + /// - Returns: The instance. + @discardableResult + public func authenticate(username: String, password: String, persistence: URLCredential.Persistence = .forSession) -> Self { + let credential = URLCredential(user: username, password: password, persistence: persistence) + + return authenticate(with: credential) + } + + /// Associates the provided credential with the instance. + /// + /// - Parameter credential: The `URLCredential`. + /// + /// - Returns: The instance. + @discardableResult + public func authenticate(with credential: URLCredential) -> Self { + $mutableState.credential = credential + + return self + } + + /// Sets a closure to be called periodically during the lifecycle of the instance as data is read from the server. + /// + /// - Note: Only the last closure provided is used. + /// + /// - Parameters: + /// - queue: The `DispatchQueue` to execute the closure on. `.main` by default. + /// - closure: The closure to be executed periodically as data is read from the server. + /// + /// - Returns: The instance. + @discardableResult + public func downloadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self { + $mutableState.downloadProgressHandler = (handler: closure, queue: queue) + + return self + } + + /// Sets a closure to be called periodically during the lifecycle of the instance as data is sent to the server. + /// + /// - Note: Only the last closure provided is used. + /// + /// - Parameters: + /// - queue: The `DispatchQueue` to execute the closure on. `.main` by default. + /// - closure: The closure to be executed periodically as data is sent to the server. + /// + /// - Returns: The instance. + @discardableResult + public func uploadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self { + $mutableState.uploadProgressHandler = (handler: closure, queue: queue) + + return self + } + + // MARK: Redirects + + /// Sets the redirect handler for the instance which will be used if a redirect response is encountered. + /// + /// - Note: Attempting to set the redirect handler more than once is a logic error and will crash. + /// + /// - Parameter handler: The `RedirectHandler`. + /// + /// - Returns: The instance. + @discardableResult + public func redirect(using handler: RedirectHandler) -> Self { + $mutableState.write { mutableState in + precondition(mutableState.redirectHandler == nil, "Redirect handler has already been set.") + mutableState.redirectHandler = handler + } + + return self + } + + // MARK: Cached Responses + + /// Sets the cached response handler for the `Request` which will be used when attempting to cache a response. + /// + /// - Note: Attempting to set the cache handler more than once is a logic error and will crash. + /// + /// - Parameter handler: The `CachedResponseHandler`. + /// + /// - Returns: The instance. + @discardableResult + public func cacheResponse(using handler: CachedResponseHandler) -> Self { + $mutableState.write { mutableState in + precondition(mutableState.cachedResponseHandler == nil, "Cached response handler has already been set.") + mutableState.cachedResponseHandler = handler + } + + return self + } + + // MARK: - Lifetime APIs + + /// Sets a handler to be called when the cURL description of the request is available. + /// + /// - Note: When waiting for a `Request`'s `URLRequest` to be created, only the last `handler` will be called. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which `handler` will be called. + /// - handler: Closure to be called when the cURL description is available. + /// + /// - Returns: The instance. + @discardableResult + public func cURLDescription(on queue: DispatchQueue, calling handler: @escaping (String) -> Void) -> Self { + $mutableState.write { mutableState in + if mutableState.requests.last != nil { + queue.async { handler(self.cURLDescription()) } + } else { + mutableState.cURLHandler = (queue, handler) + } + } + + return self + } + + /// Sets a handler to be called when the cURL description of the request is available. + /// + /// - Note: When waiting for a `Request`'s `URLRequest` to be created, only the last `handler` will be called. + /// + /// - Parameter handler: Closure to be called when the cURL description is available. Called on the instance's + /// `underlyingQueue` by default. + /// + /// - Returns: The instance. + @discardableResult + public func cURLDescription(calling handler: @escaping (String) -> Void) -> Self { + $mutableState.write { mutableState in + if mutableState.requests.last != nil { + underlyingQueue.async { handler(self.cURLDescription()) } + } else { + mutableState.cURLHandler = (underlyingQueue, handler) + } + } + + return self + } + + /// Sets a closure to called whenever Alamofire creates a `URLRequest` for this instance. + /// + /// - Note: This closure will be called multiple times if the instance adapts incoming `URLRequest`s or is retried. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which `handler` will be called. `.main` by default. + /// - handler: Closure to be called when a `URLRequest` is available. + /// + /// - Returns: The instance. + @discardableResult + public func onURLRequestCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLRequest) -> Void) -> Self { + $mutableState.write { state in + if let request = state.requests.last { + queue.async { handler(request) } + } + + state.urlRequestHandler = (queue, handler) + } + + return self + } + + /// Sets a closure to be called whenever the instance creates a `URLSessionTask`. + /// + /// - Note: This API should only be used to provide `URLSessionTask`s to existing API, like `NSFileProvider`. It + /// **SHOULD NOT** be used to interact with tasks directly, as that may be break Alamofire features. + /// Additionally, this closure may be called multiple times if the instance is retried. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which `handler` will be called. `.main` by default. + /// - handler: Closure to be called when the `URLSessionTask` is available. + /// + /// - Returns: The instance. + @discardableResult + public func onURLSessionTaskCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLSessionTask) -> Void) -> Self { + $mutableState.write { state in + if let task = state.tasks.last { + queue.async { handler(task) } + } + + state.urlSessionTaskHandler = (queue, handler) + } + + return self + } + + // MARK: Cleanup + + /// Adds a `finishHandler` closure to be called when the request completes. + /// + /// - Parameter closure: Closure to be called when the request finishes. + func onFinish(perform finishHandler: @escaping () -> Void) { + guard !isFinished else { finishHandler(); return } + + $mutableState.write { state in + state.finishHandlers.append(finishHandler) + } + } + + /// Final cleanup step executed when the instance finishes response serialization. + func cleanup() { + delegate?.cleanup(after: self) + let handlers = $mutableState.finishHandlers + handlers.forEach { $0() } + $mutableState.write { state in + state.finishHandlers.removeAll() + } + } +} + +// MARK: - Protocol Conformances + +extension Request: Equatable { + public static func ==(lhs: Request, rhs: Request) -> Bool { + lhs.id == rhs.id + } +} + +extension Request: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension Request: CustomStringConvertible { + /// A textual representation of this instance, including the `HTTPMethod` and `URL` if the `URLRequest` has been + /// created, as well as the response status code, if a response has been received. + public var description: String { + guard let request = performedRequests.last ?? lastRequest, + let url = request.url, + let method = request.httpMethod else { return "No request created yet." } + + let requestDescription = "\(method) \(url.absoluteString)" + + return response.map { "\(requestDescription) (\($0.statusCode))" } ?? requestDescription + } +} + +extension Request { + /// cURL representation of the instance. + /// + /// - Returns: The cURL equivalent of the instance. + public func cURLDescription() -> String { + guard + let request = lastRequest, + let url = request.url, + let host = url.host, + let method = request.httpMethod else { return "$ curl command could not be created" } + + var components = ["$ curl -v"] + + components.append("-X \(method)") + + if let credentialStorage = delegate?.sessionConfiguration.urlCredentialStorage { + let protectionSpace = URLProtectionSpace(host: host, + port: url.port ?? 0, + protocol: url.scheme, + realm: host, + authenticationMethod: NSURLAuthenticationMethodHTTPBasic) + + if let credentials = credentialStorage.credentials(for: protectionSpace)?.values { + for credential in credentials { + guard let user = credential.user, let password = credential.password else { continue } + components.append("-u \(user):\(password)") + } + } else { + if let credential = credential, let user = credential.user, let password = credential.password { + components.append("-u \(user):\(password)") + } + } + } + + if let configuration = delegate?.sessionConfiguration, configuration.httpShouldSetCookies { + if + let cookieStorage = configuration.httpCookieStorage, + let cookies = cookieStorage.cookies(for: url), !cookies.isEmpty { + let allCookies = cookies.map { "\($0.name)=\($0.value)" }.joined(separator: ";") + + components.append("-b \"\(allCookies)\"") + } + } + + var headers = HTTPHeaders() + + if let sessionHeaders = delegate?.sessionConfiguration.headers { + for header in sessionHeaders where header.name != "Cookie" { + headers[header.name] = header.value + } + } + + for header in request.headers where header.name != "Cookie" { + headers[header.name] = header.value + } + + for header in headers { + let escapedValue = header.value.replacingOccurrences(of: "\"", with: "\\\"") + components.append("-H \"\(header.name): \(escapedValue)\"") + } + + if let httpBodyData = request.httpBody { + let httpBody = String(decoding: httpBodyData, as: UTF8.self) + var escapedBody = httpBody.replacingOccurrences(of: "\\\"", with: "\\\\\"") + escapedBody = escapedBody.replacingOccurrences(of: "\"", with: "\\\"") + + components.append("-d \"\(escapedBody)\"") + } + + components.append("\"\(url.absoluteString)\"") + + return components.joined(separator: " \\\n\t") + } +} + +/// Protocol abstraction for `Request`'s communication back to the `SessionDelegate`. +public protocol RequestDelegate: AnyObject { + /// `URLSessionConfiguration` used to create the underlying `URLSessionTask`s. + var sessionConfiguration: URLSessionConfiguration { get } + + /// Determines whether the `Request` should automatically call `resume()` when adding the first response handler. + var startImmediately: Bool { get } + + /// Notifies the delegate the `Request` has reached a point where it needs cleanup. + /// + /// - Parameter request: The `Request` to cleanup after. + func cleanup(after request: Request) + + /// Asynchronously ask the delegate whether a `Request` will be retried. + /// + /// - Parameters: + /// - request: `Request` which failed. + /// - error: `Error` which produced the failure. + /// - completion: Closure taking the `RetryResult` for evaluation. + func retryResult(for request: Request, dueTo error: AFError, completion: @escaping (RetryResult) -> Void) + + /// Asynchronously retry the `Request`. + /// + /// - Parameters: + /// - request: `Request` which will be retried. + /// - timeDelay: `TimeInterval` after which the retry will be triggered. + func retryRequest(_ request: Request, withDelay timeDelay: TimeInterval?) +} + +// MARK: - Subclasses + +// MARK: - DataRequest + +/// `Request` subclass which handles in-memory `Data` download using `URLSessionDataTask`. +public class DataRequest: Request { + /// `URLRequestConvertible` value used to create `URLRequest`s for this instance. + public let convertible: URLRequestConvertible + /// `Data` read from the server so far. + public var data: Data? { mutableData } + + /// Protected storage for the `Data` read by the instance. + @Protected + private var mutableData: Data? = nil + + /// Creates a `DataRequest` using the provided parameters. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. + /// - convertible: `URLRequestConvertible` value used to create `URLRequest`s for this instance. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. + init(id: UUID = UUID(), + convertible: URLRequestConvertible, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + delegate: RequestDelegate) { + self.convertible = convertible + + super.init(id: id, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: delegate) + } + + override func reset() { + super.reset() + + mutableData = nil + } + + /// Called when `Data` is received by this instance. + /// + /// - Note: Also calls `updateDownloadProgress`. + /// + /// - Parameter data: The `Data` received. + func didReceive(data: Data) { + if self.data == nil { + mutableData = data + } else { + $mutableData.write { $0?.append(data) } + } + + updateDownloadProgress() + } + + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + let copiedRequest = request + return session.dataTask(with: copiedRequest) + } + + /// Called to updated the `downloadProgress` of the instance. + func updateDownloadProgress() { + let totalBytesReceived = Int64(data?.count ?? 0) + let totalBytesExpected = task?.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown + + downloadProgress.totalUnitCount = totalBytesExpected + downloadProgress.completedUnitCount = totalBytesReceived + + downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) } + } + + /// Validates the request, using the specified closure. + /// + /// - Note: If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - Parameter validation: `Validation` closure used to validate the response. + /// + /// - Returns: The instance. + @discardableResult + public func validate(_ validation: @escaping Validation) -> Self { + let validator: () -> Void = { [unowned self] in + guard self.error == nil, let response = self.response else { return } + + let result = validation(self.request, response, self.data) + + if case let .failure(error) = result { self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) } + + self.eventMonitor?.request(self, + didValidateRequest: self.request, + response: response, + data: self.data, + withResult: result) + } + + $validators.write { $0.append(validator) } + + return self + } +} + +// MARK: - DataStreamRequest + +/// `Request` subclass which streams HTTP response `Data` through a `Handler` closure. +public final class DataStreamRequest: Request { + /// Closure type handling `DataStreamRequest.Stream` values. + public typealias Handler = (Stream) throws -> Void + + /// Type encapsulating an `Event` as it flows through the stream, as well as a `CancellationToken` which can be used + /// to stop the stream at any time. + public struct Stream { + /// Latest `Event` from the stream. + public let event: Event + /// Token used to cancel the stream. + public let token: CancellationToken + + /// Cancel the ongoing stream by canceling the underlying `DataStreamRequest`. + public func cancel() { + token.cancel() + } + } + + /// Type representing an event flowing through the stream. Contains either the `Result` of processing streamed + /// `Data` or the completion of the stream. + public enum Event { + /// Output produced every time the instance receives additional `Data`. The associated value contains the + /// `Result` of processing the incoming `Data`. + case stream(Result) + /// Output produced when the instance has completed, whether due to stream end, cancellation, or an error. + /// Associated `Completion` value contains the final state. + case complete(Completion) + } + + /// Value containing the state of a `DataStreamRequest` when the stream was completed. + public struct Completion { + /// Last `URLRequest` issued by the instance. + public let request: URLRequest? + /// Last `HTTPURLResponse` received by the instance. + public let response: HTTPURLResponse? + /// Last `URLSessionTaskMetrics` produced for the instance. + public let metrics: URLSessionTaskMetrics? + /// `AFError` produced for the instance, if any. + public let error: AFError? + } + + /// Type used to cancel an ongoing stream. + public struct CancellationToken { + weak var request: DataStreamRequest? + + init(_ request: DataStreamRequest) { + self.request = request + } + + /// Cancel the ongoing stream by canceling the underlying `DataStreamRequest`. + public func cancel() { + request?.cancel() + } + } + + /// `URLRequestConvertible` value used to create `URLRequest`s for this instance. + public let convertible: URLRequestConvertible + /// Whether or not the instance will be cancelled if stream parsing encounters an error. + public let automaticallyCancelOnStreamError: Bool + + /// Internal mutable state specific to this type. + struct StreamMutableState { + /// `OutputStream` bound to the `InputStream` produced by `asInputStream`, if it has been called. + var outputStream: OutputStream? + /// Stream closures called as `Data` is received. + var streams: [(_ data: Data) -> Void] = [] + /// Number of currently executing streams. Used to ensure completions are only fired after all streams are + /// enqueued. + var numberOfExecutingStreams = 0 + /// Completion calls enqueued while streams are still executing. + var enqueuedCompletionEvents: [() -> Void] = [] + } + + @Protected + var streamMutableState = StreamMutableState() + + /// Creates a `DataStreamRequest` using the provided parameters. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` + /// by default. + /// - convertible: `URLRequestConvertible` value used to create `URLRequest`s for this + /// instance. + /// - automaticallyCancelOnStreamError: `Bool` indicating whether the instance will be cancelled when an `Error` + /// is thrown while serializing stream `Data`. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default + /// targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by + /// the `Request`. + init(id: UUID = UUID(), + convertible: URLRequestConvertible, + automaticallyCancelOnStreamError: Bool, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + delegate: RequestDelegate) { + self.convertible = convertible + self.automaticallyCancelOnStreamError = automaticallyCancelOnStreamError + + super.init(id: id, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: delegate) + } + + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + let copiedRequest = request + return session.dataTask(with: copiedRequest) + } + + override func finish(error: AFError? = nil) { + $streamMutableState.write { state in + state.outputStream?.close() + } + + super.finish(error: error) + } + + func didReceive(data: Data) { + $streamMutableState.write { state in + #if !(os(Linux) || os(Windows)) + if let stream = state.outputStream { + underlyingQueue.async { + var bytes = Array(data) + stream.write(&bytes, maxLength: bytes.count) + } + } + #endif + state.numberOfExecutingStreams += state.streams.count + let localState = state + underlyingQueue.async { localState.streams.forEach { $0(data) } } + } + } + + /// Validates the `URLRequest` and `HTTPURLResponse` received for the instance using the provided `Validation` closure. + /// + /// - Parameter validation: `Validation` closure used to validate the request and response. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func validate(_ validation: @escaping Validation) -> Self { + let validator: () -> Void = { [unowned self] in + guard self.error == nil, let response = self.response else { return } + + let result = validation(self.request, response) + + if case let .failure(error) = result { + self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) + } + + self.eventMonitor?.request(self, + didValidateRequest: self.request, + response: response, + withResult: result) + } + + $validators.write { $0.append(validator) } + + return self + } + + #if !(os(Linux) || os(Windows)) + /// Produces an `InputStream` that receives the `Data` received by the instance. + /// + /// - Note: The `InputStream` produced by this method must have `open()` called before being able to read `Data`. + /// Additionally, this method will automatically call `resume()` on the instance, regardless of whether or + /// not the creating session has `startRequestsImmediately` set to `true`. + /// + /// - Parameter bufferSize: Size, in bytes, of the buffer between the `OutputStream` and `InputStream`. + /// + /// - Returns: The `InputStream` bound to the internal `OutboundStream`. + public func asInputStream(bufferSize: Int = 1024) -> InputStream? { + defer { resume() } + + var inputStream: InputStream? + $streamMutableState.write { state in + Foundation.Stream.getBoundStreams(withBufferSize: bufferSize, + inputStream: &inputStream, + outputStream: &state.outputStream) + state.outputStream?.open() + } + + return inputStream + } + #endif + + func capturingError(from closure: () throws -> Void) { + do { + try closure() + } catch { + self.error = error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error))) + cancel() + } + } + + func appendStreamCompletion(on queue: DispatchQueue, + stream: @escaping Handler) { + appendResponseSerializer { + self.underlyingQueue.async { + self.responseSerializerDidComplete { + self.$streamMutableState.write { state in + guard state.numberOfExecutingStreams == 0 else { + state.enqueuedCompletionEvents.append { + self.enqueueCompletion(on: queue, stream: stream) + } + + return + } + + self.enqueueCompletion(on: queue, stream: stream) + } + } + } + } + } + + func enqueueCompletion(on queue: DispatchQueue, + stream: @escaping Handler) { + queue.async { + do { + let completion = Completion(request: self.request, + response: self.response, + metrics: self.metrics, + error: self.error) + try stream(.init(event: .complete(completion), token: .init(self))) + } catch { + // Ignore error, as errors on Completion can't be handled anyway. + } + } + } +} + +extension DataStreamRequest.Stream { + /// Incoming `Result` values from `Event.stream`. + public var result: Result? { + guard case let .stream(result) = event else { return nil } + + return result + } + + /// `Success` value of the instance, if any. + public var value: Success? { + guard case let .success(value) = result else { return nil } + + return value + } + + /// `Failure` value of the instance, if any. + public var error: Failure? { + guard case let .failure(error) = result else { return nil } + + return error + } + + /// `Completion` value of the instance, if any. + public var completion: DataStreamRequest.Completion? { + guard case let .complete(completion) = event else { return nil } + + return completion + } +} + +// MARK: - DownloadRequest + +/// `Request` subclass which downloads `Data` to a file on disk using `URLSessionDownloadTask`. +public class DownloadRequest: Request { + /// A set of options to be executed prior to moving a downloaded file from the temporary `URL` to the destination + /// `URL`. + public struct Options: OptionSet { + /// Specifies that intermediate directories for the destination URL should be created. + public static let createIntermediateDirectories = Options(rawValue: 1 << 0) + /// Specifies that any previous file at the destination `URL` should be removed. + public static let removePreviousFile = Options(rawValue: 1 << 1) + + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + } + + // MARK: Destination + + /// A closure executed once a `DownloadRequest` has successfully completed in order to determine where to move the + /// temporary file written to during the download process. The closure takes two arguments: the temporary file URL + /// and the `HTTPURLResponse`, and returns two values: the file URL where the temporary file should be moved and + /// the options defining how the file should be moved. + /// + /// - Note: Downloads from a local `file://` `URL`s do not use the `Destination` closure, as those downloads do not + /// return an `HTTPURLResponse`. Instead the file is merely moved within the temporary directory. + public typealias Destination = (_ temporaryURL: URL, + _ response: HTTPURLResponse) -> (destinationURL: URL, options: Options) + + /// Creates a download file destination closure which uses the default file manager to move the temporary file to a + /// file URL in the first available directory with the specified search path directory and search path domain mask. + /// + /// - Parameters: + /// - directory: The search path directory. `.documentDirectory` by default. + /// - domain: The search path domain mask. `.userDomainMask` by default. + /// - options: `DownloadRequest.Options` used when moving the downloaded file to its destination. None by + /// default. + /// - Returns: The `Destination` closure. + public class func suggestedDownloadDestination(for directory: FileManager.SearchPathDirectory = .documentDirectory, + in domain: FileManager.SearchPathDomainMask = .userDomainMask, + options: Options = []) -> Destination { + { temporaryURL, response in + let directoryURLs = FileManager.default.urls(for: directory, in: domain) + let url = directoryURLs.first?.appendingPathComponent(response.suggestedFilename!) ?? temporaryURL + + return (url, options) + } + } + + /// Default `Destination` used by Alamofire to ensure all downloads persist. This `Destination` prepends + /// `Alamofire_` to the automatically generated download name and moves it within the temporary directory. Files + /// with this destination must be additionally moved if they should survive the system reclamation of temporary + /// space. + static let defaultDestination: Destination = { url, _ in + (defaultDestinationURL(url), []) + } + + /// Default `URL` creation closure. Creates a `URL` in the temporary directory with `Alamofire_` prepended to the + /// provided file name. + static let defaultDestinationURL: (URL) -> URL = { url in + let filename = "Alamofire_\(url.lastPathComponent)" + let destination = url.deletingLastPathComponent().appendingPathComponent(filename) + + return destination + } + + // MARK: Downloadable + + /// Type describing the source used to create the underlying `URLSessionDownloadTask`. + public enum Downloadable { + /// Download should be started from the `URLRequest` produced by the associated `URLRequestConvertible` value. + case request(URLRequestConvertible) + /// Download should be started from the associated resume `Data` value. + case resumeData(Data) + } + + // MARK: Mutable State + + /// Type containing all mutable state for `DownloadRequest` instances. + private struct DownloadRequestMutableState { + /// Possible resume `Data` produced when cancelling the instance. + var resumeData: Data? + /// `URL` to which `Data` is being downloaded. + var fileURL: URL? + } + + /// Protected mutable state specific to `DownloadRequest`. + @Protected + private var mutableDownloadState = DownloadRequestMutableState() + + /// If the download is resumable and is eventually cancelled or fails, this value may be used to resume the download + /// using the `download(resumingWith data:)` API. + /// + /// - Note: For more information about `resumeData`, see [Apple's documentation](https://developer.apple.com/documentation/foundation/urlsessiondownloadtask/1411634-cancel). + public var resumeData: Data? { + #if !(os(Linux) || os(Windows)) + return $mutableDownloadState.resumeData ?? error?.downloadResumeData + #else + return $mutableDownloadState.resumeData + #endif + } + + /// If the download is successful, the `URL` where the file was downloaded. + public var fileURL: URL? { $mutableDownloadState.fileURL } + + // MARK: Initial State + + /// `Downloadable` value used for this instance. + public let downloadable: Downloadable + /// The `Destination` to which the downloaded file is moved. + let destination: Destination + + /// Creates a `DownloadRequest` using the provided parameters. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. + /// - downloadable: `Downloadable` value used to create `URLSessionDownloadTasks` for the instance. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request` + /// - destination: `Destination` closure used to move the downloaded file to its final location. + init(id: UUID = UUID(), + downloadable: Downloadable, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + delegate: RequestDelegate, + destination: @escaping Destination) { + self.downloadable = downloadable + self.destination = destination + + super.init(id: id, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: delegate) + } + + override func reset() { + super.reset() + + $mutableDownloadState.write { + $0.resumeData = nil + $0.fileURL = nil + } + } + + /// Called when a download has finished. + /// + /// - Parameters: + /// - task: `URLSessionTask` that finished the download. + /// - result: `Result` of the automatic move to `destination`. + func didFinishDownloading(using task: URLSessionTask, with result: Result) { + eventMonitor?.request(self, didFinishDownloadingUsing: task, with: result) + + switch result { + case let .success(url): $mutableDownloadState.fileURL = url + case let .failure(error): self.error = error + } + } + + /// Updates the `downloadProgress` using the provided values. + /// + /// - Parameters: + /// - bytesWritten: Total bytes written so far. + /// - totalBytesExpectedToWrite: Total bytes expected to write. + func updateDownloadProgress(bytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + downloadProgress.totalUnitCount = totalBytesExpectedToWrite + downloadProgress.completedUnitCount += bytesWritten + + downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) } + } + + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + session.downloadTask(with: request) + } + + /// Creates a `URLSessionTask` from the provided resume data. + /// + /// - Parameters: + /// - data: `Data` used to resume the download. + /// - session: `URLSession` used to create the `URLSessionTask`. + /// + /// - Returns: The `URLSessionTask` created. + public func task(forResumeData data: Data, using session: URLSession) -> URLSessionTask { + session.downloadTask(withResumeData: data) + } + + /// Cancels the instance. Once cancelled, a `DownloadRequest` can no longer be resumed or suspended. + /// + /// - Note: This method will NOT produce resume data. If you wish to cancel and produce resume data, use + /// `cancel(producingResumeData:)` or `cancel(byProducingResumeData:)`. + /// + /// - Returns: The instance. + @discardableResult + override public func cancel() -> Self { + cancel(producingResumeData: false) + } + + /// Cancels the instance, optionally producing resume data. Once cancelled, a `DownloadRequest` can no longer be + /// resumed or suspended. + /// + /// - Note: If `producingResumeData` is `true`, the `resumeData` property will be populated with any resume data, if + /// available. + /// + /// - Returns: The instance. + @discardableResult + public func cancel(producingResumeData shouldProduceResumeData: Bool) -> Self { + cancel(optionallyProducingResumeData: shouldProduceResumeData ? { _ in } : nil) + } + + /// Cancels the instance while producing resume data. Once cancelled, a `DownloadRequest` can no longer be resumed + /// or suspended. + /// + /// - Note: The resume data passed to the completion handler will also be available on the instance's `resumeData` + /// property. + /// + /// - Parameter completionHandler: The completion handler that is called when the download has been successfully + /// cancelled. It is not guaranteed to be called on a particular queue, so you may + /// want use an appropriate queue to perform your work. + /// + /// - Returns: The instance. + @discardableResult + public func cancel(byProducingResumeData completionHandler: @escaping (_ data: Data?) -> Void) -> Self { + cancel(optionallyProducingResumeData: completionHandler) + } + + /// Internal implementation of cancellation that optionally takes a resume data handler. If no handler is passed, + /// cancellation is performed without producing resume data. + /// + /// - Parameter completionHandler: Optional resume data handler. + /// + /// - Returns: The instance. + private func cancel(optionallyProducingResumeData completionHandler: ((_ resumeData: Data?) -> Void)?) -> Self { + $mutableState.write { mutableState in + guard mutableState.state.canTransitionTo(.cancelled) else { return } + + mutableState.state = .cancelled + + underlyingQueue.async { self.didCancel() } + + guard let task = mutableState.tasks.last as? URLSessionDownloadTask, task.state != .completed else { + underlyingQueue.async { self.finish() } + return + } + + if let completionHandler = completionHandler { + // Resume to ensure metrics are gathered. + task.resume() + task.cancel { resumeData in + self.$mutableDownloadState.resumeData = resumeData + self.underlyingQueue.async { self.didCancelTask(task) } + completionHandler(resumeData) + } + } else { + // Resume to ensure metrics are gathered. + task.resume() + task.cancel() + self.underlyingQueue.async { self.didCancelTask(task) } + } + } + + return self + } + + /// Validates the request, using the specified closure. + /// + /// - Note: If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - Parameter validation: `Validation` closure to validate the response. + /// + /// - Returns: The instance. + @discardableResult + public func validate(_ validation: @escaping Validation) -> Self { + let validator: () -> Void = { [unowned self] in + guard self.error == nil, let response = self.response else { return } + + let result = validation(self.request, response, self.fileURL) + + if case let .failure(error) = result { + self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) + } + + self.eventMonitor?.request(self, + didValidateRequest: self.request, + response: response, + fileURL: self.fileURL, + withResult: result) + } + + $validators.write { $0.append(validator) } + + return self + } +} + +// MARK: - UploadRequest + +/// `DataRequest` subclass which handles `Data` upload from memory, file, or stream using `URLSessionUploadTask`. +public class UploadRequest: DataRequest { + /// Type describing the origin of the upload, whether `Data`, file, or stream. + public enum Uploadable { + /// Upload from the provided `Data` value. + case data(Data) + /// Upload from the provided file `URL`, as well as a `Bool` determining whether the source file should be + /// automatically removed once uploaded. + case file(URL, shouldRemove: Bool) + /// Upload from the provided `InputStream`. + case stream(InputStream) + } + + // MARK: Initial State + + /// The `UploadableConvertible` value used to produce the `Uploadable` value for this instance. + public let upload: UploadableConvertible + + /// `FileManager` used to perform cleanup tasks, including the removal of multipart form encoded payloads written + /// to disk. + public let fileManager: FileManager + + // MARK: Mutable State + + /// `Uploadable` value used by the instance. + public var uploadable: Uploadable? + + /// Creates an `UploadRequest` using the provided parameters. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. + /// - convertible: `UploadConvertible` value used to determine the type of upload to be performed. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - fileManager: `FileManager` used to perform cleanup tasks, including the removal of multipart form + /// encoded payloads written to disk. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. + init(id: UUID = UUID(), + convertible: UploadConvertible, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + fileManager: FileManager, + delegate: RequestDelegate) { + upload = convertible + self.fileManager = fileManager + + super.init(id: id, + convertible: convertible, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: delegate) + } + + /// Called when the `Uploadable` value has been created from the `UploadConvertible`. + /// + /// - Parameter uploadable: The `Uploadable` that was created. + func didCreateUploadable(_ uploadable: Uploadable) { + self.uploadable = uploadable + + eventMonitor?.request(self, didCreateUploadable: uploadable) + } + + /// Called when the `Uploadable` value could not be created. + /// + /// - Parameter error: `AFError` produced by the failure. + func didFailToCreateUploadable(with error: AFError) { + self.error = error + + eventMonitor?.request(self, didFailToCreateUploadableWithError: error) + + retryOrFinish(error: error) + } + + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + guard let uploadable = uploadable else { + fatalError("Attempting to create a URLSessionUploadTask when Uploadable value doesn't exist.") + } + + switch uploadable { + case let .data(data): return session.uploadTask(with: request, from: data) + case let .file(url, _): return session.uploadTask(with: request, fromFile: url) + case .stream: return session.uploadTask(withStreamedRequest: request) + } + } + + override func reset() { + // Uploadable must be recreated on every retry. + uploadable = nil + + super.reset() + } + + /// Produces the `InputStream` from `uploadable`, if it can. + /// + /// - Note: Calling this method with a non-`.stream` `Uploadable` is a logic error and will crash. + /// + /// - Returns: The `InputStream`. + func inputStream() -> InputStream { + guard let uploadable = uploadable else { + fatalError("Attempting to access the input stream but the uploadable doesn't exist.") + } + + guard case let .stream(stream) = uploadable else { + fatalError("Attempted to access the stream of an UploadRequest that wasn't created with one.") + } + + eventMonitor?.request(self, didProvideInputStream: stream) + + return stream + } + + override public func cleanup() { + defer { super.cleanup() } + + guard + let uploadable = uploadable, + case let .file(url, shouldRemove) = uploadable, + shouldRemove + else { return } + + try? fileManager.removeItem(at: url) + } +} + +/// A type that can produce an `UploadRequest.Uploadable` value. +public protocol UploadableConvertible { + /// Produces an `UploadRequest.Uploadable` value from the instance. + /// + /// - Returns: The `UploadRequest.Uploadable`. + /// - Throws: Any `Error` produced during creation. + func createUploadable() throws -> UploadRequest.Uploadable +} + +extension UploadRequest.Uploadable: UploadableConvertible { + public func createUploadable() throws -> UploadRequest.Uploadable { + self + } +} + +/// A type that can be converted to an upload, whether from an `UploadRequest.Uploadable` or `URLRequestConvertible`. +public protocol UploadConvertible: UploadableConvertible & URLRequestConvertible {} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/RequestInterceptor.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/RequestInterceptor.swift new file mode 100644 index 0000000..7ed39a5 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/RequestInterceptor.swift @@ -0,0 +1,357 @@ +// +// RequestInterceptor.swift +// +// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Stores all state associated with a `URLRequest` being adapted. +public struct RequestAdapterState { + /// The `UUID` of the `Request` associated with the `URLRequest` to adapt. + public let requestID: UUID + + /// The `Session` associated with the `URLRequest` to adapt. + public let session: Session +} + +// MARK: - + +/// A type that can inspect and optionally adapt a `URLRequest` in some manner if necessary. +public protocol RequestAdapter { + /// Inspects and adapts the specified `URLRequest` in some manner and calls the completion handler with the Result. + /// + /// - Parameters: + /// - urlRequest: The `URLRequest` to adapt. + /// - session: The `Session` that will execute the `URLRequest`. + /// - completion: The completion handler that must be called when adaptation is complete. + func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) + + /// Inspects and adapts the specified `URLRequest` in some manner and calls the completion handler with the Result. + /// + /// - Parameters: + /// - urlRequest: The `URLRequest` to adapt. + /// - state: The `RequestAdapterState` associated with the `URLRequest`. + /// - completion: The completion handler that must be called when adaptation is complete. + func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result) -> Void) +} + +extension RequestAdapter { + public func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result) -> Void) { + adapt(urlRequest, for: state.session, completion: completion) + } +} + +// MARK: - + +/// Outcome of determination whether retry is necessary. +public enum RetryResult { + /// Retry should be attempted immediately. + case retry + /// Retry should be attempted after the associated `TimeInterval`. + case retryWithDelay(TimeInterval) + /// Do not retry. + case doNotRetry + /// Do not retry due to the associated `Error`. + case doNotRetryWithError(Error) +} + +extension RetryResult { + var retryRequired: Bool { + switch self { + case .retry, .retryWithDelay: return true + default: return false + } + } + + var delay: TimeInterval? { + switch self { + case let .retryWithDelay(delay): return delay + default: return nil + } + } + + var error: Error? { + guard case let .doNotRetryWithError(error) = self else { return nil } + return error + } +} + +/// A type that determines whether a request should be retried after being executed by the specified session manager +/// and encountering an error. +public protocol RequestRetrier { + /// Determines whether the `Request` should be retried by calling the `completion` closure. + /// + /// This operation is fully asynchronous. Any amount of time can be taken to determine whether the request needs + /// to be retried. The one requirement is that the completion closure is called to ensure the request is properly + /// cleaned up after. + /// + /// - Parameters: + /// - request: `Request` that failed due to the provided `Error`. + /// - session: `Session` that produced the `Request`. + /// - error: `Error` encountered while executing the `Request`. + /// - completion: Completion closure to be executed when a retry decision has been determined. + func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) +} + +// MARK: - + +/// Type that provides both `RequestAdapter` and `RequestRetrier` functionality. +public protocol RequestInterceptor: RequestAdapter, RequestRetrier {} + +extension RequestInterceptor { + public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + completion(.success(urlRequest)) + } + + public func retry(_ request: Request, + for session: Session, + dueTo error: Error, + completion: @escaping (RetryResult) -> Void) { + completion(.doNotRetry) + } +} + +/// `RequestAdapter` closure definition. +public typealias AdaptHandler = (URLRequest, Session, _ completion: @escaping (Result) -> Void) -> Void +/// `RequestRetrier` closure definition. +public typealias RetryHandler = (Request, Session, Error, _ completion: @escaping (RetryResult) -> Void) -> Void + +// MARK: - + +/// Closure-based `RequestAdapter`. +open class Adapter: RequestInterceptor { + private let adaptHandler: AdaptHandler + + /// Creates an instance using the provided closure. + /// + /// - Parameter adaptHandler: `AdaptHandler` closure to be executed when handling request adaptation. + public init(_ adaptHandler: @escaping AdaptHandler) { + self.adaptHandler = adaptHandler + } + + open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + adaptHandler(urlRequest, session, completion) + } + + open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result) -> Void) { + adaptHandler(urlRequest, state.session, completion) + } +} + +#if swift(>=5.5) +extension RequestAdapter where Self == Adapter { + /// Creates an `Adapter` using the provided `AdaptHandler` closure. + /// + /// - Parameter closure: `AdaptHandler` to use to adapt the request. + /// - Returns: The `Adapter`. + public static func adapter(using closure: @escaping AdaptHandler) -> Adapter { + Adapter(closure) + } +} +#endif + +// MARK: - + +/// Closure-based `RequestRetrier`. +open class Retrier: RequestInterceptor { + private let retryHandler: RetryHandler + + /// Creates an instance using the provided closure. + /// + /// - Parameter retryHandler: `RetryHandler` closure to be executed when handling request retry. + public init(_ retryHandler: @escaping RetryHandler) { + self.retryHandler = retryHandler + } + + open func retry(_ request: Request, + for session: Session, + dueTo error: Error, + completion: @escaping (RetryResult) -> Void) { + retryHandler(request, session, error, completion) + } +} + +#if swift(>=5.5) +extension RequestRetrier where Self == Retrier { + /// Creates a `Retrier` using the provided `RetryHandler` closure. + /// + /// - Parameter closure: `RetryHandler` to use to retry the request. + /// - Returns: The `Retrier`. + public static func retrier(using closure: @escaping RetryHandler) -> Retrier { + Retrier(closure) + } +} +#endif + +// MARK: - + +/// `RequestInterceptor` which can use multiple `RequestAdapter` and `RequestRetrier` values. +open class Interceptor: RequestInterceptor { + /// All `RequestAdapter`s associated with the instance. These adapters will be run until one fails. + public let adapters: [RequestAdapter] + /// All `RequestRetrier`s associated with the instance. These retriers will be run one at a time until one triggers retry. + public let retriers: [RequestRetrier] + + /// Creates an instance from `AdaptHandler` and `RetryHandler` closures. + /// + /// - Parameters: + /// - adaptHandler: `AdaptHandler` closure to be used. + /// - retryHandler: `RetryHandler` closure to be used. + public init(adaptHandler: @escaping AdaptHandler, retryHandler: @escaping RetryHandler) { + adapters = [Adapter(adaptHandler)] + retriers = [Retrier(retryHandler)] + } + + /// Creates an instance from `RequestAdapter` and `RequestRetrier` values. + /// + /// - Parameters: + /// - adapter: `RequestAdapter` value to be used. + /// - retrier: `RequestRetrier` value to be used. + public init(adapter: RequestAdapter, retrier: RequestRetrier) { + adapters = [adapter] + retriers = [retrier] + } + + /// Creates an instance from the arrays of `RequestAdapter` and `RequestRetrier` values. + /// + /// - Parameters: + /// - adapters: `RequestAdapter` values to be used. + /// - retriers: `RequestRetrier` values to be used. + /// - interceptors: `RequestInterceptor`s to be used. + public init(adapters: [RequestAdapter] = [], retriers: [RequestRetrier] = [], interceptors: [RequestInterceptor] = []) { + self.adapters = adapters + interceptors + self.retriers = retriers + interceptors + } + + open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + adapt(urlRequest, for: session, using: adapters, completion: completion) + } + + private func adapt(_ urlRequest: URLRequest, + for session: Session, + using adapters: [RequestAdapter], + completion: @escaping (Result) -> Void) { + var pendingAdapters = adapters + + guard !pendingAdapters.isEmpty else { completion(.success(urlRequest)); return } + + let adapter = pendingAdapters.removeFirst() + + adapter.adapt(urlRequest, for: session) { result in + switch result { + case let .success(urlRequest): + self.adapt(urlRequest, for: session, using: pendingAdapters, completion: completion) + case .failure: + completion(result) + } + } + } + + open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result) -> Void) { + adapt(urlRequest, using: state, adapters: adapters, completion: completion) + } + + private func adapt(_ urlRequest: URLRequest, + using state: RequestAdapterState, + adapters: [RequestAdapter], + completion: @escaping (Result) -> Void) { + var pendingAdapters = adapters + + guard !pendingAdapters.isEmpty else { completion(.success(urlRequest)); return } + + let adapter = pendingAdapters.removeFirst() + + adapter.adapt(urlRequest, using: state) { result in + switch result { + case let .success(urlRequest): + self.adapt(urlRequest, using: state, adapters: pendingAdapters, completion: completion) + case .failure: + completion(result) + } + } + } + + open func retry(_ request: Request, + for session: Session, + dueTo error: Error, + completion: @escaping (RetryResult) -> Void) { + retry(request, for: session, dueTo: error, using: retriers, completion: completion) + } + + private func retry(_ request: Request, + for session: Session, + dueTo error: Error, + using retriers: [RequestRetrier], + completion: @escaping (RetryResult) -> Void) { + var pendingRetriers = retriers + + guard !pendingRetriers.isEmpty else { completion(.doNotRetry); return } + + let retrier = pendingRetriers.removeFirst() + + retrier.retry(request, for: session, dueTo: error) { result in + switch result { + case .retry, .retryWithDelay, .doNotRetryWithError: + completion(result) + case .doNotRetry: + // Only continue to the next retrier if retry was not triggered and no error was encountered + self.retry(request, for: session, dueTo: error, using: pendingRetriers, completion: completion) + } + } + } +} + +#if swift(>=5.5) +extension RequestInterceptor where Self == Interceptor { + /// Creates an `Interceptor` using the provided `AdaptHandler` and `RetryHandler` closures. + /// + /// - Parameters: + /// - adapter: `AdapterHandler`to use to adapt the request. + /// - retrier: `RetryHandler` to use to retry the request. + /// - Returns: The `Interceptor`. + public static func interceptor(adapter: @escaping AdaptHandler, retrier: @escaping RetryHandler) -> Interceptor { + Interceptor(adaptHandler: adapter, retryHandler: retrier) + } + + /// Creates an `Interceptor` using the provided `RequestAdapter` and `RequestRetrier` instances. + /// - Parameters: + /// - adapter: `RequestAdapter` to use to adapt the request + /// - retrier: `RequestRetrier` to use to retry the request. + /// - Returns: The `Interceptor`. + public static func interceptor(adapter: RequestAdapter, retrier: RequestRetrier) -> Interceptor { + Interceptor(adapter: adapter, retrier: retrier) + } + + /// Creates an `Interceptor` using the provided `RequestAdapter`s, `RequestRetrier`s, and `RequestInterceptor`s. + /// - Parameters: + /// - adapters: `RequestAdapter`s to use to adapt the request. These adapters will be run until one fails. + /// - retriers: `RequestRetrier`s to use to retry the request. These retriers will be run one at a time until + /// a retry is triggered. + /// - interceptors: `RequestInterceptor`s to use to intercept the request. + /// - Returns: The `Interceptor`. + public static func interceptor(adapters: [RequestAdapter] = [], + retriers: [RequestRetrier] = [], + interceptors: [RequestInterceptor] = []) -> Interceptor { + Interceptor(adapters: adapters, retriers: retriers, interceptors: interceptors) + } +} +#endif diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/RequestTaskMap.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/RequestTaskMap.swift new file mode 100644 index 0000000..85b58f3 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/RequestTaskMap.swift @@ -0,0 +1,149 @@ +// +// RequestTaskMap.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A type that maintains a two way, one to one map of `URLSessionTask`s to `Request`s. +struct RequestTaskMap { + private typealias Events = (completed: Bool, metricsGathered: Bool) + + private var tasksToRequests: [URLSessionTask: Request] + private var requestsToTasks: [Request: URLSessionTask] + private var taskEvents: [URLSessionTask: Events] + + var requests: [Request] { + Array(tasksToRequests.values) + } + + init(tasksToRequests: [URLSessionTask: Request] = [:], + requestsToTasks: [Request: URLSessionTask] = [:], + taskEvents: [URLSessionTask: (completed: Bool, metricsGathered: Bool)] = [:]) { + self.tasksToRequests = tasksToRequests + self.requestsToTasks = requestsToTasks + self.taskEvents = taskEvents + } + + subscript(_ request: Request) -> URLSessionTask? { + get { requestsToTasks[request] } + set { + guard let newValue = newValue else { + guard let task = requestsToTasks[request] else { + fatalError("RequestTaskMap consistency error: no task corresponding to request found.") + } + + requestsToTasks.removeValue(forKey: request) + tasksToRequests.removeValue(forKey: task) + taskEvents.removeValue(forKey: task) + + return + } + + requestsToTasks[request] = newValue + tasksToRequests[newValue] = request + taskEvents[newValue] = (completed: false, metricsGathered: false) + } + } + + subscript(_ task: URLSessionTask) -> Request? { + get { tasksToRequests[task] } + set { + guard let newValue = newValue else { + guard let request = tasksToRequests[task] else { + fatalError("RequestTaskMap consistency error: no request corresponding to task found.") + } + + tasksToRequests.removeValue(forKey: task) + requestsToTasks.removeValue(forKey: request) + taskEvents.removeValue(forKey: task) + + return + } + + tasksToRequests[task] = newValue + requestsToTasks[newValue] = task + taskEvents[task] = (completed: false, metricsGathered: false) + } + } + + var count: Int { + precondition(tasksToRequests.count == requestsToTasks.count, + "RequestTaskMap.count invalid, requests.count: \(tasksToRequests.count) != tasks.count: \(requestsToTasks.count)") + + return tasksToRequests.count + } + + var eventCount: Int { + precondition(taskEvents.count == count, "RequestTaskMap.eventCount invalid, count: \(count) != taskEvents.count: \(taskEvents.count)") + + return taskEvents.count + } + + var isEmpty: Bool { + precondition(tasksToRequests.isEmpty == requestsToTasks.isEmpty, + "RequestTaskMap.isEmpty invalid, requests.isEmpty: \(tasksToRequests.isEmpty) != tasks.isEmpty: \(requestsToTasks.isEmpty)") + + return tasksToRequests.isEmpty + } + + var isEventsEmpty: Bool { + precondition(taskEvents.isEmpty == isEmpty, "RequestTaskMap.isEventsEmpty invalid, isEmpty: \(isEmpty) != taskEvents.isEmpty: \(taskEvents.isEmpty)") + + return taskEvents.isEmpty + } + + mutating func disassociateIfNecessaryAfterGatheringMetricsForTask(_ task: URLSessionTask) -> Bool { + guard let events = taskEvents[task] else { + fatalError("RequestTaskMap consistency error: no events corresponding to task found.") + } + + switch (events.completed, events.metricsGathered) { + case (_, true): fatalError("RequestTaskMap consistency error: duplicate metricsGatheredForTask call.") + case (false, false): taskEvents[task] = (completed: false, metricsGathered: true); return false + case (true, false): self[task] = nil; return true + } + } + + mutating func disassociateIfNecessaryAfterCompletingTask(_ task: URLSessionTask) -> Bool { + guard let events = taskEvents[task] else { + fatalError("RequestTaskMap consistency error: no events corresponding to task found.") + } + + switch (events.completed, events.metricsGathered) { + case (true, _): fatalError("RequestTaskMap consistency error: duplicate completionReceivedForTask call.") + #if os(Linux) // Linux doesn't gather metrics, so unconditionally remove the reference and return true. + default: self[task] = nil; return true + #else + case (false, false): + if #available(macOS 10.12, iOS 10, watchOS 7, tvOS 10, *) { + taskEvents[task] = (completed: true, metricsGathered: false); return false + } else { + // watchOS < 7 doesn't gather metrics, so unconditionally remove the reference and return true. + self[task] = nil; return true + } + case (false, true): + self[task] = nil; return true + #endif + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/Response.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/Response.swift new file mode 100644 index 0000000..d9ae9d8 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/Response.swift @@ -0,0 +1,453 @@ +// +// Response.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Default type of `DataResponse` returned by Alamofire, with an `AFError` `Failure` type. +public typealias AFDataResponse = DataResponse +/// Default type of `DownloadResponse` returned by Alamofire, with an `AFError` `Failure` type. +public typealias AFDownloadResponse = DownloadResponse + +/// Type used to store all values associated with a serialized response of a `DataRequest` or `UploadRequest`. +public struct DataResponse { + /// The URL request sent to the server. + public let request: URLRequest? + + /// The server's response to the URL request. + public let response: HTTPURLResponse? + + /// The data returned by the server. + public let data: Data? + + /// The final metrics of the response. + /// + /// - Note: Due to `FB7624529`, collection of `URLSessionTaskMetrics` on watchOS is currently disabled.` + /// + public let metrics: URLSessionTaskMetrics? + + /// The time taken to serialize the response. + public let serializationDuration: TimeInterval + + /// The result of response serialization. + public let result: Result + + /// Returns the associated value of the result if it is a success, `nil` otherwise. + public var value: Success? { result.success } + + /// Returns the associated error value if the result if it is a failure, `nil` otherwise. + public var error: Failure? { result.failure } + + /// Creates a `DataResponse` instance with the specified parameters derived from the response serialization. + /// + /// - Parameters: + /// - request: The `URLRequest` sent to the server. + /// - response: The `HTTPURLResponse` from the server. + /// - data: The `Data` returned by the server. + /// - metrics: The `URLSessionTaskMetrics` of the `DataRequest` or `UploadRequest`. + /// - serializationDuration: The duration taken by serialization. + /// - result: The `Result` of response serialization. + public init(request: URLRequest?, + response: HTTPURLResponse?, + data: Data?, + metrics: URLSessionTaskMetrics?, + serializationDuration: TimeInterval, + result: Result) { + self.request = request + self.response = response + self.data = data + self.metrics = metrics + self.serializationDuration = serializationDuration + self.result = result + } +} + +// MARK: - + +extension DataResponse: CustomStringConvertible, CustomDebugStringConvertible { + /// The textual representation used when written to an output stream, which includes whether the result was a + /// success or failure. + public var description: String { + "\(result)" + } + + /// The debug textual representation used when written to an output stream, which includes (if available) a summary + /// of the `URLRequest`, the request's headers and body (if decodable as a `String` below 100KB); the + /// `HTTPURLResponse`'s status code, headers, and body; the duration of the network and serialization actions; and + /// the `Result` of serialization. + public var debugDescription: String { + guard let urlRequest = request else { return "[Request]: None\n[Result]: \(result)" } + + let requestDescription = DebugDescription.description(of: urlRequest) + + let responseDescription = response.map { response in + let responseBodyDescription = DebugDescription.description(for: data, headers: response.headers) + + return """ + \(DebugDescription.description(of: response)) + \(responseBodyDescription.indentingNewlines()) + """ + } ?? "[Response]: None" + + let networkDuration = metrics.map { "\($0.taskInterval.duration)s" } ?? "None" + + return """ + \(requestDescription) + \(responseDescription) + [Network Duration]: \(networkDuration) + [Serialization Duration]: \(serializationDuration)s + [Result]: \(result) + """ + } +} + +// MARK: - + +extension DataResponse { + /// Evaluates the specified closure when the result of this `DataResponse` is a success, passing the unwrapped + /// result value as a parameter. + /// + /// Use the `map` method with a closure that does not throw. For example: + /// + /// let possibleData: DataResponse = ... + /// let possibleInt = possibleData.map { $0.count } + /// + /// - parameter transform: A closure that takes the success value of the instance's result. + /// + /// - returns: A `DataResponse` whose result wraps the value returned by the given closure. If this instance's + /// result is a failure, returns a response wrapping the same failure. + public func map(_ transform: (Success) -> NewSuccess) -> DataResponse { + DataResponse(request: request, + response: response, + data: data, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.map(transform)) + } + + /// Evaluates the given closure when the result of this `DataResponse` is a success, passing the unwrapped result + /// value as a parameter. + /// + /// Use the `tryMap` method with a closure that may throw an error. For example: + /// + /// let possibleData: DataResponse = ... + /// let possibleObject = possibleData.tryMap { + /// try JSONSerialization.jsonObject(with: $0) + /// } + /// + /// - parameter transform: A closure that takes the success value of the instance's result. + /// + /// - returns: A success or failure `DataResponse` depending on the result of the given closure. If this instance's + /// result is a failure, returns the same failure. + public func tryMap(_ transform: (Success) throws -> NewSuccess) -> DataResponse { + DataResponse(request: request, + response: response, + data: data, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.tryMap(transform)) + } + + /// Evaluates the specified closure when the `DataResponse` is a failure, passing the unwrapped error as a parameter. + /// + /// Use the `mapError` function with a closure that does not throw. For example: + /// + /// let possibleData: DataResponse = ... + /// let withMyError = possibleData.mapError { MyError.error($0) } + /// + /// - Parameter transform: A closure that takes the error of the instance. + /// + /// - Returns: A `DataResponse` instance containing the result of the transform. + public func mapError(_ transform: (Failure) -> NewFailure) -> DataResponse { + DataResponse(request: request, + response: response, + data: data, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.mapError(transform)) + } + + /// Evaluates the specified closure when the `DataResponse` is a failure, passing the unwrapped error as a parameter. + /// + /// Use the `tryMapError` function with a closure that may throw an error. For example: + /// + /// let possibleData: DataResponse = ... + /// let possibleObject = possibleData.tryMapError { + /// try someFailableFunction(taking: $0) + /// } + /// + /// - Parameter transform: A throwing closure that takes the error of the instance. + /// + /// - Returns: A `DataResponse` instance containing the result of the transform. + public func tryMapError(_ transform: (Failure) throws -> NewFailure) -> DataResponse { + DataResponse(request: request, + response: response, + data: data, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.tryMapError(transform)) + } +} + +// MARK: - + +/// Used to store all data associated with a serialized response of a download request. +public struct DownloadResponse { + /// The URL request sent to the server. + public let request: URLRequest? + + /// The server's response to the URL request. + public let response: HTTPURLResponse? + + /// The final destination URL of the data returned from the server after it is moved. + public let fileURL: URL? + + /// The resume data generated if the request was cancelled. + public let resumeData: Data? + + /// The final metrics of the response. + /// + /// - Note: Due to `FB7624529`, collection of `URLSessionTaskMetrics` on watchOS is currently disabled.` + /// + public let metrics: URLSessionTaskMetrics? + + /// The time taken to serialize the response. + public let serializationDuration: TimeInterval + + /// The result of response serialization. + public let result: Result + + /// Returns the associated value of the result if it is a success, `nil` otherwise. + public var value: Success? { result.success } + + /// Returns the associated error value if the result if it is a failure, `nil` otherwise. + public var error: Failure? { result.failure } + + /// Creates a `DownloadResponse` instance with the specified parameters derived from response serialization. + /// + /// - Parameters: + /// - request: The `URLRequest` sent to the server. + /// - response: The `HTTPURLResponse` from the server. + /// - fileURL: The final destination URL of the data returned from the server after it is moved. + /// - resumeData: The resume `Data` generated if the request was cancelled. + /// - metrics: The `URLSessionTaskMetrics` of the `DownloadRequest`. + /// - serializationDuration: The duration taken by serialization. + /// - result: The `Result` of response serialization. + public init(request: URLRequest?, + response: HTTPURLResponse?, + fileURL: URL?, + resumeData: Data?, + metrics: URLSessionTaskMetrics?, + serializationDuration: TimeInterval, + result: Result) { + self.request = request + self.response = response + self.fileURL = fileURL + self.resumeData = resumeData + self.metrics = metrics + self.serializationDuration = serializationDuration + self.result = result + } +} + +// MARK: - + +extension DownloadResponse: CustomStringConvertible, CustomDebugStringConvertible { + /// The textual representation used when written to an output stream, which includes whether the result was a + /// success or failure. + public var description: String { + "\(result)" + } + + /// The debug textual representation used when written to an output stream, which includes the URL request, the URL + /// response, the temporary and destination URLs, the resume data, the durations of the network and serialization + /// actions, and the response serialization result. + public var debugDescription: String { + guard let urlRequest = request else { return "[Request]: None\n[Result]: \(result)" } + + let requestDescription = DebugDescription.description(of: urlRequest) + let responseDescription = response.map(DebugDescription.description(of:)) ?? "[Response]: None" + let networkDuration = metrics.map { "\($0.taskInterval.duration)s" } ?? "None" + let resumeDataDescription = resumeData.map { "\($0)" } ?? "None" + + return """ + \(requestDescription) + \(responseDescription) + [File URL]: \(fileURL?.path ?? "None") + [Resume Data]: \(resumeDataDescription) + [Network Duration]: \(networkDuration) + [Serialization Duration]: \(serializationDuration)s + [Result]: \(result) + """ + } +} + +// MARK: - + +extension DownloadResponse { + /// Evaluates the given closure when the result of this `DownloadResponse` is a success, passing the unwrapped + /// result value as a parameter. + /// + /// Use the `map` method with a closure that does not throw. For example: + /// + /// let possibleData: DownloadResponse = ... + /// let possibleInt = possibleData.map { $0.count } + /// + /// - parameter transform: A closure that takes the success value of the instance's result. + /// + /// - returns: A `DownloadResponse` whose result wraps the value returned by the given closure. If this instance's + /// result is a failure, returns a response wrapping the same failure. + public func map(_ transform: (Success) -> NewSuccess) -> DownloadResponse { + DownloadResponse(request: request, + response: response, + fileURL: fileURL, + resumeData: resumeData, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.map(transform)) + } + + /// Evaluates the given closure when the result of this `DownloadResponse` is a success, passing the unwrapped + /// result value as a parameter. + /// + /// Use the `tryMap` method with a closure that may throw an error. For example: + /// + /// let possibleData: DownloadResponse = ... + /// let possibleObject = possibleData.tryMap { + /// try JSONSerialization.jsonObject(with: $0) + /// } + /// + /// - parameter transform: A closure that takes the success value of the instance's result. + /// + /// - returns: A success or failure `DownloadResponse` depending on the result of the given closure. If this + /// instance's result is a failure, returns the same failure. + public func tryMap(_ transform: (Success) throws -> NewSuccess) -> DownloadResponse { + DownloadResponse(request: request, + response: response, + fileURL: fileURL, + resumeData: resumeData, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.tryMap(transform)) + } + + /// Evaluates the specified closure when the `DownloadResponse` is a failure, passing the unwrapped error as a parameter. + /// + /// Use the `mapError` function with a closure that does not throw. For example: + /// + /// let possibleData: DownloadResponse = ... + /// let withMyError = possibleData.mapError { MyError.error($0) } + /// + /// - Parameter transform: A closure that takes the error of the instance. + /// + /// - Returns: A `DownloadResponse` instance containing the result of the transform. + public func mapError(_ transform: (Failure) -> NewFailure) -> DownloadResponse { + DownloadResponse(request: request, + response: response, + fileURL: fileURL, + resumeData: resumeData, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.mapError(transform)) + } + + /// Evaluates the specified closure when the `DownloadResponse` is a failure, passing the unwrapped error as a parameter. + /// + /// Use the `tryMapError` function with a closure that may throw an error. For example: + /// + /// let possibleData: DownloadResponse = ... + /// let possibleObject = possibleData.tryMapError { + /// try someFailableFunction(taking: $0) + /// } + /// + /// - Parameter transform: A throwing closure that takes the error of the instance. + /// + /// - Returns: A `DownloadResponse` instance containing the result of the transform. + public func tryMapError(_ transform: (Failure) throws -> NewFailure) -> DownloadResponse { + DownloadResponse(request: request, + response: response, + fileURL: fileURL, + resumeData: resumeData, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.tryMapError(transform)) + } +} + +private enum DebugDescription { + static func description(of request: URLRequest) -> String { + let requestSummary = "\(request.httpMethod!) \(request)" + let requestHeadersDescription = DebugDescription.description(for: request.headers) + let requestBodyDescription = DebugDescription.description(for: request.httpBody, headers: request.headers) + + return """ + [Request]: \(requestSummary) + \(requestHeadersDescription.indentingNewlines()) + \(requestBodyDescription.indentingNewlines()) + """ + } + + static func description(of response: HTTPURLResponse) -> String { + """ + [Response]: + [Status Code]: \(response.statusCode) + \(DebugDescription.description(for: response.headers).indentingNewlines()) + """ + } + + static func description(for headers: HTTPHeaders) -> String { + guard !headers.isEmpty else { return "[Headers]: None" } + + let headerDescription = "\(headers.sorted())".indentingNewlines() + return """ + [Headers]: + \(headerDescription) + """ + } + + static func description(for data: Data?, + headers: HTTPHeaders, + allowingPrintableTypes printableTypes: [String] = ["json", "xml", "text"], + maximumLength: Int = 100_000) -> String { + guard let data = data, !data.isEmpty else { return "[Body]: None" } + + guard + data.count <= maximumLength, + printableTypes.compactMap({ headers["Content-Type"]?.contains($0) }).contains(true) + else { return "[Body]: \(data.count) bytes" } + + return """ + [Body]: + \(String(decoding: data, as: UTF8.self) + .trimmingCharacters(in: .whitespacesAndNewlines) + .indentingNewlines()) + """ + } +} + +extension String { + fileprivate func indentingNewlines(by spaceCount: Int = 4) -> String { + let spaces = String(repeating: " ", count: spaceCount) + return replacingOccurrences(of: "\n", with: "\n\(spaces)") + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/ResponseSerialization.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/ResponseSerialization.swift new file mode 100644 index 0000000..3097364 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/ResponseSerialization.swift @@ -0,0 +1,1290 @@ +// +// ResponseSerialization.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +// MARK: Protocols + +/// The type to which all data response serializers must conform in order to serialize a response. +public protocol DataResponseSerializerProtocol { + /// The type of serialized object to be created. + associatedtype SerializedObject + + /// Serialize the response `Data` into the provided type.. + /// + /// - Parameters: + /// - request: `URLRequest` which was used to perform the request, if any. + /// - response: `HTTPURLResponse` received from the server, if any. + /// - data: `Data` returned from the server, if any. + /// - error: `Error` produced by Alamofire or the underlying `URLSession` during the request. + /// + /// - Returns: The `SerializedObject`. + /// - Throws: Any `Error` produced during serialization. + func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> SerializedObject +} + +/// The type to which all download response serializers must conform in order to serialize a response. +public protocol DownloadResponseSerializerProtocol { + /// The type of serialized object to be created. + associatedtype SerializedObject + + /// Serialize the downloaded response `Data` from disk into the provided type.. + /// + /// - Parameters: + /// - request: `URLRequest` which was used to perform the request, if any. + /// - response: `HTTPURLResponse` received from the server, if any. + /// - fileURL: File `URL` to which the response data was downloaded. + /// - error: `Error` produced by Alamofire or the underlying `URLSession` during the request. + /// + /// - Returns: The `SerializedObject`. + /// - Throws: Any `Error` produced during serialization. + func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> SerializedObject +} + +/// A serializer that can handle both data and download responses. +public protocol ResponseSerializer: DataResponseSerializerProtocol & DownloadResponseSerializerProtocol { + /// `DataPreprocessor` used to prepare incoming `Data` for serialization. + var dataPreprocessor: DataPreprocessor { get } + /// `HTTPMethod`s for which empty response bodies are considered appropriate. + var emptyRequestMethods: Set { get } + /// HTTP response codes for which empty response bodies are considered appropriate. + var emptyResponseCodes: Set { get } +} + +/// Type used to preprocess `Data` before it handled by a serializer. +public protocol DataPreprocessor { + /// Process `Data` before it's handled by a serializer. + /// - Parameter data: The raw `Data` to process. + func preprocess(_ data: Data) throws -> Data +} + +/// `DataPreprocessor` that returns passed `Data` without any transform. +public struct PassthroughPreprocessor: DataPreprocessor { + public init() {} + + public func preprocess(_ data: Data) throws -> Data { data } +} + +/// `DataPreprocessor` that trims Google's typical `)]}',\n` XSSI JSON header. +public struct GoogleXSSIPreprocessor: DataPreprocessor { + public init() {} + + public func preprocess(_ data: Data) throws -> Data { + (data.prefix(6) == Data(")]}',\n".utf8)) ? data.dropFirst(6) : data + } +} + +#if swift(>=5.5) +extension DataPreprocessor where Self == PassthroughPreprocessor { + /// Provides a `PassthroughPreprocessor` instance. + public static var passthrough: PassthroughPreprocessor { PassthroughPreprocessor() } +} + +extension DataPreprocessor where Self == GoogleXSSIPreprocessor { + /// Provides a `GoogleXSSIPreprocessor` instance. + public static var googleXSSI: GoogleXSSIPreprocessor { GoogleXSSIPreprocessor() } +} +#endif + +extension ResponseSerializer { + /// Default `DataPreprocessor`. `PassthroughPreprocessor` by default. + public static var defaultDataPreprocessor: DataPreprocessor { PassthroughPreprocessor() } + /// Default `HTTPMethod`s for which empty response bodies are considered appropriate. `[.head]` by default. + public static var defaultEmptyRequestMethods: Set { [.head] } + /// HTTP response codes for which empty response bodies are considered appropriate. `[204, 205]` by default. + public static var defaultEmptyResponseCodes: Set { [204, 205] } + + public var dataPreprocessor: DataPreprocessor { Self.defaultDataPreprocessor } + public var emptyRequestMethods: Set { Self.defaultEmptyRequestMethods } + public var emptyResponseCodes: Set { Self.defaultEmptyResponseCodes } + + /// Determines whether the `request` allows empty response bodies, if `request` exists. + /// + /// - Parameter request: `URLRequest` to evaluate. + /// + /// - Returns: `Bool` representing the outcome of the evaluation, or `nil` if `request` was `nil`. + public func requestAllowsEmptyResponseData(_ request: URLRequest?) -> Bool? { + request.flatMap(\.httpMethod) + .flatMap(HTTPMethod.init) + .map { emptyRequestMethods.contains($0) } + } + + /// Determines whether the `response` allows empty response bodies, if `response` exists`. + /// + /// - Parameter response: `HTTPURLResponse` to evaluate. + /// + /// - Returns: `Bool` representing the outcome of the evaluation, or `nil` if `response` was `nil`. + public func responseAllowsEmptyResponseData(_ response: HTTPURLResponse?) -> Bool? { + response.map(\.statusCode) + .map { emptyResponseCodes.contains($0) } + } + + /// Determines whether `request` and `response` allow empty response bodies. + /// + /// - Parameters: + /// - request: `URLRequest` to evaluate. + /// - response: `HTTPURLResponse` to evaluate. + /// + /// - Returns: `true` if `request` or `response` allow empty bodies, `false` otherwise. + public func emptyResponseAllowed(forRequest request: URLRequest?, response: HTTPURLResponse?) -> Bool { + (requestAllowsEmptyResponseData(request) == true) || (responseAllowsEmptyResponseData(response) == true) + } +} + +/// By default, any serializer declared to conform to both types will get file serialization for free, as it just feeds +/// the data read from disk into the data response serializer. +extension DownloadResponseSerializerProtocol where Self: DataResponseSerializerProtocol { + public func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> Self.SerializedObject { + guard error == nil else { throw error! } + + guard let fileURL = fileURL else { + throw AFError.responseSerializationFailed(reason: .inputFileNil) + } + + let data: Data + do { + data = try Data(contentsOf: fileURL) + } catch { + throw AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL)) + } + + do { + return try serialize(request: request, response: response, data: data, error: error) + } catch { + throw error + } + } +} + +// MARK: - Default + +extension DataRequest { + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + appendResponseSerializer { + // Start work that should be on the serialization queue. + let result = AFResult(value: self.data, error: self.error) + // End work that should be on the serialization queue. + + self.underlyingQueue.async { + let response = DataResponse(request: self.request, + response: self.response, + data: self.data, + metrics: self.metrics, + serializationDuration: 0, + result: result) + + self.eventMonitor?.request(self, didParseResponse: response) + + self.responseSerializerDidComplete { queue.async { completionHandler(response) } } + } + } + + return self + } + + private func _response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDataResponse) -> Void) + -> Self { + appendResponseSerializer { + // Start work that should be on the serialization queue. + let start = ProcessInfo.processInfo.systemUptime + let result: AFResult = Result { + try responseSerializer.serialize(request: self.request, + response: self.response, + data: self.data, + error: self.error) + }.mapError { error in + error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error))) + } + + let end = ProcessInfo.processInfo.systemUptime + // End work that should be on the serialization queue. + + self.underlyingQueue.async { + let response = DataResponse(request: self.request, + response: self.response, + data: self.data, + metrics: self.metrics, + serializationDuration: end - start, + result: result) + + self.eventMonitor?.request(self, didParseResponse: response) + + guard let serializerError = result.failure, let delegate = self.delegate else { + self.responseSerializerDidComplete { queue.async { completionHandler(response) } } + return + } + + delegate.retryResult(for: self, dueTo: serializerError) { retryResult in + var didComplete: (() -> Void)? + + defer { + if let didComplete = didComplete { + self.responseSerializerDidComplete { queue.async { didComplete() } } + } + } + + switch retryResult { + case .doNotRetry: + didComplete = { completionHandler(response) } + + case let .doNotRetryWithError(retryError): + let result: AFResult = .failure(retryError.asAFError(orFailWith: "Received retryError was not already AFError")) + + let response = DataResponse(request: self.request, + response: self.response, + data: self.data, + metrics: self.metrics, + serializationDuration: end - start, + result: result) + + didComplete = { completionHandler(response) } + + case .retry, .retryWithDelay: + delegate.retryRequest(self, withDelay: retryResult.delay) + } + } + } + } + + return self + } + + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default + /// - responseSerializer: The response serializer responsible for serializing the request, response, and data. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDataResponse) -> Void) + -> Self { + _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) + } + + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default + /// - responseSerializer: The response serializer responsible for serializing the request, response, and data. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDataResponse) -> Void) + -> Self { + _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) + } +} + +extension DownloadRequest { + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + completionHandler: @escaping (AFDownloadResponse) -> Void) + -> Self { + appendResponseSerializer { + // Start work that should be on the serialization queue. + let result = AFResult(value: self.fileURL, error: self.error) + // End work that should be on the serialization queue. + + self.underlyingQueue.async { + let response = DownloadResponse(request: self.request, + response: self.response, + fileURL: self.fileURL, + resumeData: self.resumeData, + metrics: self.metrics, + serializationDuration: 0, + result: result) + + self.eventMonitor?.request(self, didParseResponse: response) + + self.responseSerializerDidComplete { queue.async { completionHandler(response) } } + } + } + + return self + } + + private func _response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDownloadResponse) -> Void) + -> Self { + appendResponseSerializer { + // Start work that should be on the serialization queue. + let start = ProcessInfo.processInfo.systemUptime + let result: AFResult = Result { + try responseSerializer.serializeDownload(request: self.request, + response: self.response, + fileURL: self.fileURL, + error: self.error) + }.mapError { error in + error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error))) + } + let end = ProcessInfo.processInfo.systemUptime + // End work that should be on the serialization queue. + + self.underlyingQueue.async { + let response = DownloadResponse(request: self.request, + response: self.response, + fileURL: self.fileURL, + resumeData: self.resumeData, + metrics: self.metrics, + serializationDuration: end - start, + result: result) + + self.eventMonitor?.request(self, didParseResponse: response) + + guard let serializerError = result.failure, let delegate = self.delegate else { + self.responseSerializerDidComplete { queue.async { completionHandler(response) } } + return + } + + delegate.retryResult(for: self, dueTo: serializerError) { retryResult in + var didComplete: (() -> Void)? + + defer { + if let didComplete = didComplete { + self.responseSerializerDidComplete { queue.async { didComplete() } } + } + } + + switch retryResult { + case .doNotRetry: + didComplete = { completionHandler(response) } + + case let .doNotRetryWithError(retryError): + let result: AFResult = .failure(retryError.asAFError(orFailWith: "Received retryError was not already AFError")) + + let response = DownloadResponse(request: self.request, + response: self.response, + fileURL: self.fileURL, + resumeData: self.resumeData, + metrics: self.metrics, + serializationDuration: end - start, + result: result) + + didComplete = { completionHandler(response) } + + case .retry, .retryWithDelay: + delegate.retryRequest(self, withDelay: retryResult.delay) + } + } + } + } + + return self + } + + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - responseSerializer: The response serializer responsible for serializing the request, response, and data + /// contained in the destination `URL`. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDownloadResponse) -> Void) + -> Self { + _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) + } + + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - responseSerializer: The response serializer responsible for serializing the request, response, and data + /// contained in the destination `URL`. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDownloadResponse) -> Void) + -> Self { + _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) + } +} + +// MARK: - URL + +/// A `DownloadResponseSerializerProtocol` that performs only `Error` checking and ensures that a downloaded `fileURL` +/// is present. +public struct URLResponseSerializer: DownloadResponseSerializerProtocol { + /// Creates an instance. + public init() {} + + public func serializeDownload(request: URLRequest?, + response: HTTPURLResponse?, + fileURL: URL?, + error: Error?) throws -> URL { + guard error == nil else { throw error! } + + guard let url = fileURL else { + throw AFError.responseSerializationFailed(reason: .inputFileNil) + } + + return url + } +} + +#if swift(>=5.5) +extension DownloadResponseSerializerProtocol where Self == URLResponseSerializer { + /// Provides a `URLResponseSerializer` instance. + public static var url: URLResponseSerializer { URLResponseSerializer() } +} +#endif + +extension DownloadRequest { + /// Adds a handler using a `URLResponseSerializer` to be called once the request is finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is called. `.main` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseURL(queue: DispatchQueue = .main, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, responseSerializer: URLResponseSerializer(), completionHandler: completionHandler) + } +} + +// MARK: - Data + +/// A `ResponseSerializer` that performs minimal response checking and returns any response `Data` as-is. By default, a +/// request returning `nil` or no data is considered an error. However, if the request has an `HTTPMethod` or the +/// response has an HTTP status code valid for empty responses, then an empty `Data` value is returned. +public final class DataResponseSerializer: ResponseSerializer { + public let dataPreprocessor: DataPreprocessor + public let emptyResponseCodes: Set + public let emptyRequestMethods: Set + + /// Creates a `DataResponseSerializer` using the provided parameters. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + public init(dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) { + self.dataPreprocessor = dataPreprocessor + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + } + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Data { + guard error == nil else { throw error! } + + guard var data = data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) + } + + return Data() + } + + data = try dataPreprocessor.preprocess(data) + + return data + } +} + +#if swift(>=5.5) +extension ResponseSerializer where Self == DataResponseSerializer { + /// Provides a default `DataResponseSerializer` instance. + public static var data: DataResponseSerializer { DataResponseSerializer() } + + /// Creates a `DataResponseSerializer` using the provided parameters. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + /// + /// - Returns: The `DataResponseSerializer`. + public static func data(dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) -> DataResponseSerializer { + DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods) + } +} +#endif + +extension DataRequest { + /// Adds a handler using a `DataResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is called. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseData(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } +} + +extension DownloadRequest { + /// Adds a handler using a `DataResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is called. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseData(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } +} + +// MARK: - String + +/// A `ResponseSerializer` that decodes the response data as a `String`. By default, a request returning `nil` or no +/// data is considered an error. However, if the request has an `HTTPMethod` or the response has an HTTP status code +/// valid for empty responses, then an empty `String` is returned. +public final class StringResponseSerializer: ResponseSerializer { + public let dataPreprocessor: DataPreprocessor + /// Optional string encoding used to validate the response. + public let encoding: String.Encoding? + public let emptyResponseCodes: Set + public let emptyRequestMethods: Set + + /// Creates an instance with the provided values. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - encoding: A string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + public init(dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) { + self.dataPreprocessor = dataPreprocessor + self.encoding = encoding + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + } + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> String { + guard error == nil else { throw error! } + + guard var data = data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) + } + + return "" + } + + data = try dataPreprocessor.preprocess(data) + + var convertedEncoding = encoding + + if let encodingName = response?.textEncodingName, convertedEncoding == nil { + convertedEncoding = String.Encoding(ianaCharsetName: encodingName) + } + + let actualEncoding = convertedEncoding ?? .isoLatin1 + + guard let string = String(data: data, encoding: actualEncoding) else { + throw AFError.responseSerializationFailed(reason: .stringSerializationFailed(encoding: actualEncoding)) + } + + return string + } +} + +#if swift(>=5.5) +extension ResponseSerializer where Self == StringResponseSerializer { + /// Provides a default `StringResponseSerializer` instance. + public static var string: StringResponseSerializer { StringResponseSerializer() } + + /// Creates a `StringResponseSerializer` with the provided values. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - encoding: A string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + /// + /// - Returns: The `StringResponseSerializer`. + public static func string(dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) -> StringResponseSerializer { + StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods) + } +} +#endif + +extension DataRequest { + /// Adds a handler using a `StringResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseString(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } +} + +extension DownloadRequest { + /// Adds a handler using a `StringResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseString(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } +} + +// MARK: - JSON + +/// A `ResponseSerializer` that decodes the response data using `JSONSerialization`. By default, a request returning +/// `nil` or no data is considered an error. However, if the request has an `HTTPMethod` or the response has an +/// HTTP status code valid for empty responses, then an `NSNull` value is returned. +@available(*, deprecated, message: "JSONResponseSerializer deprecated and will be removed in Alamofire 6. Use DecodableResponseSerializer instead.") +public final class JSONResponseSerializer: ResponseSerializer { + public let dataPreprocessor: DataPreprocessor + public let emptyResponseCodes: Set + public let emptyRequestMethods: Set + /// `JSONSerialization.ReadingOptions` used when serializing a response. + public let options: JSONSerialization.ReadingOptions + + /// Creates an instance with the provided values. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + /// - options: The options to use. `.allowFragments` by default. + public init(dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = JSONResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = JSONResponseSerializer.defaultEmptyRequestMethods, + options: JSONSerialization.ReadingOptions = .allowFragments) { + self.dataPreprocessor = dataPreprocessor + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + self.options = options + } + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Any { + guard error == nil else { throw error! } + + guard var data = data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) + } + + return NSNull() + } + + data = try dataPreprocessor.preprocess(data) + + do { + return try JSONSerialization.jsonObject(with: data, options: options) + } catch { + throw AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)) + } + } +} + +extension DataRequest { + /// Adds a handler using a `JSONResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - options: `JSONSerialization.ReadingOptions` used when parsing the response. `.allowFragments` + /// by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @available(*, deprecated, message: "responseJSON deprecated and will be removed in Alamofire 6. Use responseDecodable instead.") + @discardableResult + public func responseJSON(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = JSONResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = JSONResponseSerializer.defaultEmptyRequestMethods, + options: JSONSerialization.ReadingOptions = .allowFragments, + completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods, + options: options), + completionHandler: completionHandler) + } +} + +extension DownloadRequest { + /// Adds a handler using a `JSONResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - options: `JSONSerialization.ReadingOptions` used when parsing the response. `.allowFragments` + /// by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @available(*, deprecated, message: "responseJSON deprecated and will be removed in Alamofire 6. Use responseDecodable instead.") + @discardableResult + public func responseJSON(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = JSONResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = JSONResponseSerializer.defaultEmptyRequestMethods, + options: JSONSerialization.ReadingOptions = .allowFragments, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods, + options: options), + completionHandler: completionHandler) + } +} + +// MARK: - Empty + +/// Protocol representing an empty response. Use `T.emptyValue()` to get an instance. +public protocol EmptyResponse { + /// Empty value for the conforming type. + /// + /// - Returns: Value of `Self` to use for empty values. + static func emptyValue() -> Self +} + +/// Type representing an empty value. Use `Empty.value` to get the static instance. +public struct Empty: Codable { + /// Static `Empty` instance used for all `Empty` responses. + public static let value = Empty() +} + +extension Empty: EmptyResponse { + public static func emptyValue() -> Empty { + value + } +} + +// MARK: - DataDecoder Protocol + +/// Any type which can decode `Data` into a `Decodable` type. +public protocol DataDecoder { + /// Decode `Data` into the provided type. + /// + /// - Parameters: + /// - type: The `Type` to be decoded. + /// - data: The `Data` to be decoded. + /// + /// - Returns: The decoded value of type `D`. + /// - Throws: Any error that occurs during decode. + func decode(_ type: D.Type, from data: Data) throws -> D +} + +/// `JSONDecoder` automatically conforms to `DataDecoder`. +extension JSONDecoder: DataDecoder {} +/// `PropertyListDecoder` automatically conforms to `DataDecoder`. +extension PropertyListDecoder: DataDecoder {} + +// MARK: - Decodable + +/// A `ResponseSerializer` that decodes the response data as a generic value using any type that conforms to +/// `DataDecoder`. By default, this is an instance of `JSONDecoder`. Additionally, a request returning `nil` or no data +/// is considered an error. However, if the request has an `HTTPMethod` or the response has an HTTP status code valid +/// for empty responses then an empty value will be returned. If the decoded type conforms to `EmptyResponse`, the +/// type's `emptyValue()` will be returned. If the decoded type is `Empty`, the `.value` instance is returned. If the +/// decoded type *does not* conform to `EmptyResponse` and isn't `Empty`, an error will be produced. +public final class DecodableResponseSerializer: ResponseSerializer { + public let dataPreprocessor: DataPreprocessor + /// The `DataDecoder` instance used to decode responses. + public let decoder: DataDecoder + public let emptyResponseCodes: Set + public let emptyRequestMethods: Set + + /// Creates an instance using the values provided. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - decoder: The `DataDecoder`. `JSONDecoder()` by default. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + public init(dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) { + self.dataPreprocessor = dataPreprocessor + self.decoder = decoder + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + } + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> T { + guard error == nil else { throw error! } + + guard var data = data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) + } + + guard let emptyResponseType = T.self as? EmptyResponse.Type, let emptyValue = emptyResponseType.emptyValue() as? T else { + throw AFError.responseSerializationFailed(reason: .invalidEmptyResponse(type: "\(T.self)")) + } + + return emptyValue + } + + data = try dataPreprocessor.preprocess(data) + + do { + return try decoder.decode(T.self, from: data) + } catch { + throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error)) + } + } +} + +#if swift(>=5.5) +extension ResponseSerializer { + /// Creates a `DecodableResponseSerializer` using the values provided. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - decoder: The `DataDecoder`. `JSONDecoder()` by default. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + /// + /// - Returns: The `DecodableResponseSerializer`. + public static func decodable(of type: T.Type, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DecodableResponseSerializer where Self == DecodableResponseSerializer { + DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods) + } +} +#endif + +extension DataRequest { + /// Adds a handler using a `DecodableResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseDecodable(of type: T.Type = T.self, + queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } +} + +extension DownloadRequest { + /// Adds a handler using a `DecodableResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseDecodable(of type: T.Type = T.self, + queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } +} + +// MARK: - DataStreamRequest + +/// A type which can serialize incoming `Data`. +public protocol DataStreamSerializer { + /// Type produced from the serialized `Data`. + associatedtype SerializedObject + + /// Serializes incoming `Data` into a `SerializedObject` value. + /// + /// - Parameter data: `Data` to be serialized. + /// + /// - Throws: Any error produced during serialization. + func serialize(_ data: Data) throws -> SerializedObject +} + +/// `DataStreamSerializer` which uses the provided `DataPreprocessor` and `DataDecoder` to serialize the incoming `Data`. +public struct DecodableStreamSerializer: DataStreamSerializer { + /// `DataDecoder` used to decode incoming `Data`. + public let decoder: DataDecoder + /// `DataPreprocessor` incoming `Data` is passed through before being passed to the `DataDecoder`. + public let dataPreprocessor: DataPreprocessor + + /// Creates an instance with the provided `DataDecoder` and `DataPreprocessor`. + /// - Parameters: + /// - decoder: ` DataDecoder` used to decode incoming `Data`. `JSONDecoder()` by default. + /// - dataPreprocessor: `DataPreprocessor` used to process incoming `Data` before it's passed through the + /// `decoder`. `PassthroughPreprocessor()` by default. + public init(decoder: DataDecoder = JSONDecoder(), dataPreprocessor: DataPreprocessor = PassthroughPreprocessor()) { + self.decoder = decoder + self.dataPreprocessor = dataPreprocessor + } + + public func serialize(_ data: Data) throws -> T { + let processedData = try dataPreprocessor.preprocess(data) + do { + return try decoder.decode(T.self, from: processedData) + } catch { + throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error)) + } + } +} + +/// `DataStreamSerializer` which performs no serialization on incoming `Data`. +public struct PassthroughStreamSerializer: DataStreamSerializer { + /// Creates an instance. + public init() {} + + public func serialize(_ data: Data) throws -> Data { data } +} + +/// `DataStreamSerializer` which serializes incoming stream `Data` into `UTF8`-decoded `String` values. +public struct StringStreamSerializer: DataStreamSerializer { + /// Creates an instance. + public init() {} + + public func serialize(_ data: Data) throws -> String { + String(decoding: data, as: UTF8.self) + } +} + +#if swift(>=5.5) +extension DataStreamSerializer { + /// Creates a `DecodableStreamSerializer` instance with the provided `DataDecoder` and `DataPreprocessor`. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from stream data. + /// - decoder: ` DataDecoder` used to decode incoming `Data`. `JSONDecoder()` by default. + /// - dataPreprocessor: `DataPreprocessor` used to process incoming `Data` before it's passed through the + /// `decoder`. `PassthroughPreprocessor()` by default. + public static func decodable(of type: T.Type, + decoder: DataDecoder = JSONDecoder(), + dataPreprocessor: DataPreprocessor = PassthroughPreprocessor()) -> Self where Self == DecodableStreamSerializer { + DecodableStreamSerializer(decoder: decoder, dataPreprocessor: dataPreprocessor) + } +} + +extension DataStreamSerializer where Self == PassthroughStreamSerializer { + /// Provides a `PassthroughStreamSerializer` instance. + public static var passthrough: PassthroughStreamSerializer { PassthroughStreamSerializer() } +} + +extension DataStreamSerializer where Self == StringStreamSerializer { + /// Provides a `StringStreamSerializer` instance. + public static var string: StringStreamSerializer { StringStreamSerializer() } +} +#endif + +extension DataStreamRequest { + /// Adds a `StreamHandler` which performs no parsing on incoming `Data`. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. + /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func responseStream(on queue: DispatchQueue = .main, stream: @escaping Handler) -> Self { + let parser = { [unowned self] (data: Data) in + queue.async { + self.capturingError { + try stream(.init(event: .stream(.success(data)), token: .init(self))) + } + + self.updateAndCompleteIfPossible() + } + } + + $streamMutableState.write { $0.streams.append(parser) } + appendStreamCompletion(on: queue, stream: stream) + + return self + } + + /// Adds a `StreamHandler` which uses the provided `DataStreamSerializer` to process incoming `Data`. + /// + /// - Parameters: + /// - serializer: `DataStreamSerializer` used to process incoming `Data`. Its work is done on the `serializationQueue`. + /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. + /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func responseStream(using serializer: Serializer, + on queue: DispatchQueue = .main, + stream: @escaping Handler) -> Self { + let parser = { [unowned self] (data: Data) in + self.serializationQueue.async { + // Start work on serialization queue. + let result = Result { try serializer.serialize(data) } + .mapError { $0.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: $0))) } + // End work on serialization queue. + self.underlyingQueue.async { + self.eventMonitor?.request(self, didParseStream: result) + + if result.isFailure, self.automaticallyCancelOnStreamError { + self.cancel() + } + + queue.async { + self.capturingError { + try stream(.init(event: .stream(result), token: .init(self))) + } + + self.updateAndCompleteIfPossible() + } + } + } + } + + $streamMutableState.write { $0.streams.append(parser) } + appendStreamCompletion(on: queue, stream: stream) + + return self + } + + /// Adds a `StreamHandler` which parses incoming `Data` as a UTF8 `String`. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. + /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func responseStreamString(on queue: DispatchQueue = .main, + stream: @escaping Handler) -> Self { + let parser = { [unowned self] (data: Data) in + self.serializationQueue.async { + // Start work on serialization queue. + let string = String(decoding: data, as: UTF8.self) + // End work on serialization queue. + self.underlyingQueue.async { + self.eventMonitor?.request(self, didParseStream: .success(string)) + + queue.async { + self.capturingError { + try stream(.init(event: .stream(.success(string)), token: .init(self))) + } + + self.updateAndCompleteIfPossible() + } + } + } + } + + $streamMutableState.write { $0.streams.append(parser) } + appendStreamCompletion(on: queue, stream: stream) + + return self + } + + private func updateAndCompleteIfPossible() { + $streamMutableState.write { state in + state.numberOfExecutingStreams -= 1 + + guard state.numberOfExecutingStreams == 0, !state.enqueuedCompletionEvents.isEmpty else { return } + + let completionEvents = state.enqueuedCompletionEvents + self.underlyingQueue.async { completionEvents.forEach { $0() } } + state.enqueuedCompletionEvents.removeAll() + } + } + + /// Adds a `StreamHandler` which parses incoming `Data` using the provided `DataDecoder`. + /// + /// - Parameters: + /// - type: `Decodable` type to parse incoming `Data` into. + /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. + /// - decoder: `DataDecoder` used to decode the incoming `Data`. + /// - preprocessor: `DataPreprocessor` used to process the incoming `Data` before it's passed to the `decoder`. + /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func responseStreamDecodable(of type: T.Type = T.self, + on queue: DispatchQueue = .main, + using decoder: DataDecoder = JSONDecoder(), + preprocessor: DataPreprocessor = PassthroughPreprocessor(), + stream: @escaping Handler) -> Self { + responseStream(using: DecodableStreamSerializer(decoder: decoder, dataPreprocessor: preprocessor), + stream: stream) + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/Result+Alamofire.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/Result+Alamofire.swift new file mode 100644 index 0000000..39ac286 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/Result+Alamofire.swift @@ -0,0 +1,120 @@ +// +// Result+Alamofire.swift +// +// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Default type of `Result` returned by Alamofire, with an `AFError` `Failure` type. +public typealias AFResult = Result + +// MARK: - Internal APIs + +extension Result { + /// Returns whether the instance is `.success`. + var isSuccess: Bool { + guard case .success = self else { return false } + return true + } + + /// Returns whether the instance is `.failure`. + var isFailure: Bool { + !isSuccess + } + + /// Returns the associated value if the result is a success, `nil` otherwise. + var success: Success? { + guard case let .success(value) = self else { return nil } + return value + } + + /// Returns the associated error value if the result is a failure, `nil` otherwise. + var failure: Failure? { + guard case let .failure(error) = self else { return nil } + return error + } + + /// Initializes a `Result` from value or error. Returns `.failure` if the error is non-nil, `.success` otherwise. + /// + /// - Parameters: + /// - value: A value. + /// - error: An `Error`. + init(value: Success, error: Failure?) { + if let error = error { + self = .failure(error) + } else { + self = .success(value) + } + } + + /// Evaluates the specified closure when the `Result` is a success, passing the unwrapped value as a parameter. + /// + /// Use the `tryMap` method with a closure that may throw an error. For example: + /// + /// let possibleData: Result = .success(Data(...)) + /// let possibleObject = possibleData.tryMap { + /// try JSONSerialization.jsonObject(with: $0) + /// } + /// + /// - parameter transform: A closure that takes the success value of the instance. + /// + /// - returns: A `Result` containing the result of the given closure. If this instance is a failure, returns the + /// same failure. + func tryMap(_ transform: (Success) throws -> NewSuccess) -> Result { + switch self { + case let .success(value): + do { + return try .success(transform(value)) + } catch { + return .failure(error) + } + case let .failure(error): + return .failure(error) + } + } + + /// Evaluates the specified closure when the `Result` is a failure, passing the unwrapped error as a parameter. + /// + /// Use the `tryMapError` function with a closure that may throw an error. For example: + /// + /// let possibleData: Result = .success(Data(...)) + /// let possibleObject = possibleData.tryMapError { + /// try someFailableFunction(taking: $0) + /// } + /// + /// - Parameter transform: A throwing closure that takes the error of the instance. + /// + /// - Returns: A `Result` instance containing the result of the transform. If this instance is a success, returns + /// the same success. + func tryMapError(_ transform: (Failure) throws -> NewFailure) -> Result { + switch self { + case let .failure(error): + do { + return try .failure(transform(error)) + } catch { + return .failure(error) + } + case let .success(value): + return .success(value) + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/RetryPolicy.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/RetryPolicy.swift new file mode 100644 index 0000000..f6fd8d3 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/RetryPolicy.swift @@ -0,0 +1,434 @@ +// +// RetryPolicy.swift +// +// Copyright (c) 2019-2020 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A retry policy that retries requests using an exponential backoff for allowed HTTP methods and HTTP status codes +/// as well as certain types of networking errors. +open class RetryPolicy: RequestInterceptor { + /// The default retry limit for retry policies. + public static let defaultRetryLimit: UInt = 2 + + /// The default exponential backoff base for retry policies (must be a minimum of 2). + public static let defaultExponentialBackoffBase: UInt = 2 + + /// The default exponential backoff scale for retry policies. + public static let defaultExponentialBackoffScale: Double = 0.5 + + /// The default HTTP methods to retry. + /// See [RFC 2616 - Section 9.1.2](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html) for more information. + public static let defaultRetryableHTTPMethods: Set = [.delete, // [Delete](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7) - not always idempotent + .get, // [GET](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3) - generally idempotent + .head, // [HEAD](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4) - generally idempotent + .options, // [OPTIONS](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2) - inherently idempotent + .put, // [PUT](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6) - not always idempotent + .trace // [TRACE](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8) - inherently idempotent + ] + + /// The default HTTP status codes to retry. + /// See [RFC 2616 - Section 10](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10) for more information. + public static let defaultRetryableHTTPStatusCodes: Set = [408, // [Request Timeout](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.9) + 500, // [Internal Server Error](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1) + 502, // [Bad Gateway](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.3) + 503, // [Service Unavailable](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4) + 504 // [Gateway Timeout](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.5) + ] + + /// The default URL error codes to retry. + public static let defaultRetryableURLErrorCodes: Set = [// [Security] App Transport Security disallowed a connection because there is no secure network connection. + // - [Disabled] ATS settings do not change at runtime. + // .appTransportSecurityRequiresSecureConnection, + + // [System] An app or app extension attempted to connect to a background session that is already connected to a + // process. + // - [Enabled] The other process could release the background session. + .backgroundSessionInUseByAnotherProcess, + + // [System] The shared container identifier of the URL session configuration is needed but has not been set. + // - [Disabled] Cannot change at runtime. + // .backgroundSessionRequiresSharedContainer, + + // [System] The app is suspended or exits while a background data task is processing. + // - [Enabled] App can be foregrounded or launched to recover. + .backgroundSessionWasDisconnected, + + // [Network] The URL Loading system received bad data from the server. + // - [Enabled] Server could return valid data when retrying. + .badServerResponse, + + // [Resource] A malformed URL prevented a URL request from being initiated. + // - [Disabled] URL was most likely constructed incorrectly. + // .badURL, + + // [System] A connection was attempted while a phone call is active on a network that does not support + // simultaneous phone and data communication (EDGE or GPRS). + // - [Enabled] Phone call could be ended to allow request to recover. + .callIsActive, + + // [Client] An asynchronous load has been canceled. + // - [Disabled] Request was cancelled by the client. + // .cancelled, + + // [File System] A download task couldn’t close the downloaded file on disk. + // - [Disabled] File system error is unlikely to recover with retry. + // .cannotCloseFile, + + // [Network] An attempt to connect to a host failed. + // - [Enabled] Server or DNS lookup could recover during retry. + .cannotConnectToHost, + + // [File System] A download task couldn’t create the downloaded file on disk because of an I/O failure. + // - [Disabled] File system error is unlikely to recover with retry. + // .cannotCreateFile, + + // [Data] Content data received during a connection request had an unknown content encoding. + // - [Disabled] Server is unlikely to modify the content encoding during a retry. + // .cannotDecodeContentData, + + // [Data] Content data received during a connection request could not be decoded for a known content encoding. + // - [Disabled] Server is unlikely to modify the content encoding during a retry. + // .cannotDecodeRawData, + + // [Network] The host name for a URL could not be resolved. + // - [Enabled] Server or DNS lookup could recover during retry. + .cannotFindHost, + + // [Network] A request to load an item only from the cache could not be satisfied. + // - [Enabled] Cache could be populated during a retry. + .cannotLoadFromNetwork, + + // [File System] A download task was unable to move a downloaded file on disk. + // - [Disabled] File system error is unlikely to recover with retry. + // .cannotMoveFile, + + // [File System] A download task was unable to open the downloaded file on disk. + // - [Disabled] File system error is unlikely to recover with retry. + // .cannotOpenFile, + + // [Data] A task could not parse a response. + // - [Disabled] Invalid response is unlikely to recover with retry. + // .cannotParseResponse, + + // [File System] A download task was unable to remove a downloaded file from disk. + // - [Disabled] File system error is unlikely to recover with retry. + // .cannotRemoveFile, + + // [File System] A download task was unable to write to the downloaded file on disk. + // - [Disabled] File system error is unlikely to recover with retry. + // .cannotWriteToFile, + + // [Security] A client certificate was rejected. + // - [Disabled] Client certificate is unlikely to change with retry. + // .clientCertificateRejected, + + // [Security] A client certificate was required to authenticate an SSL connection during a request. + // - [Disabled] Client certificate is unlikely to be provided with retry. + // .clientCertificateRequired, + + // [Data] The length of the resource data exceeds the maximum allowed. + // - [Disabled] Resource will likely still exceed the length maximum on retry. + // .dataLengthExceedsMaximum, + + // [System] The cellular network disallowed a connection. + // - [Enabled] WiFi connection could be established during retry. + .dataNotAllowed, + + // [Network] The host address could not be found via DNS lookup. + // - [Enabled] DNS lookup could succeed during retry. + .dnsLookupFailed, + + // [Data] A download task failed to decode an encoded file during the download. + // - [Enabled] Server could correct the decoding issue with retry. + .downloadDecodingFailedMidStream, + + // [Data] A download task failed to decode an encoded file after downloading. + // - [Enabled] Server could correct the decoding issue with retry. + .downloadDecodingFailedToComplete, + + // [File System] A file does not exist. + // - [Disabled] File system error is unlikely to recover with retry. + // .fileDoesNotExist, + + // [File System] A request for an FTP file resulted in the server responding that the file is not a plain file, + // but a directory. + // - [Disabled] FTP directory is not likely to change to a file during a retry. + // .fileIsDirectory, + + // [Network] A redirect loop has been detected or the threshold for number of allowable redirects has been + // exceeded (currently 16). + // - [Disabled] The redirect loop is unlikely to be resolved within the retry window. + // .httpTooManyRedirects, + + // [System] The attempted connection required activating a data context while roaming, but international roaming + // is disabled. + // - [Enabled] WiFi connection could be established during retry. + .internationalRoamingOff, + + // [Connectivity] A client or server connection was severed in the middle of an in-progress load. + // - [Enabled] A network connection could be established during retry. + .networkConnectionLost, + + // [File System] A resource couldn’t be read because of insufficient permissions. + // - [Disabled] Permissions are unlikely to be granted during retry. + // .noPermissionsToReadFile, + + // [Connectivity] A network resource was requested, but an internet connection has not been established and + // cannot be established automatically. + // - [Enabled] A network connection could be established during retry. + .notConnectedToInternet, + + // [Resource] A redirect was specified by way of server response code, but the server did not accompany this + // code with a redirect URL. + // - [Disabled] The redirect URL is unlikely to be supplied during a retry. + // .redirectToNonExistentLocation, + + // [Client] A body stream is needed but the client did not provide one. + // - [Disabled] The client will be unlikely to supply a body stream during retry. + // .requestBodyStreamExhausted, + + // [Resource] A requested resource couldn’t be retrieved. + // - [Disabled] The resource is unlikely to become available during the retry window. + // .resourceUnavailable, + + // [Security] An attempt to establish a secure connection failed for reasons that can’t be expressed more + // specifically. + // - [Enabled] The secure connection could be established during a retry given the lack of specificity + // provided by the error. + .secureConnectionFailed, + + // [Security] A server certificate had a date which indicates it has expired, or is not yet valid. + // - [Enabled] The server certificate could become valid within the retry window. + .serverCertificateHasBadDate, + + // [Security] A server certificate was not signed by any root server. + // - [Disabled] The server certificate is unlikely to change during the retry window. + // .serverCertificateHasUnknownRoot, + + // [Security] A server certificate is not yet valid. + // - [Enabled] The server certificate could become valid within the retry window. + .serverCertificateNotYetValid, + + // [Security] A server certificate was signed by a root server that isn’t trusted. + // - [Disabled] The server certificate is unlikely to become trusted within the retry window. + // .serverCertificateUntrusted, + + // [Network] An asynchronous operation timed out. + // - [Enabled] The request timed out for an unknown reason and should be retried. + .timedOut + + // [System] The URL Loading System encountered an error that it can’t interpret. + // - [Disabled] The error could not be interpreted and is unlikely to be recovered from during a retry. + // .unknown, + + // [Resource] A properly formed URL couldn’t be handled by the framework. + // - [Disabled] The URL is unlikely to change during a retry. + // .unsupportedURL, + + // [Client] Authentication is required to access a resource. + // - [Disabled] The user authentication is unlikely to be provided by retrying. + // .userAuthenticationRequired, + + // [Client] An asynchronous request for authentication has been canceled by the user. + // - [Disabled] The user cancelled authentication and explicitly took action to not retry. + // .userCancelledAuthentication, + + // [Resource] A server reported that a URL has a non-zero content length, but terminated the network connection + // gracefully without sending any data. + // - [Disabled] The server is unlikely to provide data during the retry window. + // .zeroByteResource, + ] + + /// The total number of times the request is allowed to be retried. + public let retryLimit: UInt + + /// The base of the exponential backoff policy (should always be greater than or equal to 2). + public let exponentialBackoffBase: UInt + + /// The scale of the exponential backoff. + public let exponentialBackoffScale: Double + + /// The HTTP methods that are allowed to be retried. + public let retryableHTTPMethods: Set + + /// The HTTP status codes that are automatically retried by the policy. + public let retryableHTTPStatusCodes: Set + + /// The URL error codes that are automatically retried by the policy. + public let retryableURLErrorCodes: Set + + /// Creates a `RetryPolicy` from the specified parameters. + /// + /// - Parameters: + /// - retryLimit: The total number of times the request is allowed to be retried. `2` by default. + /// - exponentialBackoffBase: The base of the exponential backoff policy. `2` by default. + /// - exponentialBackoffScale: The scale of the exponential backoff. `0.5` by default. + /// - retryableHTTPMethods: The HTTP methods that are allowed to be retried. + /// `RetryPolicy.defaultRetryableHTTPMethods` by default. + /// - retryableHTTPStatusCodes: The HTTP status codes that are automatically retried by the policy. + /// `RetryPolicy.defaultRetryableHTTPStatusCodes` by default. + /// - retryableURLErrorCodes: The URL error codes that are automatically retried by the policy. + /// `RetryPolicy.defaultRetryableURLErrorCodes` by default. + public init(retryLimit: UInt = RetryPolicy.defaultRetryLimit, + exponentialBackoffBase: UInt = RetryPolicy.defaultExponentialBackoffBase, + exponentialBackoffScale: Double = RetryPolicy.defaultExponentialBackoffScale, + retryableHTTPMethods: Set = RetryPolicy.defaultRetryableHTTPMethods, + retryableHTTPStatusCodes: Set = RetryPolicy.defaultRetryableHTTPStatusCodes, + retryableURLErrorCodes: Set = RetryPolicy.defaultRetryableURLErrorCodes) { + precondition(exponentialBackoffBase >= 2, "The `exponentialBackoffBase` must be a minimum of 2.") + + self.retryLimit = retryLimit + self.exponentialBackoffBase = exponentialBackoffBase + self.exponentialBackoffScale = exponentialBackoffScale + self.retryableHTTPMethods = retryableHTTPMethods + self.retryableHTTPStatusCodes = retryableHTTPStatusCodes + self.retryableURLErrorCodes = retryableURLErrorCodes + } + + open func retry(_ request: Request, + for session: Session, + dueTo error: Error, + completion: @escaping (RetryResult) -> Void) { + if request.retryCount < retryLimit, shouldRetry(request: request, dueTo: error) { + completion(.retryWithDelay(pow(Double(exponentialBackoffBase), Double(request.retryCount)) * exponentialBackoffScale)) + } else { + completion(.doNotRetry) + } + } + + /// Determines whether or not to retry the provided `Request`. + /// + /// - Parameters: + /// - request: `Request` that failed due to the provided `Error`. + /// - error: `Error` encountered while executing the `Request`. + /// + /// - Returns: `Bool` determining whether or not to retry the `Request`. + open func shouldRetry(request: Request, dueTo error: Error) -> Bool { + guard let httpMethod = request.request?.method, retryableHTTPMethods.contains(httpMethod) else { return false } + + if let statusCode = request.response?.statusCode, retryableHTTPStatusCodes.contains(statusCode) { + return true + } else { + let errorCode = (error as? URLError)?.code + let afErrorCode = (error.asAFError?.underlyingError as? URLError)?.code + + guard let code = errorCode ?? afErrorCode else { return false } + + return retryableURLErrorCodes.contains(code) + } + } +} + +#if swift(>=5.5) +extension RequestInterceptor where Self == RetryPolicy { + /// Provides a default `RetryPolicy` instance. + public static var retryPolicy: RetryPolicy { RetryPolicy() } + + /// Creates an `RetryPolicy` from the specified parameters. + /// + /// - Parameters: + /// - retryLimit: The total number of times the request is allowed to be retried. `2` by default. + /// - exponentialBackoffBase: The base of the exponential backoff policy. `2` by default. + /// - exponentialBackoffScale: The scale of the exponential backoff. `0.5` by default. + /// - retryableHTTPMethods: The HTTP methods that are allowed to be retried. + /// `RetryPolicy.defaultRetryableHTTPMethods` by default. + /// - retryableHTTPStatusCodes: The HTTP status codes that are automatically retried by the policy. + /// `RetryPolicy.defaultRetryableHTTPStatusCodes` by default. + /// - retryableURLErrorCodes: The URL error codes that are automatically retried by the policy. + /// `RetryPolicy.defaultRetryableURLErrorCodes` by default. + /// + /// - Returns: The `RetryPolicy` + public static func retryPolicy(retryLimit: UInt = RetryPolicy.defaultRetryLimit, + exponentialBackoffBase: UInt = RetryPolicy.defaultExponentialBackoffBase, + exponentialBackoffScale: Double = RetryPolicy.defaultExponentialBackoffScale, + retryableHTTPMethods: Set = RetryPolicy.defaultRetryableHTTPMethods, + retryableHTTPStatusCodes: Set = RetryPolicy.defaultRetryableHTTPStatusCodes, + retryableURLErrorCodes: Set = RetryPolicy.defaultRetryableURLErrorCodes) -> RetryPolicy { + RetryPolicy(retryLimit: retryLimit, + exponentialBackoffBase: exponentialBackoffBase, + exponentialBackoffScale: exponentialBackoffScale, + retryableHTTPMethods: retryableHTTPMethods, + retryableHTTPStatusCodes: retryableHTTPStatusCodes, + retryableURLErrorCodes: retryableURLErrorCodes) + } +} +#endif + +// MARK: - + +/// A retry policy that automatically retries idempotent requests for network connection lost errors. For more +/// information about retrying network connection lost errors, please refer to Apple's +/// [technical document](https://developer.apple.com/library/content/qa/qa1941/_index.html). +open class ConnectionLostRetryPolicy: RetryPolicy { + /// Creates a `ConnectionLostRetryPolicy` instance from the specified parameters. + /// + /// - Parameters: + /// - retryLimit: The total number of times the request is allowed to be retried. + /// `RetryPolicy.defaultRetryLimit` by default. + /// - exponentialBackoffBase: The base of the exponential backoff policy. + /// `RetryPolicy.defaultExponentialBackoffBase` by default. + /// - exponentialBackoffScale: The scale of the exponential backoff. + /// `RetryPolicy.defaultExponentialBackoffScale` by default. + /// - retryableHTTPMethods: The idempotent http methods to retry. + /// `RetryPolicy.defaultRetryableHTTPMethods` by default. + public init(retryLimit: UInt = RetryPolicy.defaultRetryLimit, + exponentialBackoffBase: UInt = RetryPolicy.defaultExponentialBackoffBase, + exponentialBackoffScale: Double = RetryPolicy.defaultExponentialBackoffScale, + retryableHTTPMethods: Set = RetryPolicy.defaultRetryableHTTPMethods) { + super.init(retryLimit: retryLimit, + exponentialBackoffBase: exponentialBackoffBase, + exponentialBackoffScale: exponentialBackoffScale, + retryableHTTPMethods: retryableHTTPMethods, + retryableHTTPStatusCodes: [], + retryableURLErrorCodes: [.networkConnectionLost]) + } +} + +#if swift(>=5.5) +extension RequestInterceptor where Self == ConnectionLostRetryPolicy { + /// Provides a default `ConnectionLostRetryPolicy` instance. + public static var connectionLostRetryPolicy: ConnectionLostRetryPolicy { ConnectionLostRetryPolicy() } + + /// Creates a `ConnectionLostRetryPolicy` instance from the specified parameters. + /// + /// - Parameters: + /// - retryLimit: The total number of times the request is allowed to be retried. + /// `RetryPolicy.defaultRetryLimit` by default. + /// - exponentialBackoffBase: The base of the exponential backoff policy. + /// `RetryPolicy.defaultExponentialBackoffBase` by default. + /// - exponentialBackoffScale: The scale of the exponential backoff. + /// `RetryPolicy.defaultExponentialBackoffScale` by default. + /// - retryableHTTPMethods: The idempotent http methods to retry. + /// + /// - Returns: The `ConnectionLostRetryPolicy`. + public static func connectionLostRetryPolicy(retryLimit: UInt = RetryPolicy.defaultRetryLimit, + exponentialBackoffBase: UInt = RetryPolicy.defaultExponentialBackoffBase, + exponentialBackoffScale: Double = RetryPolicy.defaultExponentialBackoffScale, + retryableHTTPMethods: Set = RetryPolicy.defaultRetryableHTTPMethods) -> ConnectionLostRetryPolicy { + ConnectionLostRetryPolicy(retryLimit: retryLimit, + exponentialBackoffBase: exponentialBackoffBase, + exponentialBackoffScale: exponentialBackoffScale, + retryableHTTPMethods: retryableHTTPMethods) + } +} +#endif diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/ServerTrustEvaluation.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/ServerTrustEvaluation.swift new file mode 100644 index 0000000..06abf19 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/ServerTrustEvaluation.swift @@ -0,0 +1,739 @@ +// +// ServerTrustPolicy.swift +// +// Copyright (c) 2014-2016 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Responsible for managing the mapping of `ServerTrustEvaluating` values to given hosts. +open class ServerTrustManager { + /// Determines whether all hosts for this `ServerTrustManager` must be evaluated. `true` by default. + public let allHostsMustBeEvaluated: Bool + + /// The dictionary of policies mapped to a particular host. + public let evaluators: [String: ServerTrustEvaluating] + + /// Initializes the `ServerTrustManager` instance with the given evaluators. + /// + /// Since different servers and web services can have different leaf certificates, intermediate and even root + /// certificates, it is important to have the flexibility to specify evaluation policies on a per host basis. This + /// allows for scenarios such as using default evaluation for host1, certificate pinning for host2, public key + /// pinning for host3 and disabling evaluation for host4. + /// + /// - Parameters: + /// - allHostsMustBeEvaluated: The value determining whether all hosts for this instance must be evaluated. `true` + /// by default. + /// - evaluators: A dictionary of evaluators mapped to hosts. + public init(allHostsMustBeEvaluated: Bool = true, evaluators: [String: ServerTrustEvaluating]) { + self.allHostsMustBeEvaluated = allHostsMustBeEvaluated + self.evaluators = evaluators + } + + #if !(os(Linux) || os(Windows)) + /// Returns the `ServerTrustEvaluating` value for the given host, if one is set. + /// + /// By default, this method will return the policy that perfectly matches the given host. Subclasses could override + /// this method and implement more complex mapping implementations such as wildcards. + /// + /// - Parameter host: The host to use when searching for a matching policy. + /// + /// - Returns: The `ServerTrustEvaluating` value for the given host if found, `nil` otherwise. + /// - Throws: `AFError.serverTrustEvaluationFailed` if `allHostsMustBeEvaluated` is `true` and no matching + /// evaluators are found. + open func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? { + guard let evaluator = evaluators[host] else { + if allHostsMustBeEvaluated { + throw AFError.serverTrustEvaluationFailed(reason: .noRequiredEvaluator(host: host)) + } + + return nil + } + + return evaluator + } + #endif +} + +/// A protocol describing the API used to evaluate server trusts. +public protocol ServerTrustEvaluating { + #if os(Linux) || os(Windows) + // Implement this once Linux/Windows has API for evaluating server trusts. + #else + /// Evaluates the given `SecTrust` value for the given `host`. + /// + /// - Parameters: + /// - trust: The `SecTrust` value to evaluate. + /// - host: The host for which to evaluate the `SecTrust` value. + /// + /// - Returns: A `Bool` indicating whether the evaluator considers the `SecTrust` value valid for `host`. + func evaluate(_ trust: SecTrust, forHost host: String) throws + #endif +} + +// MARK: - Server Trust Evaluators + +#if !(os(Linux) || os(Windows)) +/// An evaluator which uses the default server trust evaluation while allowing you to control whether to validate the +/// host provided by the challenge. Applications are encouraged to always validate the host in production environments +/// to guarantee the validity of the server's certificate chain. +public final class DefaultTrustEvaluator: ServerTrustEvaluating { + private let validateHost: Bool + + /// Creates a `DefaultTrustEvaluator`. + /// + /// - Parameter validateHost: Determines whether or not the evaluator should validate the host. `true` by default. + public init(validateHost: Bool = true) { + self.validateHost = validateHost + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + if validateHost { + try trust.af.performValidation(forHost: host) + } + + try trust.af.performDefaultValidation(forHost: host) + } +} + +/// An evaluator which Uses the default and revoked server trust evaluations allowing you to control whether to validate +/// the host provided by the challenge as well as specify the revocation flags for testing for revoked certificates. +/// Apple platforms did not start testing for revoked certificates automatically until iOS 10.1, macOS 10.12 and tvOS +/// 10.1 which is demonstrated in our TLS tests. Applications are encouraged to always validate the host in production +/// environments to guarantee the validity of the server's certificate chain. +public final class RevocationTrustEvaluator: ServerTrustEvaluating { + /// Represents the options to be use when evaluating the status of a certificate. + /// Only Revocation Policy Constants are valid, and can be found in [Apple's documentation](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/policies/1563600-revocation_policy_constants). + public struct Options: OptionSet { + /// Perform revocation checking using the CRL (Certification Revocation List) method. + public static let crl = Options(rawValue: kSecRevocationCRLMethod) + /// Consult only locally cached replies; do not use network access. + public static let networkAccessDisabled = Options(rawValue: kSecRevocationNetworkAccessDisabled) + /// Perform revocation checking using OCSP (Online Certificate Status Protocol). + public static let ocsp = Options(rawValue: kSecRevocationOCSPMethod) + /// Prefer CRL revocation checking over OCSP; by default, OCSP is preferred. + public static let preferCRL = Options(rawValue: kSecRevocationPreferCRL) + /// Require a positive response to pass the policy. If the flag is not set, revocation checking is done on a + /// "best attempt" basis, where failure to reach the server is not considered fatal. + public static let requirePositiveResponse = Options(rawValue: kSecRevocationRequirePositiveResponse) + /// Perform either OCSP or CRL checking. The checking is performed according to the method(s) specified in the + /// certificate and the value of `preferCRL`. + public static let any = Options(rawValue: kSecRevocationUseAnyAvailableMethod) + + /// The raw value of the option. + public let rawValue: CFOptionFlags + + /// Creates an `Options` value with the given `CFOptionFlags`. + /// + /// - Parameter rawValue: The `CFOptionFlags` value to initialize with. + public init(rawValue: CFOptionFlags) { + self.rawValue = rawValue + } + } + + private let performDefaultValidation: Bool + private let validateHost: Bool + private let options: Options + + /// Creates a `RevocationTrustEvaluator` using the provided parameters. + /// + /// - Note: Default and host validation will fail when using this evaluator with self-signed certificates. Use + /// `PinnedCertificatesTrustEvaluator` if you need to use self-signed certificates. + /// + /// - Parameters: + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. `true` by default. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition to + /// performing the default evaluation, even if `performDefaultValidation` is `false`. + /// `true` by default. + /// - options: The `Options` to use to check the revocation status of the certificate. `.any` by + /// default. + public init(performDefaultValidation: Bool = true, validateHost: Bool = true, options: Options = .any) { + self.performDefaultValidation = performDefaultValidation + self.validateHost = validateHost + self.options = options + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + if performDefaultValidation { + try trust.af.performDefaultValidation(forHost: host) + } + + if validateHost { + try trust.af.performValidation(forHost: host) + } + + if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) { + try trust.af.evaluate(afterApplying: SecPolicy.af.revocation(options: options)) + } else { + try trust.af.validate(policy: SecPolicy.af.revocation(options: options)) { status, result in + AFError.serverTrustEvaluationFailed(reason: .revocationCheckFailed(output: .init(host, trust, status, result), options: options)) + } + } + } +} + +#if swift(>=5.5) +extension ServerTrustEvaluating where Self == RevocationTrustEvaluator { + /// Provides a default `RevocationTrustEvaluator` instance. + public static var revocationChecking: RevocationTrustEvaluator { RevocationTrustEvaluator() } + + /// Creates a `RevocationTrustEvaluator` using the provided parameters. + /// + /// - Note: Default and host validation will fail when using this evaluator with self-signed certificates. Use + /// `PinnedCertificatesTrustEvaluator` if you need to use self-signed certificates. + /// + /// - Parameters: + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. `true` by default. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition + /// to performing the default evaluation, even if `performDefaultValidation` is + /// `false`. `true` by default. + /// - options: The `Options` to use to check the revocation status of the certificate. `.any` + /// by default. + /// - Returns: The `RevocationTrustEvaluator`. + public static func revocationChecking(performDefaultValidation: Bool = true, + validateHost: Bool = true, + options: RevocationTrustEvaluator.Options = .any) -> RevocationTrustEvaluator { + RevocationTrustEvaluator(performDefaultValidation: performDefaultValidation, + validateHost: validateHost, + options: options) + } +} +#endif + +/// Uses the pinned certificates to validate the server trust. The server trust is considered valid if one of the pinned +/// certificates match one of the server certificates. By validating both the certificate chain and host, certificate +/// pinning provides a very secure form of server trust validation mitigating most, if not all, MITM attacks. +/// Applications are encouraged to always validate the host and require a valid certificate chain in production +/// environments. +public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating { + private let certificates: [SecCertificate] + private let acceptSelfSignedCertificates: Bool + private let performDefaultValidation: Bool + private let validateHost: Bool + + /// Creates a `PinnedCertificatesTrustEvaluator` from the provided parameters. + /// + /// - Parameters: + /// - certificates: The certificates to use to evaluate the trust. All `cer`, `crt`, and `der` + /// certificates in `Bundle.main` by default. + /// - acceptSelfSignedCertificates: Adds the provided certificates as anchors for the trust evaluation, allowing + /// self-signed certificates to pass. `false` by default. THIS SETTING SHOULD BE + /// FALSE IN PRODUCTION! + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. `true` by default. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition + /// to performing the default evaluation, even if `performDefaultValidation` is + /// `false`. `true` by default. + public init(certificates: [SecCertificate] = Bundle.main.af.certificates, + acceptSelfSignedCertificates: Bool = false, + performDefaultValidation: Bool = true, + validateHost: Bool = true) { + self.certificates = certificates + self.acceptSelfSignedCertificates = acceptSelfSignedCertificates + self.performDefaultValidation = performDefaultValidation + self.validateHost = validateHost + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + guard !certificates.isEmpty else { + throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound) + } + + if acceptSelfSignedCertificates { + try trust.af.setAnchorCertificates(certificates) + } + + if performDefaultValidation { + try trust.af.performDefaultValidation(forHost: host) + } + + if validateHost { + try trust.af.performValidation(forHost: host) + } + + let serverCertificatesData = Set(trust.af.certificateData) + let pinnedCertificatesData = Set(certificates.af.data) + let pinnedCertificatesInServerData = !serverCertificatesData.isDisjoint(with: pinnedCertificatesData) + if !pinnedCertificatesInServerData { + throw AFError.serverTrustEvaluationFailed(reason: .certificatePinningFailed(host: host, + trust: trust, + pinnedCertificates: certificates, + serverCertificates: trust.af.certificates)) + } + } +} + +#if swift(>=5.5) +extension ServerTrustEvaluating where Self == PinnedCertificatesTrustEvaluator { + /// Provides a default `PinnedCertificatesTrustEvaluator` instance. + public static var pinnedCertificates: PinnedCertificatesTrustEvaluator { PinnedCertificatesTrustEvaluator() } + + /// Creates a `PinnedCertificatesTrustEvaluator` using the provided parameters. + /// + /// - Parameters: + /// - certificates: The certificates to use to evaluate the trust. All `cer`, `crt`, and `der` + /// certificates in `Bundle.main` by default. + /// - acceptSelfSignedCertificates: Adds the provided certificates as anchors for the trust evaluation, allowing + /// self-signed certificates to pass. `false` by default. THIS SETTING SHOULD BE + /// FALSE IN PRODUCTION! + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. `true` by default. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition + /// to performing the default evaluation, even if `performDefaultValidation` is + /// `false`. `true` by default. + public static func pinnedCertificates(certificates: [SecCertificate] = Bundle.main.af.certificates, + acceptSelfSignedCertificates: Bool = false, + performDefaultValidation: Bool = true, + validateHost: Bool = true) -> PinnedCertificatesTrustEvaluator { + PinnedCertificatesTrustEvaluator(certificates: certificates, + acceptSelfSignedCertificates: acceptSelfSignedCertificates, + performDefaultValidation: performDefaultValidation, + validateHost: validateHost) + } +} +#endif + +/// Uses the pinned public keys to validate the server trust. The server trust is considered valid if one of the pinned +/// public keys match one of the server certificate public keys. By validating both the certificate chain and host, +/// public key pinning provides a very secure form of server trust validation mitigating most, if not all, MITM attacks. +/// Applications are encouraged to always validate the host and require a valid certificate chain in production +/// environments. +public final class PublicKeysTrustEvaluator: ServerTrustEvaluating { + private let keys: [SecKey] + private let performDefaultValidation: Bool + private let validateHost: Bool + + /// Creates a `PublicKeysTrustEvaluator` from the provided parameters. + /// + /// - Note: Default and host validation will fail when using this evaluator with self-signed certificates. Use + /// `PinnedCertificatesTrustEvaluator` if you need to use self-signed certificates. + /// + /// - Parameters: + /// - keys: The `SecKey`s to use to validate public keys. Defaults to the public keys of all + /// certificates included in the main bundle. + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. `true` by default. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition to + /// performing the default evaluation, even if `performDefaultValidation` is `false`. + /// `true` by default. + public init(keys: [SecKey] = Bundle.main.af.publicKeys, + performDefaultValidation: Bool = true, + validateHost: Bool = true) { + self.keys = keys + self.performDefaultValidation = performDefaultValidation + self.validateHost = validateHost + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + guard !keys.isEmpty else { + throw AFError.serverTrustEvaluationFailed(reason: .noPublicKeysFound) + } + + if performDefaultValidation { + try trust.af.performDefaultValidation(forHost: host) + } + + if validateHost { + try trust.af.performValidation(forHost: host) + } + + let pinnedKeysInServerKeys: Bool = { + for serverPublicKey in trust.af.publicKeys { + for pinnedPublicKey in keys { + if serverPublicKey == pinnedPublicKey { + return true + } + } + } + return false + }() + + if !pinnedKeysInServerKeys { + throw AFError.serverTrustEvaluationFailed(reason: .publicKeyPinningFailed(host: host, + trust: trust, + pinnedKeys: keys, + serverKeys: trust.af.publicKeys)) + } + } +} + +#if swift(>=5.5) +extension ServerTrustEvaluating where Self == PublicKeysTrustEvaluator { + /// Provides a default `PublicKeysTrustEvaluator` instance. + public static var publicKeys: PublicKeysTrustEvaluator { PublicKeysTrustEvaluator() } + + /// Creates a `PublicKeysTrustEvaluator` from the provided parameters. + /// + /// - Note: Default and host validation will fail when using this evaluator with self-signed certificates. Use + /// `PinnedCertificatesTrustEvaluator` if you need to use self-signed certificates. + /// + /// - Parameters: + /// - keys: The `SecKey`s to use to validate public keys. Defaults to the public keys of all + /// certificates included in the main bundle. + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. `true` by default. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition to + /// performing the default evaluation, even if `performDefaultValidation` is `false`. + /// `true` by default. + public static func publicKeys(keys: [SecKey] = Bundle.main.af.publicKeys, + performDefaultValidation: Bool = true, + validateHost: Bool = true) -> PublicKeysTrustEvaluator { + PublicKeysTrustEvaluator(keys: keys, performDefaultValidation: performDefaultValidation, validateHost: validateHost) + } +} +#endif + +/// Uses the provided evaluators to validate the server trust. The trust is only considered valid if all of the +/// evaluators consider it valid. +public final class CompositeTrustEvaluator: ServerTrustEvaluating { + private let evaluators: [ServerTrustEvaluating] + + /// Creates a `CompositeTrustEvaluator` from the provided evaluators. + /// + /// - Parameter evaluators: The `ServerTrustEvaluating` values used to evaluate the server trust. + public init(evaluators: [ServerTrustEvaluating]) { + self.evaluators = evaluators + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + try evaluators.evaluate(trust, forHost: host) + } +} + +#if swift(>=5.5) +extension ServerTrustEvaluating where Self == CompositeTrustEvaluator { + /// Creates a `CompositeTrustEvaluator` from the provided evaluators. + /// + /// - Parameter evaluators: The `ServerTrustEvaluating` values used to evaluate the server trust. + public static func composite(evaluators: [ServerTrustEvaluating]) -> CompositeTrustEvaluator { + CompositeTrustEvaluator(evaluators: evaluators) + } +} +#endif + +/// Disables all evaluation which in turn will always consider any server trust as valid. +/// +/// - Note: Instead of disabling server trust evaluation, it's a better idea to configure systems to properly trust test +/// certificates, as outlined in [this Apple tech note](https://developer.apple.com/library/archive/qa/qa1948/_index.html). +/// +/// **THIS EVALUATOR SHOULD NEVER BE USED IN PRODUCTION!** +@available(*, deprecated, renamed: "DisabledTrustEvaluator", message: "DisabledEvaluator has been renamed DisabledTrustEvaluator.") +public typealias DisabledEvaluator = DisabledTrustEvaluator + +/// Disables all evaluation which in turn will always consider any server trust as valid. +/// +/// +/// - Note: Instead of disabling server trust evaluation, it's a better idea to configure systems to properly trust test +/// certificates, as outlined in [this Apple tech note](https://developer.apple.com/library/archive/qa/qa1948/_index.html). +/// +/// **THIS EVALUATOR SHOULD NEVER BE USED IN PRODUCTION!** +public final class DisabledTrustEvaluator: ServerTrustEvaluating { + /// Creates an instance. + public init() {} + + public func evaluate(_ trust: SecTrust, forHost host: String) throws {} +} + +// MARK: - Extensions + +extension Array where Element == ServerTrustEvaluating { + #if os(Linux) || os(Windows) + // Add this same convenience method for Linux/Windows. + #else + /// Evaluates the given `SecTrust` value for the given `host`. + /// + /// - Parameters: + /// - trust: The `SecTrust` value to evaluate. + /// - host: The host for which to evaluate the `SecTrust` value. + /// + /// - Returns: Whether or not the evaluator considers the `SecTrust` value valid for `host`. + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + for evaluator in self { + try evaluator.evaluate(trust, forHost: host) + } + } + #endif +} + +extension Bundle: AlamofireExtended {} +extension AlamofireExtension where ExtendedType: Bundle { + /// Returns all valid `cer`, `crt`, and `der` certificates in the bundle. + public var certificates: [SecCertificate] { + paths(forResourcesOfTypes: [".cer", ".CER", ".crt", ".CRT", ".der", ".DER"]).compactMap { path in + guard + let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData, + let certificate = SecCertificateCreateWithData(nil, certificateData) else { return nil } + + return certificate + } + } + + /// Returns all public keys for the valid certificates in the bundle. + public var publicKeys: [SecKey] { + certificates.af.publicKeys + } + + /// Returns all pathnames for the resources identified by the provided file extensions. + /// + /// - Parameter types: The filename extensions locate. + /// + /// - Returns: All pathnames for the given filename extensions. + public func paths(forResourcesOfTypes types: [String]) -> [String] { + Array(Set(types.flatMap { type.paths(forResourcesOfType: $0, inDirectory: nil) })) + } +} + +extension SecTrust: AlamofireExtended {} +extension AlamofireExtension where ExtendedType == SecTrust { + /// Evaluates `self` after applying the `SecPolicy` value provided. + /// + /// - Parameter policy: The `SecPolicy` to apply to `self` before evaluation. + /// + /// - Throws: Any `Error` from applying the `SecPolicy` or from evaluation. + @available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) + public func evaluate(afterApplying policy: SecPolicy) throws { + try apply(policy: policy).af.evaluate() + } + + /// Attempts to validate `self` using the `SecPolicy` provided and transforming any error produced using the closure passed. + /// + /// - Parameters: + /// - policy: The `SecPolicy` used to evaluate `self`. + /// - errorProducer: The closure used transform the failed `OSStatus` and `SecTrustResultType`. + /// - Throws: Any `Error` from applying the `policy`, or the result of `errorProducer` if validation fails. + @available(iOS, introduced: 10, deprecated: 12, renamed: "evaluate(afterApplying:)") + @available(macOS, introduced: 10.12, deprecated: 10.14, renamed: "evaluate(afterApplying:)") + @available(tvOS, introduced: 10, deprecated: 12, renamed: "evaluate(afterApplying:)") + @available(watchOS, introduced: 3, deprecated: 5, renamed: "evaluate(afterApplying:)") + public func validate(policy: SecPolicy, errorProducer: (_ status: OSStatus, _ result: SecTrustResultType) -> Error) throws { + try apply(policy: policy).af.validate(errorProducer: errorProducer) + } + + /// Applies a `SecPolicy` to `self`, throwing if it fails. + /// + /// - Parameter policy: The `SecPolicy`. + /// + /// - Returns: `self`, with the policy applied. + /// - Throws: An `AFError.serverTrustEvaluationFailed` instance with a `.policyApplicationFailed` reason. + public func apply(policy: SecPolicy) throws -> SecTrust { + let status = SecTrustSetPolicies(type, policy) + + guard status.af.isSuccess else { + throw AFError.serverTrustEvaluationFailed(reason: .policyApplicationFailed(trust: type, + policy: policy, + status: status)) + } + + return type + } + + /// Evaluate `self`, throwing an `Error` if evaluation fails. + /// + /// - Throws: `AFError.serverTrustEvaluationFailed` with reason `.trustValidationFailed` and associated error from + /// the underlying evaluation. + @available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) + public func evaluate() throws { + var error: CFError? + let evaluationSucceeded = SecTrustEvaluateWithError(type, &error) + + if !evaluationSucceeded { + throw AFError.serverTrustEvaluationFailed(reason: .trustEvaluationFailed(error: error)) + } + } + + /// Validate `self`, passing any failure values through `errorProducer`. + /// + /// - Parameter errorProducer: The closure used to transform the failed `OSStatus` and `SecTrustResultType` into an + /// `Error`. + /// - Throws: The `Error` produced by the `errorProducer` closure. + @available(iOS, introduced: 10, deprecated: 12, renamed: "evaluate()") + @available(macOS, introduced: 10.12, deprecated: 10.14, renamed: "evaluate()") + @available(tvOS, introduced: 10, deprecated: 12, renamed: "evaluate()") + @available(watchOS, introduced: 3, deprecated: 5, renamed: "evaluate()") + public func validate(errorProducer: (_ status: OSStatus, _ result: SecTrustResultType) -> Error) throws { + var result = SecTrustResultType.invalid + let status = SecTrustEvaluate(type, &result) + + guard status.af.isSuccess && result.af.isSuccess else { + throw errorProducer(status, result) + } + } + + /// Sets a custom certificate chain on `self`, allowing full validation of a self-signed certificate and its chain. + /// + /// - Parameter certificates: The `SecCertificate`s to add to the chain. + /// - Throws: Any error produced when applying the new certificate chain. + public func setAnchorCertificates(_ certificates: [SecCertificate]) throws { + // Add additional anchor certificates. + let status = SecTrustSetAnchorCertificates(type, certificates as CFArray) + guard status.af.isSuccess else { + throw AFError.serverTrustEvaluationFailed(reason: .settingAnchorCertificatesFailed(status: status, + certificates: certificates)) + } + + // Trust only the set anchor certs. + let onlyStatus = SecTrustSetAnchorCertificatesOnly(type, true) + guard onlyStatus.af.isSuccess else { + throw AFError.serverTrustEvaluationFailed(reason: .settingAnchorCertificatesFailed(status: onlyStatus, + certificates: certificates)) + } + } + + /// The public keys contained in `self`. + public var publicKeys: [SecKey] { + certificates.af.publicKeys + } + + #if swift(>=5.5) // Xcode 13 / 2021 SDKs. + /// The `SecCertificate`s contained in `self`. + public var certificates: [SecCertificate] { + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + return (SecTrustCopyCertificateChain(type) as? [SecCertificate]) ?? [] + } else { + return (0.. SecPolicy { + SecPolicyCreateSSL(true, hostname as CFString) + } + + /// Creates a `SecPolicy` which checks the revocation of certificates. + /// + /// - Parameter options: The `RevocationTrustEvaluator.Options` for evaluation. + /// + /// - Returns: The `SecPolicy`. + /// - Throws: An `AFError.serverTrustEvaluationFailed` error with reason `.revocationPolicyCreationFailed` + /// if the policy cannot be created. + public static func revocation(options: RevocationTrustEvaluator.Options) throws -> SecPolicy { + guard let policy = SecPolicyCreateRevocation(options.rawValue) else { + throw AFError.serverTrustEvaluationFailed(reason: .revocationPolicyCreationFailed) + } + + return policy + } +} + +extension Array: AlamofireExtended {} +extension AlamofireExtension where ExtendedType == [SecCertificate] { + /// All `Data` values for the contained `SecCertificate`s. + public var data: [Data] { + type.map { SecCertificateCopyData($0) as Data } + } + + /// All public `SecKey` values for the contained `SecCertificate`s. + public var publicKeys: [SecKey] { + type.compactMap(\.af.publicKey) + } +} + +extension SecCertificate: AlamofireExtended {} +extension AlamofireExtension where ExtendedType == SecCertificate { + /// The public key for `self`, if it can be extracted. + /// + /// - Note: On 2020 OSes and newer, only RSA and ECDSA keys are supported. + /// + public var publicKey: SecKey? { + let policy = SecPolicyCreateBasicX509() + var trust: SecTrust? + let trustCreationStatus = SecTrustCreateWithCertificates(type, policy, &trust) + + guard let createdTrust = trust, trustCreationStatus == errSecSuccess else { return nil } + + if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) { + return SecTrustCopyKey(createdTrust) + } else { + return SecTrustCopyPublicKey(createdTrust) + } + } +} + +extension OSStatus: AlamofireExtended {} +extension AlamofireExtension where ExtendedType == OSStatus { + /// Returns whether `self` is `errSecSuccess`. + public var isSuccess: Bool { type == errSecSuccess } +} + +extension SecTrustResultType: AlamofireExtended {} +extension AlamofireExtension where ExtendedType == SecTrustResultType { + /// Returns whether `self is `.unspecified` or `.proceed`. + public var isSuccess: Bool { + type == .unspecified || type == .proceed + } +} +#endif diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/Session.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/Session.swift new file mode 100644 index 0000000..4232f85 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/Session.swift @@ -0,0 +1,1264 @@ +// +// Session.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// `Session` creates and manages Alamofire's `Request` types during their lifetimes. It also provides common +/// functionality for all `Request`s, including queuing, interception, trust management, redirect handling, and response +/// cache handling. +open class Session { + /// Shared singleton instance used by all `AF.request` APIs. Cannot be modified. + public static let `default` = Session() + + /// Underlying `URLSession` used to create `URLSessionTasks` for this instance, and for which this instance's + /// `delegate` handles `URLSessionDelegate` callbacks. + /// + /// - Note: This instance should **NOT** be used to interact with the underlying `URLSessionTask`s. Doing so will + /// break internal Alamofire logic that tracks those tasks. + /// + public let session: URLSession + /// Instance's `SessionDelegate`, which handles the `URLSessionDelegate` methods and `Request` interaction. + public let delegate: SessionDelegate + /// Root `DispatchQueue` for all internal callbacks and state update. **MUST** be a serial queue. + public let rootQueue: DispatchQueue + /// Value determining whether this instance automatically calls `resume()` on all created `Request`s. + public let startRequestsImmediately: Bool + /// `DispatchQueue` on which `URLRequest`s are created asynchronously. By default this queue uses `rootQueue` as its + /// `target`, but a separate queue can be used if request creation is determined to be a bottleneck. Always profile + /// and test before introducing an additional queue. + public let requestQueue: DispatchQueue + /// `DispatchQueue` passed to all `Request`s on which they perform their response serialization. By default this + /// queue uses `rootQueue` as its `target` but a separate queue can be used if response serialization is determined + /// to be a bottleneck. Always profile and test before introducing an additional queue. + public let serializationQueue: DispatchQueue + /// `RequestInterceptor` used for all `Request` created by the instance. `RequestInterceptor`s can also be set on a + /// per-`Request` basis, in which case the `Request`'s interceptor takes precedence over this value. + public let interceptor: RequestInterceptor? + /// `ServerTrustManager` instance used to evaluate all trust challenges and provide certificate and key pinning. + public let serverTrustManager: ServerTrustManager? + /// `RedirectHandler` instance used to provide customization for request redirection. + public let redirectHandler: RedirectHandler? + /// `CachedResponseHandler` instance used to provide customization of cached response handling. + public let cachedResponseHandler: CachedResponseHandler? + /// `CompositeEventMonitor` used to compose Alamofire's `defaultEventMonitors` and any passed `EventMonitor`s. + public let eventMonitor: CompositeEventMonitor + /// `EventMonitor`s included in all instances. `[AlamofireNotifications()]` by default. + public let defaultEventMonitors: [EventMonitor] = [AlamofireNotifications()] + + /// Internal map between `Request`s and any `URLSessionTasks` that may be in flight for them. + var requestTaskMap = RequestTaskMap() + /// `Set` of currently active `Request`s. + var activeRequests: Set = [] + /// Completion events awaiting `URLSessionTaskMetrics`. + var waitingCompletions: [URLSessionTask: () -> Void] = [:] + + /// Creates a `Session` from a `URLSession` and other parameters. + /// + /// - Note: When passing a `URLSession`, you must create the `URLSession` with a specific `delegateQueue` value and + /// pass the `delegateQueue`'s `underlyingQueue` as the `rootQueue` parameter of this initializer. + /// + /// - Parameters: + /// - session: Underlying `URLSession` for this instance. + /// - delegate: `SessionDelegate` that handles `session`'s delegate callbacks as well as `Request` + /// interaction. + /// - rootQueue: Root `DispatchQueue` for all internal callbacks and state updates. **MUST** be a + /// serial queue. + /// - startRequestsImmediately: Determines whether this instance will automatically start all `Request`s. `true` + /// by default. If set to `false`, all `Request`s created must have `.resume()` called. + /// on them for them to start. + /// - requestQueue: `DispatchQueue` on which to perform `URLRequest` creation. By default this queue + /// will use the `rootQueue` as its `target`. A separate queue can be used if it's + /// determined request creation is a bottleneck, but that should only be done after + /// careful testing and profiling. `nil` by default. + /// - serializationQueue: `DispatchQueue` on which to perform all response serialization. By default this + /// queue will use the `rootQueue` as its `target`. A separate queue can be used if + /// it's determined response serialization is a bottleneck, but that should only be + /// done after careful testing and profiling. `nil` by default. + /// - interceptor: `RequestInterceptor` to be used for all `Request`s created by this instance. `nil` + /// by default. + /// - serverTrustManager: `ServerTrustManager` to be used for all trust evaluations by this instance. `nil` + /// by default. + /// - redirectHandler: `RedirectHandler` to be used by all `Request`s created by this instance. `nil` by + /// default. + /// - cachedResponseHandler: `CachedResponseHandler` to be used by all `Request`s created by this instance. + /// `nil` by default. + /// - eventMonitors: Additional `EventMonitor`s used by the instance. Alamofire always adds a + /// `AlamofireNotifications` `EventMonitor` to the array passed here. `[]` by default. + public init(session: URLSession, + delegate: SessionDelegate, + rootQueue: DispatchQueue, + startRequestsImmediately: Bool = true, + requestQueue: DispatchQueue? = nil, + serializationQueue: DispatchQueue? = nil, + interceptor: RequestInterceptor? = nil, + serverTrustManager: ServerTrustManager? = nil, + redirectHandler: RedirectHandler? = nil, + cachedResponseHandler: CachedResponseHandler? = nil, + eventMonitors: [EventMonitor] = []) { + precondition(session.configuration.identifier == nil, + "Alamofire does not support background URLSessionConfigurations.") + precondition(session.delegateQueue.underlyingQueue === rootQueue, + "Session(session:) initializer must be passed the DispatchQueue used as the delegateQueue's underlyingQueue as rootQueue.") + + self.session = session + self.delegate = delegate + self.rootQueue = rootQueue + self.startRequestsImmediately = startRequestsImmediately + self.requestQueue = requestQueue ?? DispatchQueue(label: "\(rootQueue.label).requestQueue", target: rootQueue) + self.serializationQueue = serializationQueue ?? DispatchQueue(label: "\(rootQueue.label).serializationQueue", target: rootQueue) + self.interceptor = interceptor + self.serverTrustManager = serverTrustManager + self.redirectHandler = redirectHandler + self.cachedResponseHandler = cachedResponseHandler + eventMonitor = CompositeEventMonitor(monitors: defaultEventMonitors + eventMonitors) + delegate.eventMonitor = eventMonitor + delegate.stateProvider = self + } + + /// Creates a `Session` from a `URLSessionConfiguration`. + /// + /// - Note: This initializer lets Alamofire handle the creation of the underlying `URLSession` and its + /// `delegateQueue`, and is the recommended initializer for most uses. + /// + /// - Parameters: + /// - configuration: `URLSessionConfiguration` to be used to create the underlying `URLSession`. Changes + /// to this value after being passed to this initializer will have no effect. + /// `URLSessionConfiguration.af.default` by default. + /// - delegate: `SessionDelegate` that handles `session`'s delegate callbacks as well as `Request` + /// interaction. `SessionDelegate()` by default. + /// - rootQueue: Root `DispatchQueue` for all internal callbacks and state updates. **MUST** be a + /// serial queue. `DispatchQueue(label: "org.alamofire.session.rootQueue")` by default. + /// - startRequestsImmediately: Determines whether this instance will automatically start all `Request`s. `true` + /// by default. If set to `false`, all `Request`s created must have `.resume()` called. + /// on them for them to start. + /// - requestQueue: `DispatchQueue` on which to perform `URLRequest` creation. By default this queue + /// will use the `rootQueue` as its `target`. A separate queue can be used if it's + /// determined request creation is a bottleneck, but that should only be done after + /// careful testing and profiling. `nil` by default. + /// - serializationQueue: `DispatchQueue` on which to perform all response serialization. By default this + /// queue will use the `rootQueue` as its `target`. A separate queue can be used if + /// it's determined response serialization is a bottleneck, but that should only be + /// done after careful testing and profiling. `nil` by default. + /// - interceptor: `RequestInterceptor` to be used for all `Request`s created by this instance. `nil` + /// by default. + /// - serverTrustManager: `ServerTrustManager` to be used for all trust evaluations by this instance. `nil` + /// by default. + /// - redirectHandler: `RedirectHandler` to be used by all `Request`s created by this instance. `nil` by + /// default. + /// - cachedResponseHandler: `CachedResponseHandler` to be used by all `Request`s created by this instance. + /// `nil` by default. + /// - eventMonitors: Additional `EventMonitor`s used by the instance. Alamofire always adds a + /// `AlamofireNotifications` `EventMonitor` to the array passed here. `[]` by default. + public convenience init(configuration: URLSessionConfiguration = URLSessionConfiguration.af.default, + delegate: SessionDelegate = SessionDelegate(), + rootQueue: DispatchQueue = DispatchQueue(label: "org.alamofire.session.rootQueue"), + startRequestsImmediately: Bool = true, + requestQueue: DispatchQueue? = nil, + serializationQueue: DispatchQueue? = nil, + interceptor: RequestInterceptor? = nil, + serverTrustManager: ServerTrustManager? = nil, + redirectHandler: RedirectHandler? = nil, + cachedResponseHandler: CachedResponseHandler? = nil, + eventMonitors: [EventMonitor] = []) { + precondition(configuration.identifier == nil, "Alamofire does not support background URLSessionConfigurations.") + + // Retarget the incoming rootQueue for safety, unless it's the main queue, which we know is safe. + let serialRootQueue = (rootQueue === DispatchQueue.main) ? rootQueue : DispatchQueue(label: rootQueue.label, + target: rootQueue) + let delegateQueue = OperationQueue(maxConcurrentOperationCount: 1, underlyingQueue: serialRootQueue, name: "\(serialRootQueue.label).sessionDelegate") + let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue) + + self.init(session: session, + delegate: delegate, + rootQueue: serialRootQueue, + startRequestsImmediately: startRequestsImmediately, + requestQueue: requestQueue, + serializationQueue: serializationQueue, + interceptor: interceptor, + serverTrustManager: serverTrustManager, + redirectHandler: redirectHandler, + cachedResponseHandler: cachedResponseHandler, + eventMonitors: eventMonitors) + } + + deinit { + finishRequestsForDeinit() + session.invalidateAndCancel() + } + + // MARK: - All Requests API + + /// Perform an action on all active `Request`s. + /// + /// - Note: The provided `action` closure is performed asynchronously, meaning that some `Request`s may complete and + /// be unavailable by time it runs. Additionally, this action is performed on the instances's `rootQueue`, + /// so care should be taken that actions are fast. Once the work on the `Request`s is complete, any + /// additional work should be performed on another queue. + /// + /// - Parameters: + /// - action: Closure to perform with all `Request`s. + public func withAllRequests(perform action: @escaping (Set) -> Void) { + rootQueue.async { + action(self.activeRequests) + } + } + + /// Cancel all active `Request`s, optionally calling a completion handler when complete. + /// + /// - Note: This is an asynchronous operation and does not block the creation of future `Request`s. Cancelled + /// `Request`s may not cancel immediately due internal work, and may not cancel at all if they are close to + /// completion when cancelled. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the completion handler is run. `.main` by default. + /// - completion: Closure to be called when all `Request`s have been cancelled. + public func cancelAllRequests(completingOnQueue queue: DispatchQueue = .main, completion: (() -> Void)? = nil) { + withAllRequests { requests in + requests.forEach { $0.cancel() } + queue.async { + completion?() + } + } + } + + // MARK: - DataRequest + + /// Closure which provides a `URLRequest` for mutation. + public typealias RequestModifier = (inout URLRequest) throws -> Void + + struct RequestConvertible: URLRequestConvertible { + let url: URLConvertible + let method: HTTPMethod + let parameters: Parameters? + let encoding: ParameterEncoding + let headers: HTTPHeaders? + let requestModifier: RequestModifier? + + func asURLRequest() throws -> URLRequest { + var request = try URLRequest(url: url, method: method, headers: headers) + try requestModifier?(&request) + + return try encoding.encode(request, with: parameters) + } + } + + /// Creates a `DataRequest` from a `URLRequest` created using the passed components and a `RequestInterceptor`. + /// + /// - Parameters: + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.get` by default. + /// - parameters: `Parameters` (a.k.a. `[String: Any]`) value to be encoded into the `URLRequest`. `nil` by + /// default. + /// - encoding: `ParameterEncoding` to be used to encode the `parameters` value into the `URLRequest`. + /// `URLEncoding.default` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided + /// parameters. `nil` by default. + /// + /// - Returns: The created `DataRequest`. + open func request(_ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoding: ParameterEncoding = URLEncoding.default, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil) -> DataRequest { + let convertible = RequestConvertible(url: convertible, + method: method, + parameters: parameters, + encoding: encoding, + headers: headers, + requestModifier: requestModifier) + + return request(convertible, interceptor: interceptor) + } + + struct RequestEncodableConvertible: URLRequestConvertible { + let url: URLConvertible + let method: HTTPMethod + let parameters: Parameters? + let encoder: ParameterEncoder + let headers: HTTPHeaders? + let requestModifier: RequestModifier? + + func asURLRequest() throws -> URLRequest { + var request = try URLRequest(url: url, method: method, headers: headers) + try requestModifier?(&request) + + return try parameters.map { try encoder.encode($0, into: request) } ?? request + } + } + + /// Creates a `DataRequest` from a `URLRequest` created using the passed components, `Encodable` parameters, and a + /// `RequestInterceptor`. + /// + /// - Parameters: + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.get` by default. + /// - parameters: `Encodable` value to be encoded into the `URLRequest`. `nil` by default. + /// - encoder: `ParameterEncoder` to be used to encode the `parameters` value into the `URLRequest`. + /// `URLEncodedFormParameterEncoder.default` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from + /// the provided parameters. `nil` by default. + /// + /// - Returns: The created `DataRequest`. + open func request(_ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil) -> DataRequest { + let convertible = RequestEncodableConvertible(url: convertible, + method: method, + parameters: parameters, + encoder: encoder, + headers: headers, + requestModifier: requestModifier) + + return request(convertible, interceptor: interceptor) + } + + /// Creates a `DataRequest` from a `URLRequestConvertible` value and a `RequestInterceptor`. + /// + /// - Parameters: + /// - convertible: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// + /// - Returns: The created `DataRequest`. + open func request(_ convertible: URLRequestConvertible, interceptor: RequestInterceptor? = nil) -> DataRequest { + let request = DataRequest(convertible: convertible, + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: self) + + perform(request) + + return request + } + + // MARK: - DataStreamRequest + + /// Creates a `DataStreamRequest` from the passed components, `Encodable` parameters, and `RequestInterceptor`. + /// + /// - Parameters: + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.get` by default. + /// - parameters: `Encodable` value to be encoded into the `URLRequest`. `nil` by default. + /// - encoder: `ParameterEncoder` to be used to encode the `parameters` value into the + /// `URLRequest`. + /// `URLEncodedFormParameterEncoder.default` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - automaticallyCancelOnStreamError: `Bool` indicating whether the instance should be canceled when an `Error` + /// is thrown while serializing stream `Data`. `false` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` + /// by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from + /// the provided parameters. `nil` by default. + /// + /// - Returns: The created `DataStream` request. + open func streamRequest(_ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default, + headers: HTTPHeaders? = nil, + automaticallyCancelOnStreamError: Bool = false, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil) -> DataStreamRequest { + let convertible = RequestEncodableConvertible(url: convertible, + method: method, + parameters: parameters, + encoder: encoder, + headers: headers, + requestModifier: requestModifier) + + return streamRequest(convertible, + automaticallyCancelOnStreamError: automaticallyCancelOnStreamError, + interceptor: interceptor) + } + + /// Creates a `DataStreamRequest` from the passed components and `RequestInterceptor`. + /// + /// - Parameters: + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.get` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - automaticallyCancelOnStreamError: `Bool` indicating whether the instance should be canceled when an `Error` + /// is thrown while serializing stream `Data`. `false` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` + /// by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from + /// the provided parameters. `nil` by default. + /// + /// - Returns: The created `DataStream` request. + open func streamRequest(_ convertible: URLConvertible, + method: HTTPMethod = .get, + headers: HTTPHeaders? = nil, + automaticallyCancelOnStreamError: Bool = false, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil) -> DataStreamRequest { + let convertible = RequestEncodableConvertible(url: convertible, + method: method, + parameters: Empty?.none, + encoder: URLEncodedFormParameterEncoder.default, + headers: headers, + requestModifier: requestModifier) + + return streamRequest(convertible, + automaticallyCancelOnStreamError: automaticallyCancelOnStreamError, + interceptor: interceptor) + } + + /// Creates a `DataStreamRequest` from the passed `URLRequestConvertible` value and `RequestInterceptor`. + /// + /// - Parameters: + /// - convertible: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - automaticallyCancelOnStreamError: `Bool` indicating whether the instance should be canceled when an `Error` + /// is thrown while serializing stream `Data`. `false` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` + /// by default. + /// + /// - Returns: The created `DataStreamRequest`. + open func streamRequest(_ convertible: URLRequestConvertible, + automaticallyCancelOnStreamError: Bool = false, + interceptor: RequestInterceptor? = nil) -> DataStreamRequest { + let request = DataStreamRequest(convertible: convertible, + automaticallyCancelOnStreamError: automaticallyCancelOnStreamError, + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: self) + + perform(request) + + return request + } + + // MARK: - DownloadRequest + + /// Creates a `DownloadRequest` using a `URLRequest` created using the passed components, `RequestInterceptor`, and + /// `Destination`. + /// + /// - Parameters: + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.get` by default. + /// - parameters: `Parameters` (a.k.a. `[String: Any]`) value to be encoded into the `URLRequest`. `nil` by + /// default. + /// - encoding: `ParameterEncoding` to be used to encode the `parameters` value into the `URLRequest`. + /// Defaults to `URLEncoding.default`. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided + /// parameters. `nil` by default. + /// - destination: `DownloadRequest.Destination` closure used to determine how and where the downloaded file + /// should be moved. `nil` by default. + /// + /// - Returns: The created `DownloadRequest`. + open func download(_ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoding: ParameterEncoding = URLEncoding.default, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + let convertible = RequestConvertible(url: convertible, + method: method, + parameters: parameters, + encoding: encoding, + headers: headers, + requestModifier: requestModifier) + + return download(convertible, interceptor: interceptor, to: destination) + } + + /// Creates a `DownloadRequest` from a `URLRequest` created using the passed components, `Encodable` parameters, and + /// a `RequestInterceptor`. + /// + /// - Parameters: + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.get` by default. + /// - parameters: Value conforming to `Encodable` to be encoded into the `URLRequest`. `nil` by default. + /// - encoder: `ParameterEncoder` to be used to encode the `parameters` value into the `URLRequest`. + /// Defaults to `URLEncodedFormParameterEncoder.default`. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided + /// parameters. `nil` by default. + /// - destination: `DownloadRequest.Destination` closure used to determine how and where the downloaded file + /// should be moved. `nil` by default. + /// + /// - Returns: The created `DownloadRequest`. + open func download(_ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + let convertible = RequestEncodableConvertible(url: convertible, + method: method, + parameters: parameters, + encoder: encoder, + headers: headers, + requestModifier: requestModifier) + + return download(convertible, interceptor: interceptor, to: destination) + } + + /// Creates a `DownloadRequest` from a `URLRequestConvertible` value, a `RequestInterceptor`, and a `Destination`. + /// + /// - Parameters: + /// - convertible: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - destination: `DownloadRequest.Destination` closure used to determine how and where the downloaded file + /// should be moved. `nil` by default. + /// + /// - Returns: The created `DownloadRequest`. + open func download(_ convertible: URLRequestConvertible, + interceptor: RequestInterceptor? = nil, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + let request = DownloadRequest(downloadable: .request(convertible), + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: self, + destination: destination ?? DownloadRequest.defaultDestination) + + perform(request) + + return request + } + + /// Creates a `DownloadRequest` from the `resumeData` produced from a previously cancelled `DownloadRequest`, as + /// well as a `RequestInterceptor`, and a `Destination`. + /// + /// - Note: If `destination` is not specified, the download will be moved to a temporary location determined by + /// Alamofire. The file will not be deleted until the system purges the temporary files. + /// + /// - Note: On some versions of all Apple platforms (iOS 10 - 10.2, macOS 10.12 - 10.12.2, tvOS 10 - 10.1, watchOS 3 - 3.1.1), + /// `resumeData` is broken on background URL session configurations. There's an underlying bug in the `resumeData` + /// generation logic where the data is written incorrectly and will always fail to resume the download. For more + /// information about the bug and possible workarounds, please refer to the [this Stack Overflow post](http://stackoverflow.com/a/39347461/1342462). + /// + /// - Parameters: + /// - data: The resume data from a previously cancelled `DownloadRequest` or `URLSessionDownloadTask`. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - destination: `DownloadRequest.Destination` closure used to determine how and where the downloaded file + /// should be moved. `nil` by default. + /// + /// - Returns: The created `DownloadRequest`. + open func download(resumingWith data: Data, + interceptor: RequestInterceptor? = nil, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + let request = DownloadRequest(downloadable: .resumeData(data), + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: self, + destination: destination ?? DownloadRequest.defaultDestination) + + perform(request) + + return request + } + + // MARK: - UploadRequest + + struct ParameterlessRequestConvertible: URLRequestConvertible { + let url: URLConvertible + let method: HTTPMethod + let headers: HTTPHeaders? + let requestModifier: RequestModifier? + + func asURLRequest() throws -> URLRequest { + var request = try URLRequest(url: url, method: method, headers: headers) + try requestModifier?(&request) + + return request + } + } + + struct Upload: UploadConvertible { + let request: URLRequestConvertible + let uploadable: UploadableConvertible + + func createUploadable() throws -> UploadRequest.Uploadable { + try uploadable.createUploadable() + } + + func asURLRequest() throws -> URLRequest { + try request.asURLRequest() + } + } + + // MARK: Data + + /// Creates an `UploadRequest` for the given `Data`, `URLRequest` components, and `RequestInterceptor`. + /// + /// - Parameters: + /// - data: The `Data` to upload. + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.post` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by + /// default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided + /// parameters. `nil` by default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(_ data: Data, + to convertible: URLConvertible, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default, + requestModifier: RequestModifier? = nil) -> UploadRequest { + let convertible = ParameterlessRequestConvertible(url: convertible, + method: method, + headers: headers, + requestModifier: requestModifier) + + return upload(data, with: convertible, interceptor: interceptor, fileManager: fileManager) + } + + /// Creates an `UploadRequest` for the given `Data` using the `URLRequestConvertible` value and `RequestInterceptor`. + /// + /// - Parameters: + /// - data: The `Data` to upload. + /// - convertible: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by + /// default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(_ data: Data, + with convertible: URLRequestConvertible, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default) -> UploadRequest { + upload(.data(data), with: convertible, interceptor: interceptor, fileManager: fileManager) + } + + // MARK: File + + /// Creates an `UploadRequest` for the file at the given file `URL`, using a `URLRequest` from the provided + /// components and `RequestInterceptor`. + /// + /// - Parameters: + /// - fileURL: The `URL` of the file to upload. + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.post` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `UploadRequest`. `nil` by default. + /// - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by + /// default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided + /// parameters. `nil` by default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(_ fileURL: URL, + to convertible: URLConvertible, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default, + requestModifier: RequestModifier? = nil) -> UploadRequest { + let convertible = ParameterlessRequestConvertible(url: convertible, + method: method, + headers: headers, + requestModifier: requestModifier) + + return upload(fileURL, with: convertible, interceptor: interceptor, fileManager: fileManager) + } + + /// Creates an `UploadRequest` for the file at the given file `URL` using the `URLRequestConvertible` value and + /// `RequestInterceptor`. + /// + /// - Parameters: + /// - fileURL: The `URL` of the file to upload. + /// - convertible: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by + /// default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(_ fileURL: URL, + with convertible: URLRequestConvertible, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default) -> UploadRequest { + upload(.file(fileURL, shouldRemove: false), with: convertible, interceptor: interceptor, fileManager: fileManager) + } + + // MARK: InputStream + + /// Creates an `UploadRequest` from the `InputStream` provided using a `URLRequest` from the provided components and + /// `RequestInterceptor`. + /// + /// - Parameters: + /// - stream: The `InputStream` that provides the data to upload. + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.post` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by + /// default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided + /// parameters. `nil` by default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(_ stream: InputStream, + to convertible: URLConvertible, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default, + requestModifier: RequestModifier? = nil) -> UploadRequest { + let convertible = ParameterlessRequestConvertible(url: convertible, + method: method, + headers: headers, + requestModifier: requestModifier) + + return upload(stream, with: convertible, interceptor: interceptor, fileManager: fileManager) + } + + /// Creates an `UploadRequest` from the provided `InputStream` using the `URLRequestConvertible` value and + /// `RequestInterceptor`. + /// + /// - Parameters: + /// - stream: The `InputStream` that provides the data to upload. + /// - convertible: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by + /// default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(_ stream: InputStream, + with convertible: URLRequestConvertible, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default) -> UploadRequest { + upload(.stream(stream), with: convertible, interceptor: interceptor, fileManager: fileManager) + } + + // MARK: MultipartFormData + + /// Creates an `UploadRequest` for the multipart form data built using a closure and sent using the provided + /// `URLRequest` components and `RequestInterceptor`. + /// + /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cumulative + /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most + /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to + /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory + /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be + /// used for larger payloads such as video content. + /// + /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory + /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, + /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk + /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding + /// technique was used. + /// + /// - Parameters: + /// - multipartFormData: `MultipartFormData` building closure. + /// - url: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - encodingMemoryThreshold: Byte threshold used to determine whether the form data is encoded into memory or + /// onto disk before being uploaded. `MultipartFormData.encodingMemoryThreshold` by + /// default. + /// - method: `HTTPMethod` for the `URLRequest`. `.post` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` to be used if the form data exceeds the memory threshold and is + /// written to disk before being uploaded. `.default` instance by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the + /// provided parameters. `nil` by default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(multipartFormData: @escaping (MultipartFormData) -> Void, + to url: URLConvertible, + usingThreshold encodingMemoryThreshold: UInt64 = MultipartFormData.encodingMemoryThreshold, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default, + requestModifier: RequestModifier? = nil) -> UploadRequest { + let convertible = ParameterlessRequestConvertible(url: url, + method: method, + headers: headers, + requestModifier: requestModifier) + + let formData = MultipartFormData(fileManager: fileManager) + multipartFormData(formData) + + return upload(multipartFormData: formData, + with: convertible, + usingThreshold: encodingMemoryThreshold, + interceptor: interceptor, + fileManager: fileManager) + } + + /// Creates an `UploadRequest` using a `MultipartFormData` building closure, the provided `URLRequestConvertible` + /// value, and a `RequestInterceptor`. + /// + /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cumulative + /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most + /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to + /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory + /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be + /// used for larger payloads such as video content. + /// + /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory + /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, + /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk + /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding + /// technique was used. + /// + /// - Parameters: + /// - multipartFormData: `MultipartFormData` building closure. + /// - request: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - encodingMemoryThreshold: Byte threshold used to determine whether the form data is encoded into memory or + /// onto disk before being uploaded. `MultipartFormData.encodingMemoryThreshold` by + /// default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` to be used if the form data exceeds the memory threshold and is + /// written to disk before being uploaded. `.default` instance by default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(multipartFormData: @escaping (MultipartFormData) -> Void, + with request: URLRequestConvertible, + usingThreshold encodingMemoryThreshold: UInt64 = MultipartFormData.encodingMemoryThreshold, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default) -> UploadRequest { + let formData = MultipartFormData(fileManager: fileManager) + multipartFormData(formData) + + return upload(multipartFormData: formData, + with: request, + usingThreshold: encodingMemoryThreshold, + interceptor: interceptor, + fileManager: fileManager) + } + + /// Creates an `UploadRequest` for the prebuilt `MultipartFormData` value using the provided `URLRequest` components + /// and `RequestInterceptor`. + /// + /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cumulative + /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most + /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to + /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory + /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be + /// used for larger payloads such as video content. + /// + /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory + /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, + /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk + /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding + /// technique was used. + /// + /// - Parameters: + /// - multipartFormData: `MultipartFormData` instance to upload. + /// - url: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - encodingMemoryThreshold: Byte threshold used to determine whether the form data is encoded into memory or + /// onto disk before being uploaded. `MultipartFormData.encodingMemoryThreshold` by + /// default. + /// - method: `HTTPMethod` for the `URLRequest`. `.post` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` to be used if the form data exceeds the memory threshold and is + /// written to disk before being uploaded. `.default` instance by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the + /// provided parameters. `nil` by default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(multipartFormData: MultipartFormData, + to url: URLConvertible, + usingThreshold encodingMemoryThreshold: UInt64 = MultipartFormData.encodingMemoryThreshold, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default, + requestModifier: RequestModifier? = nil) -> UploadRequest { + let convertible = ParameterlessRequestConvertible(url: url, + method: method, + headers: headers, + requestModifier: requestModifier) + + let multipartUpload = MultipartUpload(encodingMemoryThreshold: encodingMemoryThreshold, + request: convertible, + multipartFormData: multipartFormData) + + return upload(multipartUpload, interceptor: interceptor, fileManager: fileManager) + } + + /// Creates an `UploadRequest` for the prebuilt `MultipartFormData` value using the providing `URLRequestConvertible` + /// value and `RequestInterceptor`. + /// + /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cumulative + /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most + /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to + /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory + /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be + /// used for larger payloads such as video content. + /// + /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory + /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, + /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk + /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding + /// technique was used. + /// + /// - Parameters: + /// - multipartFormData: `MultipartFormData` instance to upload. + /// - request: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - encodingMemoryThreshold: Byte threshold used to determine whether the form data is encoded into memory or + /// onto disk before being uploaded. `MultipartFormData.encodingMemoryThreshold` by + /// default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by + /// default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(multipartFormData: MultipartFormData, + with request: URLRequestConvertible, + usingThreshold encodingMemoryThreshold: UInt64 = MultipartFormData.encodingMemoryThreshold, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default) -> UploadRequest { + let multipartUpload = MultipartUpload(encodingMemoryThreshold: encodingMemoryThreshold, + request: request, + multipartFormData: multipartFormData) + + return upload(multipartUpload, interceptor: interceptor, fileManager: fileManager) + } + + // MARK: - Internal API + + // MARK: Uploadable + + func upload(_ uploadable: UploadRequest.Uploadable, + with convertible: URLRequestConvertible, + interceptor: RequestInterceptor?, + fileManager: FileManager) -> UploadRequest { + let uploadable = Upload(request: convertible, uploadable: uploadable) + + return upload(uploadable, interceptor: interceptor, fileManager: fileManager) + } + + func upload(_ upload: UploadConvertible, interceptor: RequestInterceptor?, fileManager: FileManager) -> UploadRequest { + let request = UploadRequest(convertible: upload, + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + fileManager: fileManager, + delegate: self) + + perform(request) + + return request + } + + // MARK: Perform + + /// Starts performing the provided `Request`. + /// + /// - Parameter request: The `Request` to perform. + func perform(_ request: Request) { + rootQueue.async { + guard !request.isCancelled else { return } + + self.activeRequests.insert(request) + + self.requestQueue.async { + // Leaf types must come first, otherwise they will cast as their superclass. + switch request { + case let r as UploadRequest: self.performUploadRequest(r) // UploadRequest must come before DataRequest due to subtype relationship. + case let r as DataRequest: self.performDataRequest(r) + case let r as DownloadRequest: self.performDownloadRequest(r) + case let r as DataStreamRequest: self.performDataStreamRequest(r) + default: fatalError("Attempted to perform unsupported Request subclass: \(type(of: request))") + } + } + } + } + + func performDataRequest(_ request: DataRequest) { + dispatchPrecondition(condition: .onQueue(requestQueue)) + + performSetupOperations(for: request, convertible: request.convertible) + } + + func performDataStreamRequest(_ request: DataStreamRequest) { + dispatchPrecondition(condition: .onQueue(requestQueue)) + + performSetupOperations(for: request, convertible: request.convertible) + } + + func performUploadRequest(_ request: UploadRequest) { + dispatchPrecondition(condition: .onQueue(requestQueue)) + + performSetupOperations(for: request, convertible: request.convertible) { + do { + let uploadable = try request.upload.createUploadable() + self.rootQueue.async { request.didCreateUploadable(uploadable) } + return true + } catch { + self.rootQueue.async { request.didFailToCreateUploadable(with: error.asAFError(or: .createUploadableFailed(error: error))) } + return false + } + } + } + + func performDownloadRequest(_ request: DownloadRequest) { + dispatchPrecondition(condition: .onQueue(requestQueue)) + + switch request.downloadable { + case let .request(convertible): + performSetupOperations(for: request, convertible: convertible) + case let .resumeData(resumeData): + rootQueue.async { self.didReceiveResumeData(resumeData, for: request) } + } + } + + func performSetupOperations(for request: Request, + convertible: URLRequestConvertible, + shouldCreateTask: @escaping () -> Bool = { true }) { + dispatchPrecondition(condition: .onQueue(requestQueue)) + + let initialRequest: URLRequest + + do { + initialRequest = try convertible.asURLRequest() + try initialRequest.validate() + } catch { + rootQueue.async { request.didFailToCreateURLRequest(with: error.asAFError(or: .createURLRequestFailed(error: error))) } + return + } + + rootQueue.async { request.didCreateInitialURLRequest(initialRequest) } + + guard !request.isCancelled else { return } + + guard let adapter = adapter(for: request) else { + guard shouldCreateTask() else { return } + rootQueue.async { self.didCreateURLRequest(initialRequest, for: request) } + return + } + + let adapterState = RequestAdapterState(requestID: request.id, session: self) + + adapter.adapt(initialRequest, using: adapterState) { result in + do { + let adaptedRequest = try result.get() + try adaptedRequest.validate() + + self.rootQueue.async { request.didAdaptInitialRequest(initialRequest, to: adaptedRequest) } + + guard shouldCreateTask() else { return } + + self.rootQueue.async { self.didCreateURLRequest(adaptedRequest, for: request) } + } catch { + self.rootQueue.async { request.didFailToAdaptURLRequest(initialRequest, withError: .requestAdaptationFailed(error: error)) } + } + } + } + + // MARK: - Task Handling + + func didCreateURLRequest(_ urlRequest: URLRequest, for request: Request) { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + request.didCreateURLRequest(urlRequest) + + guard !request.isCancelled else { return } + + let task = request.task(for: urlRequest, using: session) + requestTaskMap[request] = task + request.didCreateTask(task) + + updateStatesForTask(task, request: request) + } + + func didReceiveResumeData(_ data: Data, for request: DownloadRequest) { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + guard !request.isCancelled else { return } + + let task = request.task(forResumeData: data, using: session) + requestTaskMap[request] = task + request.didCreateTask(task) + + updateStatesForTask(task, request: request) + } + + func updateStatesForTask(_ task: URLSessionTask, request: Request) { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + request.withState { state in + switch state { + case .initialized, .finished: + // Do nothing. + break + case .resumed: + task.resume() + rootQueue.async { request.didResumeTask(task) } + case .suspended: + task.suspend() + rootQueue.async { request.didSuspendTask(task) } + case .cancelled: + // Resume to ensure metrics are gathered. + task.resume() + task.cancel() + rootQueue.async { request.didCancelTask(task) } + } + } + } + + // MARK: - Adapters and Retriers + + func adapter(for request: Request) -> RequestAdapter? { + if let requestInterceptor = request.interceptor, let sessionInterceptor = interceptor { + return Interceptor(adapters: [requestInterceptor, sessionInterceptor]) + } else { + return request.interceptor ?? interceptor + } + } + + func retrier(for request: Request) -> RequestRetrier? { + if let requestInterceptor = request.interceptor, let sessionInterceptor = interceptor { + return Interceptor(retriers: [requestInterceptor, sessionInterceptor]) + } else { + return request.interceptor ?? interceptor + } + } + + // MARK: - Invalidation + + func finishRequestsForDeinit() { + requestTaskMap.requests.forEach { request in + rootQueue.async { + request.finish(error: AFError.sessionDeinitialized) + } + } + } +} + +// MARK: - RequestDelegate + +extension Session: RequestDelegate { + public var sessionConfiguration: URLSessionConfiguration { + session.configuration + } + + public var startImmediately: Bool { startRequestsImmediately } + + public func cleanup(after request: Request) { + activeRequests.remove(request) + } + + public func retryResult(for request: Request, dueTo error: AFError, completion: @escaping (RetryResult) -> Void) { + guard let retrier = retrier(for: request) else { + rootQueue.async { completion(.doNotRetry) } + return + } + + retrier.retry(request, for: self, dueTo: error) { retryResult in + self.rootQueue.async { + guard let retryResultError = retryResult.error else { completion(retryResult); return } + + let retryError = AFError.requestRetryFailed(retryError: retryResultError, originalError: error) + completion(.doNotRetryWithError(retryError)) + } + } + } + + public func retryRequest(_ request: Request, withDelay timeDelay: TimeInterval?) { + rootQueue.async { + let retry: () -> Void = { + guard !request.isCancelled else { return } + + request.prepareForRetry() + self.perform(request) + } + + if let retryDelay = timeDelay { + self.rootQueue.after(retryDelay) { retry() } + } else { + retry() + } + } + } +} + +// MARK: - SessionStateProvider + +extension Session: SessionStateProvider { + func request(for task: URLSessionTask) -> Request? { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + return requestTaskMap[task] + } + + func didGatherMetricsForTask(_ task: URLSessionTask) { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + let didDisassociate = requestTaskMap.disassociateIfNecessaryAfterGatheringMetricsForTask(task) + + if didDisassociate { + waitingCompletions[task]?() + waitingCompletions[task] = nil + } + } + + func didCompleteTask(_ task: URLSessionTask, completion: @escaping () -> Void) { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + let didDisassociate = requestTaskMap.disassociateIfNecessaryAfterCompletingTask(task) + + if didDisassociate { + completion() + } else { + waitingCompletions[task] = completion + } + } + + func credential(for task: URLSessionTask, in protectionSpace: URLProtectionSpace) -> URLCredential? { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + return requestTaskMap[task]?.credential ?? + session.configuration.urlCredentialStorage?.defaultCredential(for: protectionSpace) + } + + func cancelRequestsForSessionInvalidation(with error: Error?) { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + requestTaskMap.requests.forEach { $0.finish(error: AFError.sessionInvalidated(error: error)) } + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/SessionDelegate.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/SessionDelegate.swift new file mode 100644 index 0000000..a794d83 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/SessionDelegate.swift @@ -0,0 +1,336 @@ +// +// SessionDelegate.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Class which implements the various `URLSessionDelegate` methods to connect various Alamofire features. +open class SessionDelegate: NSObject { + private let fileManager: FileManager + + weak var stateProvider: SessionStateProvider? + var eventMonitor: EventMonitor? + + /// Creates an instance from the given `FileManager`. + /// + /// - Parameter fileManager: `FileManager` to use for underlying file management, such as moving downloaded files. + /// `.default` by default. + public init(fileManager: FileManager = .default) { + self.fileManager = fileManager + } + + /// Internal method to find and cast requests while maintaining some integrity checking. + /// + /// - Parameters: + /// - task: The `URLSessionTask` for which to find the associated `Request`. + /// - type: The `Request` subclass type to cast any `Request` associate with `task`. + func request(for task: URLSessionTask, as type: R.Type) -> R? { + guard let provider = stateProvider else { + assertionFailure("StateProvider is nil.") + return nil + } + + return provider.request(for: task) as? R + } +} + +/// Type which provides various `Session` state values. +protocol SessionStateProvider: AnyObject { + var serverTrustManager: ServerTrustManager? { get } + var redirectHandler: RedirectHandler? { get } + var cachedResponseHandler: CachedResponseHandler? { get } + + func request(for task: URLSessionTask) -> Request? + func didGatherMetricsForTask(_ task: URLSessionTask) + func didCompleteTask(_ task: URLSessionTask, completion: @escaping () -> Void) + func credential(for task: URLSessionTask, in protectionSpace: URLProtectionSpace) -> URLCredential? + func cancelRequestsForSessionInvalidation(with error: Error?) +} + +// MARK: URLSessionDelegate + +extension SessionDelegate: URLSessionDelegate { + open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + eventMonitor?.urlSession(session, didBecomeInvalidWithError: error) + + stateProvider?.cancelRequestsForSessionInvalidation(with: error) + } +} + +// MARK: URLSessionTaskDelegate + +extension SessionDelegate: URLSessionTaskDelegate { + /// Result of a `URLAuthenticationChallenge` evaluation. + typealias ChallengeEvaluation = (disposition: URLSession.AuthChallengeDisposition, credential: URLCredential?, error: AFError?) + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + eventMonitor?.urlSession(session, task: task, didReceive: challenge) + + let evaluation: ChallengeEvaluation + switch challenge.protectionSpace.authenticationMethod { + case NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest, NSURLAuthenticationMethodNTLM, + NSURLAuthenticationMethodNegotiate: + evaluation = attemptCredentialAuthentication(for: challenge, belongingTo: task) + #if !(os(Linux) || os(Windows)) + case NSURLAuthenticationMethodServerTrust: + evaluation = attemptServerTrustAuthentication(with: challenge) + case NSURLAuthenticationMethodClientCertificate: + evaluation = attemptCredentialAuthentication(for: challenge, belongingTo: task) + #endif + default: + evaluation = (.performDefaultHandling, nil, nil) + } + + if let error = evaluation.error { + stateProvider?.request(for: task)?.didFailTask(task, earlyWithError: error) + } + + completionHandler(evaluation.disposition, evaluation.credential) + } + + #if !(os(Linux) || os(Windows)) + /// Evaluates the server trust `URLAuthenticationChallenge` received. + /// + /// - Parameter challenge: The `URLAuthenticationChallenge`. + /// + /// - Returns: The `ChallengeEvaluation`. + func attemptServerTrustAuthentication(with challenge: URLAuthenticationChallenge) -> ChallengeEvaluation { + let host = challenge.protectionSpace.host + + guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + let trust = challenge.protectionSpace.serverTrust + else { + return (.performDefaultHandling, nil, nil) + } + + do { + guard let evaluator = try stateProvider?.serverTrustManager?.serverTrustEvaluator(forHost: host) else { + return (.performDefaultHandling, nil, nil) + } + + try evaluator.evaluate(trust, forHost: host) + + return (.useCredential, URLCredential(trust: trust), nil) + } catch { + return (.cancelAuthenticationChallenge, nil, error.asAFError(or: .serverTrustEvaluationFailed(reason: .customEvaluationFailed(error: error)))) + } + } + #endif + + /// Evaluates the credential-based authentication `URLAuthenticationChallenge` received for `task`. + /// + /// - Parameters: + /// - challenge: The `URLAuthenticationChallenge`. + /// - task: The `URLSessionTask` which received the challenge. + /// + /// - Returns: The `ChallengeEvaluation`. + func attemptCredentialAuthentication(for challenge: URLAuthenticationChallenge, + belongingTo task: URLSessionTask) -> ChallengeEvaluation { + guard challenge.previousFailureCount == 0 else { + return (.rejectProtectionSpace, nil, nil) + } + + guard let credential = stateProvider?.credential(for: task, in: challenge.protectionSpace) else { + return (.performDefaultHandling, nil, nil) + } + + return (.useCredential, credential, nil) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) { + eventMonitor?.urlSession(session, + task: task, + didSendBodyData: bytesSent, + totalBytesSent: totalBytesSent, + totalBytesExpectedToSend: totalBytesExpectedToSend) + + stateProvider?.request(for: task)?.updateUploadProgress(totalBytesSent: totalBytesSent, + totalBytesExpectedToSend: totalBytesExpectedToSend) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) { + eventMonitor?.urlSession(session, taskNeedsNewBodyStream: task) + + guard let request = request(for: task, as: UploadRequest.self) else { + assertionFailure("needNewBodyStream did not find UploadRequest.") + completionHandler(nil) + return + } + + completionHandler(request.inputStream()) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) { + eventMonitor?.urlSession(session, task: task, willPerformHTTPRedirection: response, newRequest: request) + + if let redirectHandler = stateProvider?.request(for: task)?.redirectHandler ?? stateProvider?.redirectHandler { + redirectHandler.task(task, willBeRedirectedTo: request, for: response, completion: completionHandler) + } else { + completionHandler(request) + } + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + eventMonitor?.urlSession(session, task: task, didFinishCollecting: metrics) + + stateProvider?.request(for: task)?.didGatherMetrics(metrics) + + stateProvider?.didGatherMetricsForTask(task) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + eventMonitor?.urlSession(session, task: task, didCompleteWithError: error) + + let request = stateProvider?.request(for: task) + + stateProvider?.didCompleteTask(task) { + request?.didCompleteTask(task, with: error.map { $0.asAFError(or: .sessionTaskFailed(error: $0)) }) + } + } + + @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) + open func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { + eventMonitor?.urlSession(session, taskIsWaitingForConnectivity: task) + } +} + +// MARK: URLSessionDataDelegate + +extension SessionDelegate: URLSessionDataDelegate { + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + eventMonitor?.urlSession(session, dataTask: dataTask, didReceive: data) + + if let request = request(for: dataTask, as: DataRequest.self) { + request.didReceive(data: data) + } else if let request = request(for: dataTask, as: DataStreamRequest.self) { + request.didReceive(data: data) + } else { + assertionFailure("dataTask did not find DataRequest or DataStreamRequest in didReceive") + return + } + } + + open func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + willCacheResponse proposedResponse: CachedURLResponse, + completionHandler: @escaping (CachedURLResponse?) -> Void) { + eventMonitor?.urlSession(session, dataTask: dataTask, willCacheResponse: proposedResponse) + + if let handler = stateProvider?.request(for: dataTask)?.cachedResponseHandler ?? stateProvider?.cachedResponseHandler { + handler.dataTask(dataTask, willCacheResponse: proposedResponse, completion: completionHandler) + } else { + completionHandler(proposedResponse) + } + } +} + +// MARK: URLSessionDownloadDelegate + +extension SessionDelegate: URLSessionDownloadDelegate { + open func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) { + eventMonitor?.urlSession(session, + downloadTask: downloadTask, + didResumeAtOffset: fileOffset, + expectedTotalBytes: expectedTotalBytes) + guard let downloadRequest = request(for: downloadTask, as: DownloadRequest.self) else { + assertionFailure("downloadTask did not find DownloadRequest.") + return + } + + downloadRequest.updateDownloadProgress(bytesWritten: fileOffset, + totalBytesExpectedToWrite: expectedTotalBytes) + } + + open func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) { + eventMonitor?.urlSession(session, + downloadTask: downloadTask, + didWriteData: bytesWritten, + totalBytesWritten: totalBytesWritten, + totalBytesExpectedToWrite: totalBytesExpectedToWrite) + guard let downloadRequest = request(for: downloadTask, as: DownloadRequest.self) else { + assertionFailure("downloadTask did not find DownloadRequest.") + return + } + + downloadRequest.updateDownloadProgress(bytesWritten: bytesWritten, + totalBytesExpectedToWrite: totalBytesExpectedToWrite) + } + + open func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + eventMonitor?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location) + + guard let request = request(for: downloadTask, as: DownloadRequest.self) else { + assertionFailure("downloadTask did not find DownloadRequest.") + return + } + + let (destination, options): (URL, DownloadRequest.Options) + if let response = request.response { + (destination, options) = request.destination(location, response) + } else { + // If there's no response this is likely a local file download, so generate the temporary URL directly. + (destination, options) = (DownloadRequest.defaultDestinationURL(location), []) + } + + eventMonitor?.request(request, didCreateDestinationURL: destination) + + do { + if options.contains(.removePreviousFile), fileManager.fileExists(atPath: destination.path) { + try fileManager.removeItem(at: destination) + } + + if options.contains(.createIntermediateDirectories) { + let directory = destination.deletingLastPathComponent() + try fileManager.createDirectory(at: directory, withIntermediateDirectories: true) + } + + try fileManager.moveItem(at: location, to: destination) + + request.didFinishDownloading(using: downloadTask, with: .success(destination)) + } catch { + request.didFinishDownloading(using: downloadTask, with: .failure(.downloadedFileMoveFailed(error: error, + source: location, + destination: destination))) + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/StringEncoding+Alamofire.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/StringEncoding+Alamofire.swift new file mode 100644 index 0000000..8fa6133 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/StringEncoding+Alamofire.swift @@ -0,0 +1,55 @@ +// +// StringEncoding+Alamofire.swift +// +// Copyright (c) 2020 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension String.Encoding { + /// Creates an encoding from the IANA charset name. + /// + /// - Notes: These mappings match those [provided by CoreFoundation](https://opensource.apple.com/source/CF/CF-476.18/CFStringUtilities.c.auto.html) + /// + /// - Parameter name: IANA charset name. + init?(ianaCharsetName name: String) { + switch name.lowercased() { + case "utf-8": + self = .utf8 + case "iso-8859-1": + self = .isoLatin1 + case "unicode-1-1", "iso-10646-ucs-2", "utf-16": + self = .utf16 + case "utf-16be": + self = .utf16BigEndian + case "utf-16le": + self = .utf16LittleEndian + case "utf-32": + self = .utf32 + case "utf-32be": + self = .utf32BigEndian + case "utf-32le": + self = .utf32LittleEndian + default: + return nil + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/URLConvertible+URLRequestConvertible.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/URLConvertible+URLRequestConvertible.swift new file mode 100644 index 0000000..455c4bc --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/URLConvertible+URLRequestConvertible.swift @@ -0,0 +1,105 @@ +// +// URLConvertible+URLRequestConvertible.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Types adopting the `URLConvertible` protocol can be used to construct `URL`s, which can then be used to construct +/// `URLRequests`. +public protocol URLConvertible { + /// Returns a `URL` from the conforming instance or throws. + /// + /// - Returns: The `URL` created from the instance. + /// - Throws: Any error thrown while creating the `URL`. + func asURL() throws -> URL +} + +extension String: URLConvertible { + /// Returns a `URL` if `self` can be used to initialize a `URL` instance, otherwise throws. + /// + /// - Returns: The `URL` initialized with `self`. + /// - Throws: An `AFError.invalidURL` instance. + public func asURL() throws -> URL { + guard let url = URL(string: self) else { throw AFError.invalidURL(url: self) } + + return url + } +} + +extension URL: URLConvertible { + /// Returns `self`. + public func asURL() throws -> URL { self } +} + +extension URLComponents: URLConvertible { + /// Returns a `URL` if the `self`'s `url` is not nil, otherwise throws. + /// + /// - Returns: The `URL` from the `url` property. + /// - Throws: An `AFError.invalidURL` instance. + public func asURL() throws -> URL { + guard let url = url else { throw AFError.invalidURL(url: self) } + + return url + } +} + +// MARK: - + +/// Types adopting the `URLRequestConvertible` protocol can be used to safely construct `URLRequest`s. +public protocol URLRequestConvertible { + /// Returns a `URLRequest` or throws if an `Error` was encountered. + /// + /// - Returns: A `URLRequest`. + /// - Throws: Any error thrown while constructing the `URLRequest`. + func asURLRequest() throws -> URLRequest +} + +extension URLRequestConvertible { + /// The `URLRequest` returned by discarding any `Error` encountered. + public var urlRequest: URLRequest? { try? asURLRequest() } +} + +extension URLRequest: URLRequestConvertible { + /// Returns `self`. + public func asURLRequest() throws -> URLRequest { self } +} + +// MARK: - + +extension URLRequest { + /// Creates an instance with the specified `url`, `method`, and `headers`. + /// + /// - Parameters: + /// - url: The `URLConvertible` value. + /// - method: The `HTTPMethod`. + /// - headers: The `HTTPHeaders`, `nil` by default. + /// - Throws: Any error thrown while converting the `URLConvertible` to a `URL`. + public init(url: URLConvertible, method: HTTPMethod, headers: HTTPHeaders? = nil) throws { + let url = try url.asURL() + + self.init(url: url) + + httpMethod = method.rawValue + allHTTPHeaderFields = headers?.dictionary + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/URLEncodedFormEncoder.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/URLEncodedFormEncoder.swift new file mode 100644 index 0000000..dfadc2e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/URLEncodedFormEncoder.swift @@ -0,0 +1,982 @@ +// +// URLEncodedFormEncoder.swift +// +// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// An object that encodes instances into URL-encoded query strings. +/// +/// There is no published specification for how to encode collection types. By default, the convention of appending +/// `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for +/// nested dictionary values (`foo[bar]=baz`) is used. Optionally, `ArrayEncoding` can be used to omit the +/// square brackets appended to array keys. +/// +/// `BoolEncoding` can be used to configure how `Bool` values are encoded. The default behavior is to encode +/// `true` as 1 and `false` as 0. +/// +/// `DateEncoding` can be used to configure how `Date` values are encoded. By default, the `.deferredToDate` +/// strategy is used, which formats dates from their structure. +/// +/// `SpaceEncoding` can be used to configure how spaces are encoded. Modern encodings use percent replacement (`%20`), +/// while older encodings may expect spaces to be replaced with `+`. +/// +/// This type is largely based on Vapor's [`url-encoded-form`](https://github.com/vapor/url-encoded-form) project. +public final class URLEncodedFormEncoder { + /// Encoding to use for `Array` values. + public enum ArrayEncoding { + /// An empty set of square brackets ("[]") are appended to the key for every value. This is the default encoding. + case brackets + /// No brackets are appended to the key and the key is encoded as is. + case noBrackets + /// Brackets containing the item index are appended. This matches the jQuery and Node.js behavior. + case indexInBrackets + + /// Encodes the key according to the encoding. + /// + /// - Parameters: + /// - key: The `key` to encode. + /// - index: When this enum instance is `.indexInBrackets`, the `index` to encode. + /// + /// - Returns: The encoded key. + func encode(_ key: String, atIndex index: Int) -> String { + switch self { + case .brackets: return "\(key)[]" + case .noBrackets: return key + case .indexInBrackets: return "\(key)[\(index)]" + } + } + } + + /// Encoding to use for `Bool` values. + public enum BoolEncoding { + /// Encodes `true` as `1`, `false` as `0`. + case numeric + /// Encodes `true` as "true", `false` as "false". This is the default encoding. + case literal + + /// Encodes the given `Bool` as a `String`. + /// + /// - Parameter value: The `Bool` to encode. + /// + /// - Returns: The encoded `String`. + func encode(_ value: Bool) -> String { + switch self { + case .numeric: return value ? "1" : "0" + case .literal: return value ? "true" : "false" + } + } + } + + /// Encoding to use for `Data` values. + public enum DataEncoding { + /// Defers encoding to the `Data` type. + case deferredToData + /// Encodes `Data` as a Base64-encoded string. This is the default encoding. + case base64 + /// Encode the `Data` as a custom value encoded by the given closure. + case custom((Data) throws -> String) + + /// Encodes `Data` according to the encoding. + /// + /// - Parameter data: The `Data` to encode. + /// + /// - Returns: The encoded `String`, or `nil` if the `Data` should be encoded according to its + /// `Encodable` implementation. + func encode(_ data: Data) throws -> String? { + switch self { + case .deferredToData: return nil + case .base64: return data.base64EncodedString() + case let .custom(encoding): return try encoding(data) + } + } + } + + /// Encoding to use for `Date` values. + public enum DateEncoding { + /// ISO8601 and RFC3339 formatter. + private static let iso8601Formatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = .withInternetDateTime + return formatter + }() + + /// Defers encoding to the `Date` type. This is the default encoding. + case deferredToDate + /// Encodes `Date`s as seconds since midnight UTC on January 1, 1970. + case secondsSince1970 + /// Encodes `Date`s as milliseconds since midnight UTC on January 1, 1970. + case millisecondsSince1970 + /// Encodes `Date`s according to the ISO8601 and RFC3339 standards. + case iso8601 + /// Encodes `Date`s using the given `DateFormatter`. + case formatted(DateFormatter) + /// Encodes `Date`s using the given closure. + case custom((Date) throws -> String) + + /// Encodes the date according to the encoding. + /// + /// - Parameter date: The `Date` to encode. + /// + /// - Returns: The encoded `String`, or `nil` if the `Date` should be encoded according to its + /// `Encodable` implementation. + func encode(_ date: Date) throws -> String? { + switch self { + case .deferredToDate: + return nil + case .secondsSince1970: + return String(date.timeIntervalSince1970) + case .millisecondsSince1970: + return String(date.timeIntervalSince1970 * 1000.0) + case .iso8601: + return DateEncoding.iso8601Formatter.string(from: date) + case let .formatted(formatter): + return formatter.string(from: date) + case let .custom(closure): + return try closure(date) + } + } + } + + /// Encoding to use for keys. + /// + /// This type is derived from [`JSONEncoder`'s `KeyEncodingStrategy`](https://github.com/apple/swift/blob/6aa313b8dd5f05135f7f878eccc1db6f9fbe34ff/stdlib/public/Darwin/Foundation/JSONEncoder.swift#L128) + /// and [`XMLEncoder`s `KeyEncodingStrategy`](https://github.com/MaxDesiatov/XMLCoder/blob/master/Sources/XMLCoder/Encoder/XMLEncoder.swift#L102). + public enum KeyEncoding { + /// Use the keys specified by each type. This is the default encoding. + case useDefaultKeys + /// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key. + /// + /// Capital characters are determined by testing membership in + /// `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters` + /// (Unicode General Categories Lu and Lt). + /// The conversion to lower case uses `Locale.system`, also known as + /// the ICU "root" locale. This means the result is consistent + /// regardless of the current user's locale and language preferences. + /// + /// Converting from camel case to snake case: + /// 1. Splits words at the boundary of lower-case to upper-case + /// 2. Inserts `_` between words + /// 3. Lowercases the entire string + /// 4. Preserves starting and ending `_`. + /// + /// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`. + /// + /// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted. + case convertToSnakeCase + /// Same as convertToSnakeCase, but using `-` instead of `_`. + /// For example `oneTwoThree` becomes `one-two-three`. + case convertToKebabCase + /// Capitalize the first letter only. + /// For example `oneTwoThree` becomes `OneTwoThree`. + case capitalized + /// Uppercase all letters. + /// For example `oneTwoThree` becomes `ONETWOTHREE`. + case uppercased + /// Lowercase all letters. + /// For example `oneTwoThree` becomes `onetwothree`. + case lowercased + /// A custom encoding using the provided closure. + case custom((String) -> String) + + func encode(_ key: String) -> String { + switch self { + case .useDefaultKeys: return key + case .convertToSnakeCase: return convertToSnakeCase(key) + case .convertToKebabCase: return convertToKebabCase(key) + case .capitalized: return String(key.prefix(1).uppercased() + key.dropFirst()) + case .uppercased: return key.uppercased() + case .lowercased: return key.lowercased() + case let .custom(encoding): return encoding(key) + } + } + + private func convertToSnakeCase(_ key: String) -> String { + convert(key, usingSeparator: "_") + } + + private func convertToKebabCase(_ key: String) -> String { + convert(key, usingSeparator: "-") + } + + private func convert(_ key: String, usingSeparator separator: String) -> String { + guard !key.isEmpty else { return key } + + var words: [Range] = [] + // The general idea of this algorithm is to split words on + // transition from lower to upper case, then on transition of >1 + // upper case characters to lowercase + // + // myProperty -> my_property + // myURLProperty -> my_url_property + // + // It is assumed, per Swift naming conventions, that the first character of the key is lowercase. + var wordStart = key.startIndex + var searchRange = key.index(after: wordStart)..1 capital letters. Turn those into a word, stopping at the capital before the lower case character. + let beforeLowerIndex = key.index(before: lowerCaseRange.lowerBound) + words.append(upperCaseRange.lowerBound.. String { + switch self { + case .percentEscaped: return string.replacingOccurrences(of: " ", with: "%20") + case .plusReplaced: return string.replacingOccurrences(of: " ", with: "+") + } + } + } + + /// `URLEncodedFormEncoder` error. + public enum Error: Swift.Error { + /// An invalid root object was created by the encoder. Only keyed values are valid. + case invalidRootObject(String) + + var localizedDescription: String { + switch self { + case let .invalidRootObject(object): + return "URLEncodedFormEncoder requires keyed root object. Received \(object) instead." + } + } + } + + /// Whether or not to sort the encoded key value pairs. + /// + /// - Note: This setting ensures a consistent ordering for all encodings of the same parameters. When set to `false`, + /// encoded `Dictionary` values may have a different encoded order each time they're encoded due to + /// ` Dictionary`'s random storage order, but `Encodable` types will maintain their encoded order. + public let alphabetizeKeyValuePairs: Bool + /// The `ArrayEncoding` to use. + public let arrayEncoding: ArrayEncoding + /// The `BoolEncoding` to use. + public let boolEncoding: BoolEncoding + /// THe `DataEncoding` to use. + public let dataEncoding: DataEncoding + /// The `DateEncoding` to use. + public let dateEncoding: DateEncoding + /// The `KeyEncoding` to use. + public let keyEncoding: KeyEncoding + /// The `SpaceEncoding` to use. + public let spaceEncoding: SpaceEncoding + /// The `CharacterSet` of allowed (non-escaped) characters. + public var allowedCharacters: CharacterSet + + /// Creates an instance from the supplied parameters. + /// + /// - Parameters: + /// - alphabetizeKeyValuePairs: Whether or not to sort the encoded key value pairs. `true` by default. + /// - arrayEncoding: The `ArrayEncoding` to use. `.brackets` by default. + /// - boolEncoding: The `BoolEncoding` to use. `.numeric` by default. + /// - dataEncoding: The `DataEncoding` to use. `.base64` by default. + /// - dateEncoding: The `DateEncoding` to use. `.deferredToDate` by default. + /// - keyEncoding: The `KeyEncoding` to use. `.useDefaultKeys` by default. + /// - spaceEncoding: The `SpaceEncoding` to use. `.percentEscaped` by default. + /// - allowedCharacters: The `CharacterSet` of allowed (non-escaped) characters. `.afURLQueryAllowed` by + /// default. + public init(alphabetizeKeyValuePairs: Bool = true, + arrayEncoding: ArrayEncoding = .brackets, + boolEncoding: BoolEncoding = .numeric, + dataEncoding: DataEncoding = .base64, + dateEncoding: DateEncoding = .deferredToDate, + keyEncoding: KeyEncoding = .useDefaultKeys, + spaceEncoding: SpaceEncoding = .percentEscaped, + allowedCharacters: CharacterSet = .afURLQueryAllowed) { + self.alphabetizeKeyValuePairs = alphabetizeKeyValuePairs + self.arrayEncoding = arrayEncoding + self.boolEncoding = boolEncoding + self.dataEncoding = dataEncoding + self.dateEncoding = dateEncoding + self.keyEncoding = keyEncoding + self.spaceEncoding = spaceEncoding + self.allowedCharacters = allowedCharacters + } + + func encode(_ value: Encodable) throws -> URLEncodedFormComponent { + let context = URLEncodedFormContext(.object([])) + let encoder = _URLEncodedFormEncoder(context: context, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + try value.encode(to: encoder) + + return context.component + } + + /// Encodes the `value` as a URL form encoded `String`. + /// + /// - Parameter value: The `Encodable` value.` + /// + /// - Returns: The encoded `String`. + /// - Throws: An `Error` or `EncodingError` instance if encoding fails. + public func encode(_ value: Encodable) throws -> String { + let component: URLEncodedFormComponent = try encode(value) + + guard case let .object(object) = component else { + throw Error.invalidRootObject("\(component)") + } + + let serializer = URLEncodedFormSerializer(alphabetizeKeyValuePairs: alphabetizeKeyValuePairs, + arrayEncoding: arrayEncoding, + keyEncoding: keyEncoding, + spaceEncoding: spaceEncoding, + allowedCharacters: allowedCharacters) + let query = serializer.serialize(object) + + return query + } + + /// Encodes the value as `Data`. This is performed by first creating an encoded `String` and then returning the + /// `.utf8` data. + /// + /// - Parameter value: The `Encodable` value. + /// + /// - Returns: The encoded `Data`. + /// + /// - Throws: An `Error` or `EncodingError` instance if encoding fails. + public func encode(_ value: Encodable) throws -> Data { + let string: String = try encode(value) + + return Data(string.utf8) + } +} + +final class _URLEncodedFormEncoder { + var codingPath: [CodingKey] + // Returns an empty dictionary, as this encoder doesn't support userInfo. + var userInfo: [CodingUserInfoKey: Any] { [:] } + + let context: URLEncodedFormContext + + private let boolEncoding: URLEncodedFormEncoder.BoolEncoding + private let dataEncoding: URLEncodedFormEncoder.DataEncoding + private let dateEncoding: URLEncodedFormEncoder.DateEncoding + + init(context: URLEncodedFormContext, + codingPath: [CodingKey] = [], + boolEncoding: URLEncodedFormEncoder.BoolEncoding, + dataEncoding: URLEncodedFormEncoder.DataEncoding, + dateEncoding: URLEncodedFormEncoder.DateEncoding) { + self.context = context + self.codingPath = codingPath + self.boolEncoding = boolEncoding + self.dataEncoding = dataEncoding + self.dateEncoding = dateEncoding + } +} + +extension _URLEncodedFormEncoder: Encoder { + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { + let container = _URLEncodedFormEncoder.KeyedContainer(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + return KeyedEncodingContainer(container) + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + _URLEncodedFormEncoder.UnkeyedContainer(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + } + + func singleValueContainer() -> SingleValueEncodingContainer { + _URLEncodedFormEncoder.SingleValueContainer(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + } +} + +final class URLEncodedFormContext { + var component: URLEncodedFormComponent + + init(_ component: URLEncodedFormComponent) { + self.component = component + } +} + +enum URLEncodedFormComponent { + typealias Object = [(key: String, value: URLEncodedFormComponent)] + + case string(String) + case array([URLEncodedFormComponent]) + case object(Object) + + /// Converts self to an `[URLEncodedFormData]` or returns `nil` if not convertible. + var array: [URLEncodedFormComponent]? { + switch self { + case let .array(array): return array + default: return nil + } + } + + /// Converts self to an `Object` or returns `nil` if not convertible. + var object: Object? { + switch self { + case let .object(object): return object + default: return nil + } + } + + /// Sets self to the supplied value at a given path. + /// + /// data.set(to: "hello", at: ["path", "to", "value"]) + /// + /// - parameters: + /// - value: Value of `Self` to set at the supplied path. + /// - path: `CodingKey` path to update with the supplied value. + public mutating func set(to value: URLEncodedFormComponent, at path: [CodingKey]) { + set(&self, to: value, at: path) + } + + /// Recursive backing method to `set(to:at:)`. + private func set(_ context: inout URLEncodedFormComponent, to value: URLEncodedFormComponent, at path: [CodingKey]) { + guard !path.isEmpty else { + context = value + return + } + + let end = path[0] + var child: URLEncodedFormComponent + switch path.count { + case 1: + child = value + case 2...: + if let index = end.intValue { + let array = context.array ?? [] + if array.count > index { + child = array[index] + } else { + child = .array([]) + } + set(&child, to: value, at: Array(path[1...])) + } else { + child = context.object?.first { $0.key == end.stringValue }?.value ?? .object(.init()) + set(&child, to: value, at: Array(path[1...])) + } + default: fatalError("Unreachable") + } + + if let index = end.intValue { + if var array = context.array { + if array.count > index { + array[index] = child + } else { + array.append(child) + } + context = .array(array) + } else { + context = .array([child]) + } + } else { + if var object = context.object { + if let index = object.firstIndex(where: { $0.key == end.stringValue }) { + object[index] = (key: end.stringValue, value: child) + } else { + object.append((key: end.stringValue, value: child)) + } + context = .object(object) + } else { + context = .object([(key: end.stringValue, value: child)]) + } + } + } +} + +struct AnyCodingKey: CodingKey, Hashable { + let stringValue: String + let intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + intValue = nil + } + + init?(intValue: Int) { + stringValue = "\(intValue)" + self.intValue = intValue + } + + init(_ base: Key) where Key: CodingKey { + if let intValue = base.intValue { + self.init(intValue: intValue)! + } else { + self.init(stringValue: base.stringValue)! + } + } +} + +extension _URLEncodedFormEncoder { + final class KeyedContainer where Key: CodingKey { + var codingPath: [CodingKey] + + private let context: URLEncodedFormContext + private let boolEncoding: URLEncodedFormEncoder.BoolEncoding + private let dataEncoding: URLEncodedFormEncoder.DataEncoding + private let dateEncoding: URLEncodedFormEncoder.DateEncoding + + init(context: URLEncodedFormContext, + codingPath: [CodingKey], + boolEncoding: URLEncodedFormEncoder.BoolEncoding, + dataEncoding: URLEncodedFormEncoder.DataEncoding, + dateEncoding: URLEncodedFormEncoder.DateEncoding) { + self.context = context + self.codingPath = codingPath + self.boolEncoding = boolEncoding + self.dataEncoding = dataEncoding + self.dateEncoding = dateEncoding + } + + private func nestedCodingPath(for key: CodingKey) -> [CodingKey] { + codingPath + [key] + } + } +} + +extension _URLEncodedFormEncoder.KeyedContainer: KeyedEncodingContainerProtocol { + func encodeNil(forKey key: Key) throws { + let context = EncodingError.Context(codingPath: codingPath, + debugDescription: "URLEncodedFormEncoder cannot encode nil values.") + throw EncodingError.invalidValue("\(key): nil", context) + } + + func encode(_ value: T, forKey key: Key) throws where T: Encodable { + var container = nestedSingleValueEncoder(for: key) + try container.encode(value) + } + + func nestedSingleValueEncoder(for key: Key) -> SingleValueEncodingContainer { + let container = _URLEncodedFormEncoder.SingleValueContainer(context: context, + codingPath: nestedCodingPath(for: key), + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + + return container + } + + func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + let container = _URLEncodedFormEncoder.UnkeyedContainer(context: context, + codingPath: nestedCodingPath(for: key), + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + + return container + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey: CodingKey { + let container = _URLEncodedFormEncoder.KeyedContainer(context: context, + codingPath: nestedCodingPath(for: key), + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + + return KeyedEncodingContainer(container) + } + + func superEncoder() -> Encoder { + _URLEncodedFormEncoder(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + } + + func superEncoder(forKey key: Key) -> Encoder { + _URLEncodedFormEncoder(context: context, + codingPath: nestedCodingPath(for: key), + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + } +} + +extension _URLEncodedFormEncoder { + final class SingleValueContainer { + var codingPath: [CodingKey] + + private var canEncodeNewValue = true + + private let context: URLEncodedFormContext + private let boolEncoding: URLEncodedFormEncoder.BoolEncoding + private let dataEncoding: URLEncodedFormEncoder.DataEncoding + private let dateEncoding: URLEncodedFormEncoder.DateEncoding + + init(context: URLEncodedFormContext, + codingPath: [CodingKey], + boolEncoding: URLEncodedFormEncoder.BoolEncoding, + dataEncoding: URLEncodedFormEncoder.DataEncoding, + dateEncoding: URLEncodedFormEncoder.DateEncoding) { + self.context = context + self.codingPath = codingPath + self.boolEncoding = boolEncoding + self.dataEncoding = dataEncoding + self.dateEncoding = dateEncoding + } + + private func checkCanEncode(value: Any?) throws { + guard canEncodeNewValue else { + let context = EncodingError.Context(codingPath: codingPath, + debugDescription: "Attempt to encode value through single value container when previously value already encoded.") + throw EncodingError.invalidValue(value as Any, context) + } + } + } +} + +extension _URLEncodedFormEncoder.SingleValueContainer: SingleValueEncodingContainer { + func encodeNil() throws { + try checkCanEncode(value: nil) + defer { canEncodeNewValue = false } + + let context = EncodingError.Context(codingPath: codingPath, + debugDescription: "URLEncodedFormEncoder cannot encode nil values.") + throw EncodingError.invalidValue("nil", context) + } + + func encode(_ value: Bool) throws { + try encode(value, as: String(boolEncoding.encode(value))) + } + + func encode(_ value: String) throws { + try encode(value, as: value) + } + + func encode(_ value: Double) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Float) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int8) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int16) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int32) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int64) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt8) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt16) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt32) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt64) throws { + try encode(value, as: String(value)) + } + + private func encode(_ value: T, as string: String) throws where T: Encodable { + try checkCanEncode(value: value) + defer { canEncodeNewValue = false } + + context.component.set(to: .string(string), at: codingPath) + } + + func encode(_ value: T) throws where T: Encodable { + switch value { + case let date as Date: + guard let string = try dateEncoding.encode(date) else { + try attemptToEncode(value) + return + } + + try encode(value, as: string) + case let data as Data: + guard let string = try dataEncoding.encode(data) else { + try attemptToEncode(value) + return + } + + try encode(value, as: string) + case let decimal as Decimal: + // Decimal's `Encodable` implementation returns an object, not a single value, so override it. + try encode(value, as: String(describing: decimal)) + default: + try attemptToEncode(value) + } + } + + private func attemptToEncode(_ value: T) throws where T: Encodable { + try checkCanEncode(value: value) + defer { canEncodeNewValue = false } + + let encoder = _URLEncodedFormEncoder(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + try value.encode(to: encoder) + } +} + +extension _URLEncodedFormEncoder { + final class UnkeyedContainer { + var codingPath: [CodingKey] + + var count = 0 + var nestedCodingPath: [CodingKey] { + codingPath + [AnyCodingKey(intValue: count)!] + } + + private let context: URLEncodedFormContext + private let boolEncoding: URLEncodedFormEncoder.BoolEncoding + private let dataEncoding: URLEncodedFormEncoder.DataEncoding + private let dateEncoding: URLEncodedFormEncoder.DateEncoding + + init(context: URLEncodedFormContext, + codingPath: [CodingKey], + boolEncoding: URLEncodedFormEncoder.BoolEncoding, + dataEncoding: URLEncodedFormEncoder.DataEncoding, + dateEncoding: URLEncodedFormEncoder.DateEncoding) { + self.context = context + self.codingPath = codingPath + self.boolEncoding = boolEncoding + self.dataEncoding = dataEncoding + self.dateEncoding = dateEncoding + } + } +} + +extension _URLEncodedFormEncoder.UnkeyedContainer: UnkeyedEncodingContainer { + func encodeNil() throws { + let context = EncodingError.Context(codingPath: codingPath, + debugDescription: "URLEncodedFormEncoder cannot encode nil values.") + throw EncodingError.invalidValue("nil", context) + } + + func encode(_ value: T) throws where T: Encodable { + var container = nestedSingleValueContainer() + try container.encode(value) + } + + func nestedSingleValueContainer() -> SingleValueEncodingContainer { + defer { count += 1 } + + return _URLEncodedFormEncoder.SingleValueContainer(context: context, + codingPath: nestedCodingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey: CodingKey { + defer { count += 1 } + let container = _URLEncodedFormEncoder.KeyedContainer(context: context, + codingPath: nestedCodingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + + return KeyedEncodingContainer(container) + } + + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + defer { count += 1 } + + return _URLEncodedFormEncoder.UnkeyedContainer(context: context, + codingPath: nestedCodingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + } + + func superEncoder() -> Encoder { + defer { count += 1 } + + return _URLEncodedFormEncoder(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + } +} + +final class URLEncodedFormSerializer { + private let alphabetizeKeyValuePairs: Bool + private let arrayEncoding: URLEncodedFormEncoder.ArrayEncoding + private let keyEncoding: URLEncodedFormEncoder.KeyEncoding + private let spaceEncoding: URLEncodedFormEncoder.SpaceEncoding + private let allowedCharacters: CharacterSet + + init(alphabetizeKeyValuePairs: Bool, + arrayEncoding: URLEncodedFormEncoder.ArrayEncoding, + keyEncoding: URLEncodedFormEncoder.KeyEncoding, + spaceEncoding: URLEncodedFormEncoder.SpaceEncoding, + allowedCharacters: CharacterSet) { + self.alphabetizeKeyValuePairs = alphabetizeKeyValuePairs + self.arrayEncoding = arrayEncoding + self.keyEncoding = keyEncoding + self.spaceEncoding = spaceEncoding + self.allowedCharacters = allowedCharacters + } + + func serialize(_ object: URLEncodedFormComponent.Object) -> String { + var output: [String] = [] + for (key, component) in object { + let value = serialize(component, forKey: key) + output.append(value) + } + output = alphabetizeKeyValuePairs ? output.sorted() : output + + return output.joinedWithAmpersands() + } + + func serialize(_ component: URLEncodedFormComponent, forKey key: String) -> String { + switch component { + case let .string(string): return "\(escape(keyEncoding.encode(key)))=\(escape(string))" + case let .array(array): return serialize(array, forKey: key) + case let .object(object): return serialize(object, forKey: key) + } + } + + func serialize(_ object: URLEncodedFormComponent.Object, forKey key: String) -> String { + var segments: [String] = object.map { subKey, value in + let keyPath = "[\(subKey)]" + return serialize(value, forKey: key + keyPath) + } + segments = alphabetizeKeyValuePairs ? segments.sorted() : segments + + return segments.joinedWithAmpersands() + } + + func serialize(_ array: [URLEncodedFormComponent], forKey key: String) -> String { + var segments: [String] = array.enumerated().map { index, component in + let keyPath = arrayEncoding.encode(key, atIndex: index) + return serialize(component, forKey: keyPath) + } + segments = alphabetizeKeyValuePairs ? segments.sorted() : segments + + return segments.joinedWithAmpersands() + } + + func escape(_ query: String) -> String { + var allowedCharactersWithSpace = allowedCharacters + allowedCharactersWithSpace.insert(charactersIn: " ") + let escapedQuery = query.addingPercentEncoding(withAllowedCharacters: allowedCharactersWithSpace) ?? query + let spaceEncodedQuery = spaceEncoding.encode(escapedQuery) + + return spaceEncodedQuery + } +} + +extension Array where Element == String { + func joinedWithAmpersands() -> String { + joined(separator: "&") + } +} + +extension CharacterSet { + /// Creates a CharacterSet from RFC 3986 allowed characters. + /// + /// RFC 3986 states that the following characters are "reserved" characters. + /// + /// - General Delimiters: ":", "#", "[", "]", "@", "?", "/" + /// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "=" + /// + /// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow + /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/" + /// should be percent-escaped in the query string. + public static let afURLQueryAllowed: CharacterSet = { + let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 + let subDelimitersToEncode = "!$&'()*+,;=" + let encodableDelimiters = CharacterSet(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") + + return CharacterSet.urlQueryAllowed.subtracting(encodableDelimiters) + }() +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/URLRequest+Alamofire.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/URLRequest+Alamofire.swift new file mode 100644 index 0000000..be27c8e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/URLRequest+Alamofire.swift @@ -0,0 +1,39 @@ +// +// URLRequest+Alamofire.swift +// +// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension URLRequest { + /// Returns the `httpMethod` as Alamofire's `HTTPMethod` type. + public var method: HTTPMethod? { + get { httpMethod.flatMap(HTTPMethod.init) } + set { httpMethod = newValue?.rawValue } + } + + public func validate() throws { + if method == .get, let bodyData = httpBody { + throw AFError.urlRequestValidationFailed(reason: .bodyDataInGETRequest(bodyData)) + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/URLSessionConfiguration+Alamofire.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/URLSessionConfiguration+Alamofire.swift new file mode 100644 index 0000000..292a8fe --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/URLSessionConfiguration+Alamofire.swift @@ -0,0 +1,46 @@ +// +// URLSessionConfiguration+Alamofire.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension URLSessionConfiguration: AlamofireExtended {} +extension AlamofireExtension where ExtendedType: URLSessionConfiguration { + /// Alamofire's default configuration. Same as `URLSessionConfiguration.default` but adds Alamofire default + /// `Accept-Language`, `Accept-Encoding`, and `User-Agent` headers. + public static var `default`: URLSessionConfiguration { + let configuration = URLSessionConfiguration.default + configuration.headers = .default + + return configuration + } + + /// `.ephemeral` configuration with Alamofire's default `Accept-Language`, `Accept-Encoding`, and `User-Agent` + /// headers. + public static var ephemeral: URLSessionConfiguration { + let configuration = URLSessionConfiguration.ephemeral + configuration.headers = .default + + return configuration + } +} diff --git a/jaem/week6/NewsApp/Pods/Alamofire/Source/Validation.swift b/jaem/week6/NewsApp/Pods/Alamofire/Source/Validation.swift new file mode 100644 index 0000000..1dc3025 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Alamofire/Source/Validation.swift @@ -0,0 +1,302 @@ +// +// Validation.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension Request { + // MARK: Helper Types + + fileprivate typealias ErrorReason = AFError.ResponseValidationFailureReason + + /// Used to represent whether a validation succeeded or failed. + public typealias ValidationResult = Result + + fileprivate struct MIMEType { + let type: String + let subtype: String + + var isWildcard: Bool { type == "*" && subtype == "*" } + + init?(_ string: String) { + let components: [String] = { + let stripped = string.trimmingCharacters(in: .whitespacesAndNewlines) + let split = stripped[..<(stripped.range(of: ";")?.lowerBound ?? stripped.endIndex)] + + return split.components(separatedBy: "/") + }() + + if let type = components.first, let subtype = components.last { + self.type = type + self.subtype = subtype + } else { + return nil + } + } + + func matches(_ mime: MIMEType) -> Bool { + switch (type, subtype) { + case (mime.type, mime.subtype), (mime.type, "*"), ("*", mime.subtype), ("*", "*"): + return true + default: + return false + } + } + } + + // MARK: Properties + + fileprivate var acceptableStatusCodes: Range { 200..<300 } + + fileprivate var acceptableContentTypes: [String] { + if let accept = request?.value(forHTTPHeaderField: "Accept") { + return accept.components(separatedBy: ",") + } + + return ["*/*"] + } + + // MARK: Status Code + + fileprivate func validate(statusCode acceptableStatusCodes: S, + response: HTTPURLResponse) + -> ValidationResult + where S.Iterator.Element == Int { + if acceptableStatusCodes.contains(response.statusCode) { + return .success(()) + } else { + let reason: ErrorReason = .unacceptableStatusCode(code: response.statusCode) + return .failure(AFError.responseValidationFailed(reason: reason)) + } + } + + // MARK: Content Type + + fileprivate func validate(contentType acceptableContentTypes: S, + response: HTTPURLResponse, + data: Data?) + -> ValidationResult + where S.Iterator.Element == String { + guard let data = data, !data.isEmpty else { return .success(()) } + + return validate(contentType: acceptableContentTypes, response: response) + } + + fileprivate func validate(contentType acceptableContentTypes: S, + response: HTTPURLResponse) + -> ValidationResult + where S.Iterator.Element == String { + guard + let responseContentType = response.mimeType, + let responseMIMEType = MIMEType(responseContentType) + else { + for contentType in acceptableContentTypes { + if let mimeType = MIMEType(contentType), mimeType.isWildcard { + return .success(()) + } + } + + let error: AFError = { + let reason: ErrorReason = .missingContentType(acceptableContentTypes: acceptableContentTypes.sorted()) + return AFError.responseValidationFailed(reason: reason) + }() + + return .failure(error) + } + + for contentType in acceptableContentTypes { + if let acceptableMIMEType = MIMEType(contentType), acceptableMIMEType.matches(responseMIMEType) { + return .success(()) + } + } + + let error: AFError = { + let reason: ErrorReason = .unacceptableContentType(acceptableContentTypes: acceptableContentTypes.sorted(), + responseContentType: responseContentType) + + return AFError.responseValidationFailed(reason: reason) + }() + + return .failure(error) + } +} + +// MARK: - + +extension DataRequest { + /// A closure used to validate a request that takes a URL request, a URL response and data, and returns whether the + /// request was valid. + public typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult + + /// Validates that the response has a status code in the specified sequence. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes. + /// + /// - Returns: The instance. + @discardableResult + public func validate(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int { + validate { [unowned self] _, response, _ in + self.validate(statusCode: acceptableStatusCodes, response: response) + } + } + + /// Validates that the response has a content type in the specified sequence. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes. + /// + /// - returns: The request. + @discardableResult + public func validate(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String { + validate { [unowned self] _, response, data in + self.validate(contentType: acceptableContentTypes(), response: response, data: data) + } + } + + /// Validates that the response has a status code in the default acceptable range of 200...299, and that the content + /// type matches any specified in the Accept HTTP header field. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - returns: The request. + @discardableResult + public func validate() -> Self { + let contentTypes: () -> [String] = { [unowned self] in + self.acceptableContentTypes + } + return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes()) + } +} + +extension DataStreamRequest { + /// A closure used to validate a request that takes a `URLRequest` and `HTTPURLResponse` and returns whether the + /// request was valid. + public typealias Validation = (_ request: URLRequest?, _ response: HTTPURLResponse) -> ValidationResult + + /// Validates that the response has a status code in the specified sequence. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes. + /// + /// - Returns: The instance. + @discardableResult + public func validate(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int { + validate { [unowned self] _, response in + self.validate(statusCode: acceptableStatusCodes, response: response) + } + } + + /// Validates that the response has a content type in the specified sequence. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes. + /// + /// - returns: The request. + @discardableResult + public func validate(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String { + validate { [unowned self] _, response in + self.validate(contentType: acceptableContentTypes(), response: response) + } + } + + /// Validates that the response has a status code in the default acceptable range of 200...299, and that the content + /// type matches any specified in the Accept HTTP header field. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - Returns: The instance. + @discardableResult + public func validate() -> Self { + let contentTypes: () -> [String] = { [unowned self] in + self.acceptableContentTypes + } + return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes()) + } +} + +// MARK: - + +extension DownloadRequest { + /// A closure used to validate a request that takes a URL request, a URL response, a temporary URL and a + /// destination URL, and returns whether the request was valid. + public typealias Validation = (_ request: URLRequest?, + _ response: HTTPURLResponse, + _ fileURL: URL?) + -> ValidationResult + + /// Validates that the response has a status code in the specified sequence. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes. + /// + /// - Returns: The instance. + @discardableResult + public func validate(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int { + validate { [unowned self] _, response, _ in + self.validate(statusCode: acceptableStatusCodes, response: response) + } + } + + /// Validates that the response has a content type in the specified sequence. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes. + /// + /// - returns: The request. + @discardableResult + public func validate(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String { + validate { [unowned self] _, response, fileURL in + guard let validFileURL = fileURL else { + return .failure(AFError.responseValidationFailed(reason: .dataFileNil)) + } + + do { + let data = try Data(contentsOf: validFileURL) + return self.validate(contentType: acceptableContentTypes(), response: response, data: data) + } catch { + return .failure(AFError.responseValidationFailed(reason: .dataFileReadFailed(at: validFileURL))) + } + } + } + + /// Validates that the response has a status code in the default acceptable range of 200...299, and that the content + /// type matches any specified in the Accept HTTP header field. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - returns: The request. + @discardableResult + public func validate() -> Self { + let contentTypes = { [unowned self] in + self.acceptableContentTypes + } + return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes()) + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/LICENSE b/jaem/week6/NewsApp/Pods/Kingfisher/LICENSE new file mode 100644 index 0000000..80888ba --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2019 Wei Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/README.md b/jaem/week6/NewsApp/Pods/Kingfisher/README.md new file mode 100644 index 0000000..b486aee --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/README.md @@ -0,0 +1,258 @@ +

    +Kingfisher +

    + +
    + +Kingfisher is a powerful, pure-Swift library for downloading and caching images from the web. It provides you a chance to use a pure-Swift way to work with remote images in your next app. + +## Features + +- [x] Asynchronous image downloading and caching. +- [x] Loading image from either `URLSession`-based networking or local provided data. +- [x] Useful image processors and filters provided. +- [x] Multiple-layer hybrid cache for both memory and disk. +- [x] Fine control on cache behavior. Customizable expiration date and size limit. +- [x] Cancelable downloading and auto-reusing previous downloaded content to improve performance. +- [x] Independent components. Use the downloader, caching system, and image processors separately as you need. +- [x] Prefetching images and showing them from the cache to boost your app. +- [x] View extensions for `UIImageView`, `NSImageView`, `NSButton` and `UIButton` to directly set an image from a URL. +- [x] Built-in transition animation when setting images. +- [x] Customizable placeholder and indicator while loading images. +- [x] Extensible image processing and image format easily. +- [x] Low Data Mode support. +- [x] SwiftUI support. + +### Kingfisher 101 + +The simplest use-case is setting an image to an image view with the `UIImageView` extension: + +```swift +import Kingfisher + +let url = URL(string: "https://example.com/image.png") +imageView.kf.setImage(with: url) +``` + +Kingfisher will download the image from `url`, send it to both memory cache and disk cache, and display it in `imageView`. +When you set with the same URL later, the image will be retrieved from the cache and shown immediately. + +It also works if you use SwiftUI: + +```swift +var body: some View { + KFImage(URL(string: "https://example.com/image.png")!) +} +``` + +### A More Advanced Example + +With the powerful options, you can do hard tasks with Kingfisher in a simple way. For example, the code below: + +1. Downloads a high-resolution image. +2. Downsamples it to match the image view size. +3. Makes it round cornered with a given radius. +4. Shows a system indicator and a placeholder image while downloading. +5. When prepared, it animates the small thumbnail image with a "fade in" effect. +6. The original large image is also cached to disk for later use, to get rid of downloading it again in a detail view. +7. A console log is printed when the task finishes, either for success or failure. + +```swift +let url = URL(string: "https://example.com/high_resolution_image.png") +let processor = DownsamplingImageProcessor(size: imageView.bounds.size) + |> RoundCornerImageProcessor(cornerRadius: 20) +imageView.kf.indicatorType = .activity +imageView.kf.setImage( + with: url, + placeholder: UIImage(named: "placeholderImage"), + options: [ + .processor(processor), + .scaleFactor(UIScreen.main.scale), + .transition(.fade(1)), + .cacheOriginalImage + ]) +{ + result in + switch result { + case .success(let value): + print("Task done for: \(value.source.url?.absoluteString ?? "")") + case .failure(let error): + print("Job failed: \(error.localizedDescription)") + } +} +``` + +It is a common situation I can meet in my daily work. Think about how many lines you need to write without +Kingfisher! + +### Method Chaining + +If you are not a fan of the `kf` extension, you can also prefer to use the `KF` builder and chained the method +invocations. The code below is doing the same thing: + +```swift +// Use `kf` extension +imageView.kf.setImage( + with: url, + placeholder: placeholderImage, + options: [ + .processor(processor), + .loadDiskFileSynchronously, + .cacheOriginalImage, + .transition(.fade(0.25)), + .lowDataMode(.network(lowResolutionURL)) + ], + progressBlock: { receivedSize, totalSize in + // Progress updated + }, + completionHandler: { result in + // Done + } +) + +// Use `KF` builder +KF.url(url) + .placeholder(placeholderImage) + .setProcessor(processor) + .loadDiskFileSynchronously() + .cacheMemoryOnly() + .fade(duration: 0.25) + .lowDataModeSource(.network(lowResolutionURL)) + .onProgress { receivedSize, totalSize in } + .onSuccess { result in } + .onFailure { error in } + .set(to: imageView) +``` + +And even better, if later you want to switch to SwiftUI, just change the `KF` above to `KFImage`, and you've done: + +```swift +struct ContentView: View { + var body: some View { + KFImage.url(url) + .placeholder(placeholderImage) + .setProcessor(processor) + .loadDiskFileSynchronously() + .cacheMemoryOnly() + .fade(duration: 0.25) + .lowDataModeSource(.network(lowResolutionURL)) + .onProgress { receivedSize, totalSize in } + .onSuccess { result in } + .onFailure { error in } + } +} +``` + +### Learn More + +To learn the use of Kingfisher by more examples, take a look at the well-prepared [Cheat Sheet](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet). +There we summarized the most common tasks in Kingfisher, you can get a better idea of what this framework can do. +There are also some performance tips, remember to check them too. + +## Requirements + +- iOS 12.0+ / macOS 10.14+ / tvOS 12.0+ / watchOS 5.0+ (if you use only UIKit/AppKit) +- iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+ (if you use it in SwiftUI) +- Swift 5.0+ + +> If you need to support from iOS 10 (UIKit/AppKit) or iOS 13 (SwiftUI), use Kingfisher version 6.x. But it won't work +> with Xcode 13.0 and Xcode 13.1 [#1802](https://github.com/onevcat/Kingfisher/issues/1802). +> +> If you need to use Xcode 13.0 and 13.1 but cannot upgrade to v7, use the `version6-xcode13` branch. However, you have to drop +> iOS 10 support due to another Xcode 13 bug. +> +> | UIKit | SwiftUI | Xcode | Kingfisher | +> |---|---|---|---| +> | iOS 10+ | iOS 13+ | 12 | ~> 6.3.1 | +> | iOS 11+ | iOS 13+ | 13 | `version6-xcode13` | +> | iOS 12+ | iOS 14+ | 13 | ~> 7.0 | + +### Installation + +A detailed guide for installation can be found in [Installation Guide](https://github.com/onevcat/Kingfisher/wiki/Installation-Guide). + +#### Swift Package Manager + +- File > Swift Packages > Add Package Dependency +- Add `https://github.com/onevcat/Kingfisher.git` +- Select "Up to Next Major" with "7.0.0" + +#### CocoaPods + +```ruby +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '12.0' +use_frameworks! + +target 'MyApp' do + pod 'Kingfisher', '~> 7.0' +end +``` + +#### Carthage + +``` +github "onevcat/Kingfisher" ~> 7.0 +``` + + +### Migrating + +[Kingfisher 7.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-7.0-Migration-Guide) - Kingfisher 7.x is NOT fully compatible with the previous version. However, changes should be trivial or not required at all. Please follow the [migration guide](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-7.0-Migration-Guide) when you prepare to upgrade Kingfisher in your project. + +If you are using an even earlier version, see the guides below to know the steps for migrating. + +> - [Kingfisher 6.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-6.0-Migration-Guide) - Kingfisher 6.x is NOT fully compatible with the previous version. However, the migration is not difficult. Depending on your use cases, it may take no effect or several minutes to modify your existing code for the new version. Please follow the [migration guide](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-6.0-Migration-Guide) when you prepare to upgrade Kingfisher in your project. +> - [Kingfisher 5.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-5.0-Migration-Guide) - If you are upgrading to Kingfisher 5.x from 4.x, please read this for more information. +> - Kingfisher 4.0 Migration - Kingfisher 3.x should be source compatible to Kingfisher 4. The reason for a major update is that we need to specify the Swift version explicitly for Xcode. All deprecated methods in Kingfisher 3 were removed, so please ensure you have no warning left before you migrate from Kingfisher 3 to Kingfisher 4. If you have any trouble when migrating, please open an issue to discuss. +> - [Kingfisher 3.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-3.0-Migration-Guide) - If you are upgrading to Kingfisher 3.x from an earlier version, please read this for more information. + +## Next Steps + +We prepared a [wiki page](https://github.com/onevcat/Kingfisher/wiki). You can find tons of useful things there. + +* [Installation Guide](https://github.com/onevcat/Kingfisher/wiki/Installation-Guide) - Follow it to integrate Kingfisher into your project. +* [Cheat Sheet](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet)- Curious about what Kingfisher could do and how would it look like when used in your project? See this page for useful code snippets. If you are already familiar with Kingfisher, you could also learn new tricks to improve the way you use Kingfisher! +* [API Reference](https://kingfisher.onevcat.com/) - Lastly, please remember to read the full API reference whenever you need more detailed documentation. + +## Other + +### Future of Kingfisher + +I want to keep Kingfisher lightweight. This framework focuses on providing a simple solution for downloading and caching images. This doesn’t mean the framework can’t be improved. Kingfisher is far from perfect, so necessary and useful updates will be made to make it better. + +### Developments and Tests + +Any contributing and pull requests are warmly welcome. However, before you plan to implement some features or try to fix an uncertain issue, it is recommended to open a discussion first. It would be appreciated if your pull requests could build and with all tests green. :) + +### About the logo + +The logo of Kingfisher is inspired by [Tangram (七巧板)](http://en.wikipedia.org/wiki/Tangram), a dissection puzzle consisting of seven flat shapes from China. I believe she's a kingfisher bird instead of a swift, but someone insists that she is a pigeon. I guess I should give her a name. Hi, guys, do you have any suggestions? + +### Contact + +Follow and contact me on [Twitter](http://twitter.com/onevcat) or [Sina Weibo](http://weibo.com/onevcat). If you find an issue, [open a ticket](https://github.com/onevcat/Kingfisher/issues/new). Pull requests are warmly welcome as well. + +## Backers & Sponsors + +Open-source projects cannot live long without your help. If you find Kingfisher is useful, please consider supporting this +project by becoming a sponsor. Your user icon or company logo shows up [on my blog](https://onevcat.com/tabs/about/) with a link to your home page. + +Become a sponsor through [GitHub Sponsors](https://github.com/sponsors/onevcat). :heart: + +Special thanks to: + +[![imgly](https://user-images.githubusercontent.com/1812216/106253726-271ed000-6218-11eb-98e0-c9c681925770.png)](https://img.ly/) + +### License + +Kingfisher is released under the MIT license. See LICENSE for details. diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift new file mode 100644 index 0000000..8cb09f2 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift @@ -0,0 +1,117 @@ +// +// CacheSerializer.swift +// Kingfisher +// +// Created by Wei Wang on 2016/09/02. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CoreGraphics + +/// An `CacheSerializer` is used to convert some data to an image object after +/// retrieving it from disk storage, and vice versa, to convert an image to data object +/// for storing to the disk storage. +public protocol CacheSerializer { + + /// Gets the serialized data from a provided image + /// and optional original data for caching to disk. + /// + /// - Parameters: + /// - image: The image needed to be serialized. + /// - original: The original data which is just downloaded. + /// If the image is retrieved from cache instead of + /// downloaded, it will be `nil`. + /// - Returns: The data object for storing to disk, or `nil` when no valid + /// data could be serialized. + func data(with image: KFCrossPlatformImage, original: Data?) -> Data? + + /// Gets an image from provided serialized data. + /// + /// - Parameters: + /// - data: The data from which an image should be deserialized. + /// - options: The parsed options for deserialization. + /// - Returns: An image deserialized or `nil` when no valid image + /// could be deserialized. + func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? +} + +/// Represents a basic and default `CacheSerializer` used in Kingfisher disk cache system. +/// It could serialize and deserialize images in PNG, JPEG and GIF format. For +/// image other than these formats, a normalized `pngRepresentation` will be used. +public struct DefaultCacheSerializer: CacheSerializer { + + /// The default general cache serializer used across Kingfisher's cache. + public static let `default` = DefaultCacheSerializer() + + /// The compression quality when converting image to a lossy format data. Default is 1.0. + public var compressionQuality: CGFloat = 1.0 + + /// Whether the original data should be preferred when serializing the image. + /// If `true`, the input original data will be checked first and used unless the data is `nil`. + /// In that case, the serialization will fall back to creating data from image. + public var preferCacheOriginalData: Bool = false + + /// Creates a cache serializer that serialize and deserialize images in PNG, JPEG and GIF format. + /// + /// - Note: + /// Use `DefaultCacheSerializer.default` unless you need to specify your own properties. + /// + public init() { } + + /// - Parameters: + /// - image: The image needed to be serialized. + /// - original: The original data which is just downloaded. + /// If the image is retrieved from cache instead of + /// downloaded, it will be `nil`. + /// - Returns: The data object for storing to disk, or `nil` when no valid + /// data could be serialized. + /// + /// - Note: + /// Only when `original` contains valid PNG, JPEG and GIF format data, the `image` will be + /// converted to the corresponding data type. Otherwise, if the `original` is provided but it is not + /// If `original` is `nil`, the input `image` will be encoded as PNG data. + public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? { + if preferCacheOriginalData { + return original ?? + image.kf.data( + format: original?.kf.imageFormat ?? .unknown, + compressionQuality: compressionQuality + ) + } else { + return image.kf.data( + format: original?.kf.imageFormat ?? .unknown, + compressionQuality: compressionQuality + ) + } + } + + /// Gets an image deserialized from provided data. + /// + /// - Parameters: + /// - data: The data from which an image should be deserialized. + /// - options: Options for deserialization. + /// - Returns: An image deserialized or `nil` when no valid image + /// could be deserialized. + public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/DiskStorage.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/DiskStorage.swift new file mode 100644 index 0000000..debe7f4 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/DiskStorage.swift @@ -0,0 +1,586 @@ +// +// DiskStorage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + + +/// Represents a set of conception related to storage which stores a certain type of value in disk. +/// This is a namespace for the disk storage types. A `Backend` with a certain `Config` will be used to describe the +/// storage. See these composed types for more information. +public enum DiskStorage { + + /// Represents a storage back-end for the `DiskStorage`. The value is serialized to data + /// and stored as file in the file system under a specified location. + /// + /// You can config a `DiskStorage.Backend` in its initializer by passing a `DiskStorage.Config` value. + /// or modifying the `config` property after it being created. `DiskStorage` will use file's attributes to keep + /// track of a file for its expiration or size limitation. + public class Backend { + /// The config used for this disk storage. + public var config: Config + + // The final storage URL on disk, with `name` and `cachePathBlock` considered. + public let directoryURL: URL + + let metaChangingQueue: DispatchQueue + + var maybeCached : Set? + let maybeCachedCheckingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.maybeCachedCheckingQueue") + + // `false` if the storage initialized with an error. This prevents unexpected forcibly crash when creating + // storage in the default cache. + private var storageReady: Bool = true + + /// Creates a disk storage with the given `DiskStorage.Config`. + /// + /// - Parameter config: The config used for this disk storage. + /// - Throws: An error if the folder for storage cannot be got or created. + public convenience init(config: Config) throws { + self.init(noThrowConfig: config, creatingDirectory: false) + try prepareDirectory() + } + + // If `creatingDirectory` is `false`, the directory preparation will be skipped. + // We need to call `prepareDirectory` manually after this returns. + init(noThrowConfig config: Config, creatingDirectory: Bool) { + var config = config + + let creation = Creation(config) + self.directoryURL = creation.directoryURL + + // Break any possible retain cycle set by outside. + config.cachePathBlock = nil + self.config = config + + metaChangingQueue = DispatchQueue(label: creation.cacheName) + setupCacheChecking() + + if creatingDirectory { + try? prepareDirectory() + } + } + + private func setupCacheChecking() { + maybeCachedCheckingQueue.async { + do { + self.maybeCached = Set() + try self.config.fileManager.contentsOfDirectory(atPath: self.directoryURL.path).forEach { fileName in + self.maybeCached?.insert(fileName) + } + } catch { + // Just disable the functionality if we fail to initialize it properly. This will just revert to + // the behavior which is to check file existence on disk directly. + self.maybeCached = nil + } + } + } + + // Creates the storage folder. + private func prepareDirectory() throws { + let fileManager = config.fileManager + let path = directoryURL.path + + guard !fileManager.fileExists(atPath: path) else { return } + + do { + try fileManager.createDirectory( + atPath: path, + withIntermediateDirectories: true, + attributes: nil) + } catch { + self.storageReady = false + throw KingfisherError.cacheError(reason: .cannotCreateDirectory(path: path, error: error)) + } + } + + /// Stores a value to the storage under the specified key and expiration policy. + /// - Parameters: + /// - value: The value to be stored. + /// - key: The key to which the `value` will be stored. If there is already a value under the key, + /// the old value will be overwritten by `value`. + /// - expiration: The expiration policy used by this store action. + /// - writeOptions: Data writing options used the new files. + /// - Throws: An error during converting the value to a data format or during writing it to disk. + public func store( + value: T, + forKey key: String, + expiration: StorageExpiration? = nil, + writeOptions: Data.WritingOptions = []) throws + { + guard storageReady else { + throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL)) + } + + let expiration = expiration ?? config.expiration + // The expiration indicates that already expired, no need to store. + guard !expiration.isExpired else { return } + + let data: Data + do { + data = try value.toData() + } catch { + throw KingfisherError.cacheError(reason: .cannotConvertToData(object: value, error: error)) + } + + let fileURL = cacheFileURL(forKey: key) + do { + try data.write(to: fileURL, options: writeOptions) + } catch { + throw KingfisherError.cacheError( + reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error) + ) + } + + let now = Date() + let attributes: [FileAttributeKey : Any] = [ + // The last access date. + .creationDate: now.fileAttributeDate, + // The estimated expiration date. + .modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate + ] + do { + try config.fileManager.setAttributes(attributes, ofItemAtPath: fileURL.path) + } catch { + try? config.fileManager.removeItem(at: fileURL) + throw KingfisherError.cacheError( + reason: .cannotSetCacheFileAttribute( + filePath: fileURL.path, + attributes: attributes, + error: error + ) + ) + } + + maybeCachedCheckingQueue.async { + self.maybeCached?.insert(fileURL.lastPathComponent) + } + } + + /// Gets a value from the storage. + /// - Parameters: + /// - key: The cache key of value. + /// - extendingExpiration: The expiration policy used by this getting action. + /// - Throws: An error during converting the data to a value or during operation of disk files. + /// - Returns: The value under `key` if it is valid and found in the storage. Otherwise, `nil`. + public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) throws -> T? { + return try value(forKey: key, referenceDate: Date(), actuallyLoad: true, extendingExpiration: extendingExpiration) + } + + func value( + forKey key: String, + referenceDate: Date, + actuallyLoad: Bool, + extendingExpiration: ExpirationExtending) throws -> T? + { + guard storageReady else { + throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL)) + } + + let fileManager = config.fileManager + let fileURL = cacheFileURL(forKey: key) + let filePath = fileURL.path + + let fileMaybeCached = maybeCachedCheckingQueue.sync { + return maybeCached?.contains(fileURL.lastPathComponent) ?? true + } + guard fileMaybeCached else { + return nil + } + guard fileManager.fileExists(atPath: filePath) else { + return nil + } + + let meta: FileMeta + do { + let resourceKeys: Set = [.contentModificationDateKey, .creationDateKey] + meta = try FileMeta(fileURL: fileURL, resourceKeys: resourceKeys) + } catch { + throw KingfisherError.cacheError( + reason: .invalidURLResource(error: error, key: key, url: fileURL)) + } + + if meta.expired(referenceDate: referenceDate) { + return nil + } + if !actuallyLoad { return T.empty } + + do { + let data = try Data(contentsOf: fileURL) + let obj = try T.fromData(data) + metaChangingQueue.async { + meta.extendExpiration(with: fileManager, extendingExpiration: extendingExpiration) + } + return obj + } catch { + throw KingfisherError.cacheError(reason: .cannotLoadDataFromDisk(url: fileURL, error: error)) + } + } + + /// Whether there is valid cached data under a given key. + /// - Parameter key: The cache key of value. + /// - Returns: If there is valid data under the key, `true`. Otherwise, `false`. + /// + /// - Note: + /// This method does not actually load the data from disk, so it is faster than directly loading the cached value + /// by checking the nullability of `value(forKey:extendingExpiration:)` method. + /// + public func isCached(forKey key: String) -> Bool { + return isCached(forKey: key, referenceDate: Date()) + } + + /// Whether there is valid cached data under a given key and a reference date. + /// - Parameters: + /// - key: The cache key of value. + /// - referenceDate: A reference date to check whether the cache is still valid. + /// - Returns: If there is valid data under the key, `true`. Otherwise, `false`. + /// + /// - Note: + /// If you pass `Date()` to `referenceDate`, this method is identical to `isCached(forKey:)`. Use the + /// `referenceDate` to determine whether the cache is still valid for a future date. + public func isCached(forKey key: String, referenceDate: Date) -> Bool { + do { + let result = try value( + forKey: key, + referenceDate: referenceDate, + actuallyLoad: false, + extendingExpiration: .none + ) + return result != nil + } catch { + return false + } + } + + /// Removes a value from a specified key. + /// - Parameter key: The cache key of value. + /// - Throws: An error during removing the value. + public func remove(forKey key: String) throws { + let fileURL = cacheFileURL(forKey: key) + try removeFile(at: fileURL) + } + + func removeFile(at url: URL) throws { + try config.fileManager.removeItem(at: url) + } + + /// Removes all values in this storage. + /// - Throws: An error during removing the values. + public func removeAll() throws { + try removeAll(skipCreatingDirectory: false) + } + + func removeAll(skipCreatingDirectory: Bool) throws { + try config.fileManager.removeItem(at: directoryURL) + if !skipCreatingDirectory { + try prepareDirectory() + } + } + + /// The URL of the cached file with a given computed `key`. + /// + /// - Parameter key: The final computed key used when caching the image. Please note that usually this is not + /// the `cacheKey` of an image `Source`. It is the computed key with processor identifier considered. + /// + /// - Note: + /// This method does not guarantee there is an image already cached in the returned URL. It just gives your + /// the URL that the image should be if it exists in disk storage, with the give key. + /// + public func cacheFileURL(forKey key: String) -> URL { + let fileName = cacheFileName(forKey: key) + return directoryURL.appendingPathComponent(fileName, isDirectory: false) + } + + func cacheFileName(forKey key: String) -> String { + if config.usesHashedFileName { + let hashedKey = key.kf.md5 + if let ext = config.pathExtension { + return "\(hashedKey).\(ext)" + } else if config.autoExtAfterHashedFileName, + let ext = key.kf.ext { + return "\(hashedKey).\(ext)" + } + return hashedKey + } else { + if let ext = config.pathExtension { + return "\(key).\(ext)" + } + return key + } + } + + func allFileURLs(for propertyKeys: [URLResourceKey]) throws -> [URL] { + let fileManager = config.fileManager + + guard let directoryEnumerator = fileManager.enumerator( + at: directoryURL, includingPropertiesForKeys: propertyKeys, options: .skipsHiddenFiles) else + { + throw KingfisherError.cacheError(reason: .fileEnumeratorCreationFailed(url: directoryURL)) + } + + guard let urls = directoryEnumerator.allObjects as? [URL] else { + throw KingfisherError.cacheError(reason: .invalidFileEnumeratorContent(url: directoryURL)) + } + return urls + } + + /// Removes all expired values from this storage. + /// - Throws: A file manager error during removing the file. + /// - Returns: The URLs for removed files. + public func removeExpiredValues() throws -> [URL] { + return try removeExpiredValues(referenceDate: Date()) + } + + func removeExpiredValues(referenceDate: Date) throws -> [URL] { + let propertyKeys: [URLResourceKey] = [ + .isDirectoryKey, + .contentModificationDateKey + ] + + let urls = try allFileURLs(for: propertyKeys) + let keys = Set(propertyKeys) + let expiredFiles = urls.filter { fileURL in + do { + let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys) + if meta.isDirectory { + return false + } + return meta.expired(referenceDate: referenceDate) + } catch { + return true + } + } + try expiredFiles.forEach { url in + try removeFile(at: url) + } + return expiredFiles + } + + /// Removes all size exceeded values from this storage. + /// - Throws: A file manager error during removing the file. + /// - Returns: The URLs for removed files. + /// + /// - Note: This method checks `config.sizeLimit` and remove cached files in an LRU (Least Recently Used) way. + func removeSizeExceededValues() throws -> [URL] { + + if config.sizeLimit == 0 { return [] } // Back compatible. 0 means no limit. + + var size = try totalSize() + if size < config.sizeLimit { return [] } + + let propertyKeys: [URLResourceKey] = [ + .isDirectoryKey, + .creationDateKey, + .fileSizeKey + ] + let keys = Set(propertyKeys) + + let urls = try allFileURLs(for: propertyKeys) + var pendings: [FileMeta] = urls.compactMap { fileURL in + guard let meta = try? FileMeta(fileURL: fileURL, resourceKeys: keys) else { + return nil + } + return meta + } + // Sort by last access date. Most recent file first. + pendings.sort(by: FileMeta.lastAccessDate) + + var removed: [URL] = [] + let target = config.sizeLimit / 2 + while size > target, let meta = pendings.popLast() { + size -= UInt(meta.fileSize) + try removeFile(at: meta.url) + removed.append(meta.url) + } + return removed + } + + /// Gets the total file size of the folder in bytes. + public func totalSize() throws -> UInt { + let propertyKeys: [URLResourceKey] = [.fileSizeKey] + let urls = try allFileURLs(for: propertyKeys) + let keys = Set(propertyKeys) + let totalSize: UInt = urls.reduce(0) { size, fileURL in + do { + let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys) + return size + UInt(meta.fileSize) + } catch { + return size + } + } + return totalSize + } + } +} + +extension DiskStorage { + /// Represents the config used in a `DiskStorage`. + public struct Config { + + /// The file size limit on disk of the storage in bytes. 0 means no limit. + public var sizeLimit: UInt + + /// The `StorageExpiration` used in this disk storage. Default is `.days(7)`, + /// means that the disk cache would expire in one week. + public var expiration: StorageExpiration = .days(7) + + /// The preferred extension of cache item. It will be appended to the file name as its extension. + /// Default is `nil`, means that the cache file does not contain a file extension. + public var pathExtension: String? = nil + + /// Default is `true`, means that the cache file name will be hashed before storing. + public var usesHashedFileName = true + + /// Default is `false` + /// If set to `true`, image extension will be extracted from original file name and append to + /// the hased file name and used as the cache key on disk. + public var autoExtAfterHashedFileName = false + + let name: String + let fileManager: FileManager + let directory: URL? + + var cachePathBlock: ((_ directory: URL, _ cacheName: String) -> URL)! = { + (directory, cacheName) in + return directory.appendingPathComponent(cacheName, isDirectory: true) + } + + /// Creates a config value based on given parameters. + /// + /// - Parameters: + /// - name: The name of cache. It is used as a part of storage folder. It is used to identify the disk + /// storage. Two storages with the same `name` would share the same folder in disk, and it should + /// be prevented. + /// - sizeLimit: The size limit in bytes for all existing files in the disk storage. + /// - fileManager: The `FileManager` used to manipulate files on disk. Default is `FileManager.default`. + /// - directory: The URL where the disk storage should live. The storage will use this as the root folder, + /// and append a path which is constructed by input `name`. Default is `nil`, indicates that + /// the cache directory under user domain mask will be used. + public init( + name: String, + sizeLimit: UInt, + fileManager: FileManager = .default, + directory: URL? = nil) + { + self.name = name + self.fileManager = fileManager + self.directory = directory + self.sizeLimit = sizeLimit + } + } +} + +extension DiskStorage { + struct FileMeta { + + let url: URL + + let lastAccessDate: Date? + let estimatedExpirationDate: Date? + let isDirectory: Bool + let fileSize: Int + + static func lastAccessDate(lhs: FileMeta, rhs: FileMeta) -> Bool { + return lhs.lastAccessDate ?? .distantPast > rhs.lastAccessDate ?? .distantPast + } + + init(fileURL: URL, resourceKeys: Set) throws { + let meta = try fileURL.resourceValues(forKeys: resourceKeys) + self.init( + fileURL: fileURL, + lastAccessDate: meta.creationDate, + estimatedExpirationDate: meta.contentModificationDate, + isDirectory: meta.isDirectory ?? false, + fileSize: meta.fileSize ?? 0) + } + + init( + fileURL: URL, + lastAccessDate: Date?, + estimatedExpirationDate: Date?, + isDirectory: Bool, + fileSize: Int) + { + self.url = fileURL + self.lastAccessDate = lastAccessDate + self.estimatedExpirationDate = estimatedExpirationDate + self.isDirectory = isDirectory + self.fileSize = fileSize + } + + func expired(referenceDate: Date) -> Bool { + return estimatedExpirationDate?.isPast(referenceDate: referenceDate) ?? true + } + + func extendExpiration(with fileManager: FileManager, extendingExpiration: ExpirationExtending) { + guard let lastAccessDate = lastAccessDate, + let lastEstimatedExpiration = estimatedExpirationDate else + { + return + } + + let attributes: [FileAttributeKey : Any] + + switch extendingExpiration { + case .none: + // not extending expiration time here + return + case .cacheTime: + let originalExpiration: StorageExpiration = + .seconds(lastEstimatedExpiration.timeIntervalSince(lastAccessDate)) + attributes = [ + .creationDate: Date().fileAttributeDate, + .modificationDate: originalExpiration.estimatedExpirationSinceNow.fileAttributeDate + ] + case .expirationTime(let expirationTime): + attributes = [ + .creationDate: Date().fileAttributeDate, + .modificationDate: expirationTime.estimatedExpirationSinceNow.fileAttributeDate + ] + } + + try? fileManager.setAttributes(attributes, ofItemAtPath: url.path) + } + } +} + +extension DiskStorage { + struct Creation { + let directoryURL: URL + let cacheName: String + + init(_ config: Config) { + let url: URL + if let directory = config.directory { + url = directory + } else { + url = config.fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] + } + + cacheName = "com.onevcat.Kingfisher.ImageCache.\(config.name)" + directoryURL = config.cachePathBlock(url, cacheName) + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift new file mode 100644 index 0000000..cdfb7c3 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift @@ -0,0 +1,118 @@ +// +// RequestModifier.swift +// Kingfisher +// +// Created by Junyu Kuang on 5/28/17. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CoreGraphics + +/// `FormatIndicatedCacheSerializer` lets you indicate an image format for serialized caches. +/// +/// It could serialize and deserialize PNG, JPEG and GIF images. For +/// image other than these formats, a normalized `pngRepresentation` will be used. +/// +/// Example: +/// ```` +/// let profileImageSize = CGSize(width: 44, height: 44) +/// +/// // A round corner image. +/// let imageProcessor = RoundCornerImageProcessor( +/// cornerRadius: profileImageSize.width / 2, targetSize: profileImageSize) +/// +/// let optionsInfo: KingfisherOptionsInfo = [ +/// .cacheSerializer(FormatIndicatedCacheSerializer.png), +/// .processor(imageProcessor)] +/// +/// A URL pointing to a JPEG image. +/// let url = URL(string: "https://example.com/image.jpg")! +/// +/// // Image will be always cached as PNG format to preserve alpha channel for round rectangle. +/// // So when you load it from cache again later, it will be still round cornered. +/// // Otherwise, the corner part would be filled by white color (since JPEG does not contain an alpha channel). +/// imageView.kf.setImage(with: url, options: optionsInfo) +/// ```` +public struct FormatIndicatedCacheSerializer: CacheSerializer { + + /// A `FormatIndicatedCacheSerializer` which converts image from and to PNG format. If the image cannot be + /// represented by PNG format, it will fallback to its real format which is determined by `original` data. + public static let png = FormatIndicatedCacheSerializer(imageFormat: .PNG, jpegCompressionQuality: nil) + + /// A `FormatIndicatedCacheSerializer` which converts image from and to JPEG format. If the image cannot be + /// represented by JPEG format, it will fallback to its real format which is determined by `original` data. + /// The compression quality is 1.0 when using this serializer. If you need to set a customized compression quality, + /// use `jpeg(compressionQuality:)`. + public static let jpeg = FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: 1.0) + + /// A `FormatIndicatedCacheSerializer` which converts image from and to JPEG format with a settable compression + /// quality. If the image cannot be represented by JPEG format, it will fallback to its real format which is + /// determined by `original` data. + /// - Parameter compressionQuality: The compression quality when converting image to JPEG data. + public static func jpeg(compressionQuality: CGFloat) -> FormatIndicatedCacheSerializer { + return FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: compressionQuality) + } + + /// A `FormatIndicatedCacheSerializer` which converts image from and to GIF format. If the image cannot be + /// represented by GIF format, it will fallback to its real format which is determined by `original` data. + public static let gif = FormatIndicatedCacheSerializer(imageFormat: .GIF, jpegCompressionQuality: nil) + + /// The indicated image format. + private let imageFormat: ImageFormat + + /// The compression quality used for loss image format (like JPEG). + private let jpegCompressionQuality: CGFloat? + + /// Creates data which represents the given `image` under a format. + public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? { + + func imageData(withFormat imageFormat: ImageFormat) -> Data? { + return autoreleasepool { () -> Data? in + switch imageFormat { + case .PNG: return image.kf.pngRepresentation() + case .JPEG: return image.kf.jpegRepresentation(compressionQuality: jpegCompressionQuality ?? 1.0) + case .GIF: return image.kf.gifRepresentation() + case .unknown: return nil + } + } + } + + // generate data with indicated image format + if let data = imageData(withFormat: imageFormat) { + return data + } + + let originalFormat = original?.kf.imageFormat ?? .unknown + + // generate data with original image's format + if originalFormat != imageFormat, let data = imageData(withFormat: originalFormat) { + return data + } + + return original ?? image.kf.normalized.kf.pngRepresentation() + } + + /// Same implementation as `DefaultCacheSerializer`. + public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/ImageCache.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/ImageCache.swift new file mode 100644 index 0000000..b73100a --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/ImageCache.swift @@ -0,0 +1,875 @@ +// +// ImageCache.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +extension Notification.Name { + /// This notification will be sent when the disk cache got cleaned either there are cached files expired or the + /// total size exceeding the max allowed size. The manually invoking of `clearDiskCache` method will not trigger + /// this notification. + /// + /// The `object` of this notification is the `ImageCache` object which sends the notification. + /// A list of removed hashes (files) could be retrieved by accessing the array under + /// `KingfisherDiskCacheCleanedHashKey` key in `userInfo` of the notification object you received. + /// By checking the array, you could know the hash codes of files are removed. + public static let KingfisherDidCleanDiskCache = + Notification.Name("com.onevcat.Kingfisher.KingfisherDidCleanDiskCache") +} + +/// Key for array of cleaned hashes in `userInfo` of `KingfisherDidCleanDiskCacheNotification`. +public let KingfisherDiskCacheCleanedHashKey = "com.onevcat.Kingfisher.cleanedHash" + +/// Cache type of a cached image. +/// - none: The image is not cached yet when retrieving it. +/// - memory: The image is cached in memory. +/// - disk: The image is cached in disk. +public enum CacheType { + /// The image is not cached yet when retrieving it. + case none + /// The image is cached in memory. + case memory + /// The image is cached in disk. + case disk + + /// Whether the cache type represents the image is already cached or not. + public var cached: Bool { + switch self { + case .memory, .disk: return true + case .none: return false + } + } +} + +/// Represents the caching operation result. +public struct CacheStoreResult { + + /// The cache result for memory cache. Caching an image to memory will never fail. + public let memoryCacheResult: Result<(), Never> + + /// The cache result for disk cache. If an error happens during caching operation, + /// you can get it from `.failure` case of this `diskCacheResult`. + public let diskCacheResult: Result<(), KingfisherError> +} + +extension KFCrossPlatformImage: CacheCostCalculable { + /// Cost of an image + public var cacheCost: Int { return kf.cost } +} + +extension Data: DataTransformable { + public func toData() throws -> Data { + return self + } + + public static func fromData(_ data: Data) throws -> Data { + return data + } + + public static let empty = Data() +} + + +/// Represents the getting image operation from the cache. +/// +/// - disk: The image can be retrieved from disk cache. +/// - memory: The image can be retrieved memory cache. +/// - none: The image does not exist in the cache. +public enum ImageCacheResult { + + /// The image can be retrieved from disk cache. + case disk(KFCrossPlatformImage) + + /// The image can be retrieved memory cache. + case memory(KFCrossPlatformImage) + + /// The image does not exist in the cache. + case none + + /// Extracts the image from cache result. It returns the associated `Image` value for + /// `.disk` and `.memory` case. For `.none` case, `nil` is returned. + public var image: KFCrossPlatformImage? { + switch self { + case .disk(let image): return image + case .memory(let image): return image + case .none: return nil + } + } + + /// Returns the corresponding `CacheType` value based on the result type of `self`. + public var cacheType: CacheType { + switch self { + case .disk: return .disk + case .memory: return .memory + case .none: return .none + } + } +} + +/// Represents a hybrid caching system which is composed by a `MemoryStorage.Backend` and a `DiskStorage.Backend`. +/// `ImageCache` is a high level abstract for storing an image as well as its data to memory and disk, and +/// retrieving them back. +/// +/// While a default image cache object will be used if you prefer the extension methods of Kingfisher, you can create +/// your own cache object and configure its storages as your need. This class also provide an interface for you to set +/// the memory and disk storage config. +open class ImageCache { + + // MARK: Singleton + /// The default `ImageCache` object. Kingfisher will use this cache for its related methods if there is no + /// other cache specified. The `name` of this default cache is "default", and you should not use this name + /// for any of your customize cache. + public static let `default` = ImageCache(name: "default") + + + // MARK: Public Properties + /// The `MemoryStorage.Backend` object used in this cache. This storage holds loaded images in memory with a + /// reasonable expire duration and a maximum memory usage. To modify the configuration of a storage, just set + /// the storage `config` and its properties. + public let memoryStorage: MemoryStorage.Backend + + /// The `DiskStorage.Backend` object used in this cache. This storage stores loaded images in disk with a + /// reasonable expire duration and a maximum disk usage. To modify the configuration of a storage, just set + /// the storage `config` and its properties. + public let diskStorage: DiskStorage.Backend + + private let ioQueue: DispatchQueue + + /// Closure that defines the disk cache path from a given path and cacheName. + public typealias DiskCachePathClosure = (URL, String) -> URL + + // MARK: Initializers + + /// Creates an `ImageCache` from a customized `MemoryStorage` and `DiskStorage`. + /// + /// - Parameters: + /// - memoryStorage: The `MemoryStorage.Backend` object to use in the image cache. + /// - diskStorage: The `DiskStorage.Backend` object to use in the image cache. + public init( + memoryStorage: MemoryStorage.Backend, + diskStorage: DiskStorage.Backend) + { + self.memoryStorage = memoryStorage + self.diskStorage = diskStorage + let ioQueueName = "com.onevcat.Kingfisher.ImageCache.ioQueue.\(UUID().uuidString)" + ioQueue = DispatchQueue(label: ioQueueName) + + let notifications: [(Notification.Name, Selector)] + #if !os(macOS) && !os(watchOS) + notifications = [ + (UIApplication.didReceiveMemoryWarningNotification, #selector(clearMemoryCache)), + (UIApplication.willTerminateNotification, #selector(cleanExpiredDiskCache)), + (UIApplication.didEnterBackgroundNotification, #selector(backgroundCleanExpiredDiskCache)) + ] + #elseif os(macOS) + notifications = [ + (NSApplication.willResignActiveNotification, #selector(cleanExpiredDiskCache)), + ] + #else + notifications = [] + #endif + notifications.forEach { + NotificationCenter.default.addObserver(self, selector: $0.1, name: $0.0, object: nil) + } + } + + /// Creates an `ImageCache` with a given `name`. Both `MemoryStorage` and `DiskStorage` will be created + /// with a default config based on the `name`. + /// + /// - Parameter name: The name of cache object. It is used to setup disk cache directories and IO queue. + /// You should not use the same `name` for different caches, otherwise, the disk storage would + /// be conflicting to each other. The `name` should not be an empty string. + public convenience init(name: String) { + self.init(noThrowName: name, cacheDirectoryURL: nil, diskCachePathClosure: nil) + } + + /// Creates an `ImageCache` with a given `name`, cache directory `path` + /// and a closure to modify the cache directory. + /// + /// - Parameters: + /// - name: The name of cache object. It is used to setup disk cache directories and IO queue. + /// You should not use the same `name` for different caches, otherwise, the disk storage would + /// be conflicting to each other. + /// - cacheDirectoryURL: Location of cache directory URL on disk. It will be internally pass to the + /// initializer of `DiskStorage` as the disk cache directory. If `nil`, the cache + /// directory under user domain mask will be used. + /// - diskCachePathClosure: Closure that takes in an optional initial path string and generates + /// the final disk cache path. You could use it to fully customize your cache path. + /// - Throws: An error that happens during image cache creating, such as unable to create a directory at the given + /// path. + public convenience init( + name: String, + cacheDirectoryURL: URL?, + diskCachePathClosure: DiskCachePathClosure? = nil + ) throws + { + if name.isEmpty { + fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.") + } + + let memoryStorage = ImageCache.createMemoryStorage() + + let config = ImageCache.createConfig( + name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure + ) + let diskStorage = try DiskStorage.Backend(config: config) + self.init(memoryStorage: memoryStorage, diskStorage: diskStorage) + } + + convenience init( + noThrowName name: String, + cacheDirectoryURL: URL?, + diskCachePathClosure: DiskCachePathClosure? + ) + { + if name.isEmpty { + fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.") + } + + let memoryStorage = ImageCache.createMemoryStorage() + + let config = ImageCache.createConfig( + name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure + ) + let diskStorage = DiskStorage.Backend(noThrowConfig: config, creatingDirectory: true) + self.init(memoryStorage: memoryStorage, diskStorage: diskStorage) + } + + private static func createMemoryStorage() -> MemoryStorage.Backend { + let totalMemory = ProcessInfo.processInfo.physicalMemory + let costLimit = totalMemory / 4 + let memoryStorage = MemoryStorage.Backend(config: + .init(totalCostLimit: (costLimit > Int.max) ? Int.max : Int(costLimit))) + return memoryStorage + } + + private static func createConfig( + name: String, + cacheDirectoryURL: URL?, + diskCachePathClosure: DiskCachePathClosure? = nil + ) -> DiskStorage.Config + { + var diskConfig = DiskStorage.Config( + name: name, + sizeLimit: 0, + directory: cacheDirectoryURL + ) + if let closure = diskCachePathClosure { + diskConfig.cachePathBlock = closure + } + return diskConfig + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: Storing Images + + open func store(_ image: KFCrossPlatformImage, + original: Data? = nil, + forKey key: String, + options: KingfisherParsedOptionsInfo, + toDisk: Bool = true, + completionHandler: ((CacheStoreResult) -> Void)? = nil) + { + let identifier = options.processor.identifier + let callbackQueue = options.callbackQueue + + let computedKey = key.computedKey(with: identifier) + // Memory storage should not throw. + memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration) + + guard toDisk else { + if let completionHandler = completionHandler { + let result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(())) + callbackQueue.execute { completionHandler(result) } + } + return + } + + ioQueue.async { + let serializer = options.cacheSerializer + if let data = serializer.data(with: image, original: original) { + self.syncStoreToDisk( + data, + forKey: key, + processorIdentifier: identifier, + callbackQueue: callbackQueue, + expiration: options.diskCacheExpiration, + writeOptions: options.diskStoreWriteOptions, + completionHandler: completionHandler) + } else { + guard let completionHandler = completionHandler else { return } + + let diskError = KingfisherError.cacheError( + reason: .cannotSerializeImage(image: image, original: original, serializer: serializer)) + let result = CacheStoreResult( + memoryCacheResult: .success(()), + diskCacheResult: .failure(diskError)) + callbackQueue.execute { completionHandler(result) } + } + } + } + + /// Stores an image to the cache. + /// + /// - Parameters: + /// - image: The image to be stored. + /// - original: The original data of the image. This value will be forwarded to the provided `serializer` for + /// further use. By default, Kingfisher uses a `DefaultCacheSerializer` to serialize the image to + /// data for caching in disk, it checks the image format based on `original` data to determine in + /// which image format should be used. For other types of `serializer`, it depends on their + /// implementation detail on how to use this original data. + /// - key: The key used for caching the image. + /// - identifier: The identifier of processor being used for caching. If you are using a processor for the + /// image, pass the identifier of processor to this parameter. + /// - serializer: The `CacheSerializer` + /// - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory. + /// Otherwise, it is cached in both memory storage and disk storage. Default is `true`. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`. For case + /// that `toDisk` is `false`, a `.untouch` queue means `callbackQueue` will be invoked from the + /// caller queue of this method. If `toDisk` is `true`, the `completionHandler` will be called + /// from an internal file IO queue. To change this behavior, specify another `CallbackQueue` + /// value. + /// - completionHandler: A closure which is invoked when the cache operation finishes. + open func store(_ image: KFCrossPlatformImage, + original: Data? = nil, + forKey key: String, + processorIdentifier identifier: String = "", + cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default, + toDisk: Bool = true, + callbackQueue: CallbackQueue = .untouch, + completionHandler: ((CacheStoreResult) -> Void)? = nil) + { + struct TempProcessor: ImageProcessor { + let identifier: String + func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return nil + } + } + + let options = KingfisherParsedOptionsInfo([ + .processor(TempProcessor(identifier: identifier)), + .cacheSerializer(serializer), + .callbackQueue(callbackQueue) + ]) + store(image, original: original, forKey: key, options: options, + toDisk: toDisk, completionHandler: completionHandler) + } + + open func storeToDisk( + _ data: Data, + forKey key: String, + processorIdentifier identifier: String = "", + expiration: StorageExpiration? = nil, + callbackQueue: CallbackQueue = .untouch, + completionHandler: ((CacheStoreResult) -> Void)? = nil) + { + ioQueue.async { + self.syncStoreToDisk( + data, + forKey: key, + processorIdentifier: identifier, + callbackQueue: callbackQueue, + expiration: expiration, + completionHandler: completionHandler) + } + } + + private func syncStoreToDisk( + _ data: Data, + forKey key: String, + processorIdentifier identifier: String = "", + callbackQueue: CallbackQueue = .untouch, + expiration: StorageExpiration? = nil, + writeOptions: Data.WritingOptions = [], + completionHandler: ((CacheStoreResult) -> Void)? = nil) + { + let computedKey = key.computedKey(with: identifier) + let result: CacheStoreResult + do { + try self.diskStorage.store(value: data, forKey: computedKey, expiration: expiration, writeOptions: writeOptions) + result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(())) + } catch { + let diskError: KingfisherError + if let error = error as? KingfisherError { + diskError = error + } else { + diskError = .cacheError(reason: .cannotConvertToData(object: data, error: error)) + } + + result = CacheStoreResult( + memoryCacheResult: .success(()), + diskCacheResult: .failure(diskError) + ) + } + if let completionHandler = completionHandler { + callbackQueue.execute { completionHandler(result) } + } + } + + // MARK: Removing Images + + /// Removes the image for the given key from the cache. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: The identifier of processor being used for caching. If you are using a processor for the + /// image, pass the identifier of processor to this parameter. + /// - fromMemory: Whether this image should be removed from memory storage or not. + /// If `false`, the image won't be removed from the memory storage. Default is `true`. + /// - fromDisk: Whether this image should be removed from disk storage or not. + /// If `false`, the image won't be removed from the disk storage. Default is `true`. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`. + /// - completionHandler: A closure which is invoked when the cache removing operation finishes. + open func removeImage(forKey key: String, + processorIdentifier identifier: String = "", + fromMemory: Bool = true, + fromDisk: Bool = true, + callbackQueue: CallbackQueue = .untouch, + completionHandler: (() -> Void)? = nil) + { + let computedKey = key.computedKey(with: identifier) + + if fromMemory { + memoryStorage.remove(forKey: computedKey) + } + + if fromDisk { + ioQueue.async{ + try? self.diskStorage.remove(forKey: computedKey) + if let completionHandler = completionHandler { + callbackQueue.execute { completionHandler() } + } + } + } else { + if let completionHandler = completionHandler { + callbackQueue.execute { completionHandler() } + } + } + } + + // MARK: Getting Images + + /// Gets an image for a given key from the cache, either from memory storage or disk storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The `KingfisherParsedOptionsInfo` options setting used for retrieving the image. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.mainCurrentOrAsync`. + /// - completionHandler: A closure which is invoked when the image getting operation finishes. If the + /// image retrieving operation finishes without problem, an `ImageCacheResult` value + /// will be sent to this closure as result. Otherwise, a `KingfisherError` result + /// with detail failing reason will be sent. + open func retrieveImage( + forKey key: String, + options: KingfisherParsedOptionsInfo, + callbackQueue: CallbackQueue = .mainCurrentOrAsync, + completionHandler: ((Result) -> Void)?) + { + // No completion handler. No need to start working and early return. + guard let completionHandler = completionHandler else { return } + + // Try to check the image from memory cache first. + if let image = retrieveImageInMemoryCache(forKey: key, options: options) { + callbackQueue.execute { completionHandler(.success(.memory(image))) } + } else if options.fromMemoryCacheOrRefresh { + callbackQueue.execute { completionHandler(.success(.none)) } + } else { + + // Begin to disk search. + self.retrieveImageInDiskCache(forKey: key, options: options, callbackQueue: callbackQueue) { + result in + switch result { + case .success(let image): + + guard let image = image else { + // No image found in disk storage. + callbackQueue.execute { completionHandler(.success(.none)) } + return + } + + // Cache the disk image to memory. + // We are passing `false` to `toDisk`, the memory cache does not change + // callback queue, we can call `completionHandler` without another dispatch. + var cacheOptions = options + cacheOptions.callbackQueue = .untouch + self.store( + image, + forKey: key, + options: cacheOptions, + toDisk: false) + { + _ in + callbackQueue.execute { completionHandler(.success(.disk(image))) } + } + case .failure(let error): + callbackQueue.execute { completionHandler(.failure(error)) } + } + } + } + } + + /// Gets an image for a given key from the cache, either from memory storage or disk storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.mainCurrentOrAsync`. + /// - completionHandler: A closure which is invoked when the image getting operation finishes. If the + /// image retrieving operation finishes without problem, an `ImageCacheResult` value + /// will be sent to this closure as result. Otherwise, a `KingfisherError` result + /// with detail failing reason will be sent. + /// + /// Note: This method is marked as `open` for only compatible purpose. Do not overide this method. Instead, override + /// the version receives `KingfisherParsedOptionsInfo` instead. + open func retrieveImage(forKey key: String, + options: KingfisherOptionsInfo? = nil, + callbackQueue: CallbackQueue = .mainCurrentOrAsync, + completionHandler: ((Result) -> Void)?) + { + retrieveImage( + forKey: key, + options: KingfisherParsedOptionsInfo(options), + callbackQueue: callbackQueue, + completionHandler: completionHandler) + } + + /// Gets an image for a given key from the memory storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The `KingfisherParsedOptionsInfo` options setting used for retrieving the image. + /// - Returns: The image stored in memory cache, if exists and valid. Otherwise, if the image does not exist or + /// has already expired, `nil` is returned. + open func retrieveImageInMemoryCache( + forKey key: String, + options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? + { + let computedKey = key.computedKey(with: options.processor.identifier) + return memoryStorage.value(forKey: computedKey, extendingExpiration: options.memoryCacheAccessExtendingExpiration) + } + + /// Gets an image for a given key from the memory storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image. + /// - Returns: The image stored in memory cache, if exists and valid. Otherwise, if the image does not exist or + /// has already expired, `nil` is returned. + /// + /// Note: This method is marked as `open` for only compatible purpose. Do not overide this method. Instead, override + /// the version receives `KingfisherParsedOptionsInfo` instead. + open func retrieveImageInMemoryCache( + forKey key: String, + options: KingfisherOptionsInfo? = nil) -> KFCrossPlatformImage? + { + return retrieveImageInMemoryCache(forKey: key, options: KingfisherParsedOptionsInfo(options)) + } + + func retrieveImageInDiskCache( + forKey key: String, + options: KingfisherParsedOptionsInfo, + callbackQueue: CallbackQueue = .untouch, + completionHandler: @escaping (Result) -> Void) + { + let computedKey = key.computedKey(with: options.processor.identifier) + let loadingQueue: CallbackQueue = options.loadDiskFileSynchronously ? .untouch : .dispatch(ioQueue) + loadingQueue.execute { + do { + var image: KFCrossPlatformImage? = nil + if let data = try self.diskStorage.value(forKey: computedKey, extendingExpiration: options.diskCacheAccessExtendingExpiration) { + image = options.cacheSerializer.image(with: data, options: options) + } + callbackQueue.execute { completionHandler(.success(image)) } + } catch { + if let error = error as? KingfisherError { + callbackQueue.execute { completionHandler(.failure(error)) } + } else { + assertionFailure("The internal thrown error should be a `KingfisherError`.") + } + } + } + } + + /// Gets an image for a given key from the disk storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`. + /// - completionHandler: A closure which is invoked when the operation finishes. + open func retrieveImageInDiskCache( + forKey key: String, + options: KingfisherOptionsInfo? = nil, + callbackQueue: CallbackQueue = .untouch, + completionHandler: @escaping (Result) -> Void) + { + retrieveImageInDiskCache( + forKey: key, + options: KingfisherParsedOptionsInfo(options), + callbackQueue: callbackQueue, + completionHandler: completionHandler) + } + + // MARK: Cleaning + /// Clears the memory & disk storage of this cache. This is an async operation. + /// + /// - Parameter handler: A closure which is invoked when the cache clearing operation finishes. + /// This `handler` will be called from the main queue. + public func clearCache(completion handler: (() -> Void)? = nil) { + clearMemoryCache() + clearDiskCache(completion: handler) + } + + /// Clears the memory storage of this cache. + @objc public func clearMemoryCache() { + memoryStorage.removeAll() + } + + /// Clears the disk storage of this cache. This is an async operation. + /// + /// - Parameter handler: A closure which is invoked when the cache clearing operation finishes. + /// This `handler` will be called from the main queue. + open func clearDiskCache(completion handler: (() -> Void)? = nil) { + ioQueue.async { + do { + try self.diskStorage.removeAll() + } catch _ { } + if let handler = handler { + DispatchQueue.main.async { handler() } + } + } + } + + /// Clears the expired images from memory & disk storage. This is an async operation. + open func cleanExpiredCache(completion handler: (() -> Void)? = nil) { + cleanExpiredMemoryCache() + cleanExpiredDiskCache(completion: handler) + } + + /// Clears the expired images from disk storage. + open func cleanExpiredMemoryCache() { + memoryStorage.removeExpired() + } + + /// Clears the expired images from disk storage. This is an async operation. + @objc func cleanExpiredDiskCache() { + cleanExpiredDiskCache(completion: nil) + } + + /// Clears the expired images from disk storage. This is an async operation. + /// + /// - Parameter handler: A closure which is invoked when the cache clearing operation finishes. + /// This `handler` will be called from the main queue. + open func cleanExpiredDiskCache(completion handler: (() -> Void)? = nil) { + ioQueue.async { + do { + var removed: [URL] = [] + let removedExpired = try self.diskStorage.removeExpiredValues() + removed.append(contentsOf: removedExpired) + + let removedSizeExceeded = try self.diskStorage.removeSizeExceededValues() + removed.append(contentsOf: removedSizeExceeded) + + if !removed.isEmpty { + DispatchQueue.main.async { + let cleanedHashes = removed.map { $0.lastPathComponent } + NotificationCenter.default.post( + name: .KingfisherDidCleanDiskCache, + object: self, + userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes]) + } + } + + if let handler = handler { + DispatchQueue.main.async { handler() } + } + } catch {} + } + } + +#if !os(macOS) && !os(watchOS) + /// Clears the expired images from disk storage when app is in background. This is an async operation. + /// In most cases, you should not call this method explicitly. + /// It will be called automatically when `UIApplicationDidEnterBackgroundNotification` received. + @objc public func backgroundCleanExpiredDiskCache() { + // if 'sharedApplication()' is unavailable, then return + guard let sharedApplication = KingfisherWrapper.shared else { return } + + func endBackgroundTask(_ task: inout UIBackgroundTaskIdentifier) { + sharedApplication.endBackgroundTask(task) + task = UIBackgroundTaskIdentifier.invalid + } + + var backgroundTask: UIBackgroundTaskIdentifier! + backgroundTask = sharedApplication.beginBackgroundTask { + endBackgroundTask(&backgroundTask!) + } + + cleanExpiredDiskCache { + endBackgroundTask(&backgroundTask!) + } + } +#endif + + // MARK: Image Cache State + + /// Returns the cache type for a given `key` and `identifier` combination. + /// This method is used for checking whether an image is cached in current cache. + /// It also provides information on which kind of cache can it be found in the return value. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: Processor identifier which used for this image. Default is the `identifier` of + /// `DefaultImageProcessor.default`. + /// - Returns: A `CacheType` instance which indicates the cache status. + /// `.none` means the image is not in cache or it is already expired. + open func imageCachedType( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> CacheType + { + let computedKey = key.computedKey(with: identifier) + if memoryStorage.isCached(forKey: computedKey) { return .memory } + if diskStorage.isCached(forKey: computedKey) { return .disk } + return .none + } + + /// Returns whether the file exists in cache for a given `key` and `identifier` combination. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: Processor identifier which used for this image. Default is the `identifier` of + /// `DefaultImageProcessor.default`. + /// - Returns: A `Bool` which indicates whether a cache could match the given `key` and `identifier` combination. + /// + /// - Note: + /// The return value does not contain information about from which kind of storage the cache matches. + /// To get the information about cache type according `CacheType`, + /// use `imageCachedType(forKey:processorIdentifier:)` instead. + public func isCached( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> Bool + { + return imageCachedType(forKey: key, processorIdentifier: identifier).cached + } + + /// Gets the hash used as cache file name for the key. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: Processor identifier which used for this image. Default is the `identifier` of + /// `DefaultImageProcessor.default`. + /// - Returns: The hash which is used as the cache file name. + /// + /// - Note: + /// By default, for a given combination of `key` and `identifier`, `ImageCache` will use the value + /// returned by this method as the cache file name. You can use this value to check and match cache file + /// if you need. + open func hash( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> String + { + let computedKey = key.computedKey(with: identifier) + return diskStorage.cacheFileName(forKey: computedKey) + } + + /// Calculates the size taken by the disk storage. + /// It is the total file size of all cached files in the `diskStorage` on disk in bytes. + /// + /// - Parameter handler: Called with the size calculating finishes. This closure is invoked from the main queue. + open func calculateDiskStorageSize(completion handler: @escaping ((Result) -> Void)) { + ioQueue.async { + do { + let size = try self.diskStorage.totalSize() + DispatchQueue.main.async { handler(.success(size)) } + } catch { + if let error = error as? KingfisherError { + DispatchQueue.main.async { handler(.failure(error)) } + } else { + assertionFailure("The internal thrown error should be a `KingfisherError`.") + } + + } + } + } + + /// Gets the cache path for the key. + /// It is useful for projects with web view or anyone that needs access to the local file path. + /// + /// i.e. Replacing the `` tag in your HTML. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: Processor identifier which used for this image. Default is the `identifier` of + /// `DefaultImageProcessor.default`. + /// - Returns: The disk path of cached image under the given `key` and `identifier`. + /// + /// - Note: + /// This method does not guarantee there is an image already cached in the returned path. It just gives your + /// the path that the image should be, if it exists in disk storage. + /// + /// You could use `isCached(forKey:)` method to check whether the image is cached under that key in disk. + open func cachePath( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> String + { + let computedKey = key.computedKey(with: identifier) + return diskStorage.cacheFileURL(forKey: computedKey).path + } +} + +extension Dictionary { + func keysSortedByValue(_ isOrderedBefore: (Value, Value) -> Bool) -> [Key] { + return Array(self).sorted{ isOrderedBefore($0.1, $1.1) }.map{ $0.0 } + } +} + +#if !os(macOS) && !os(watchOS) +// MARK: - For App Extensions +extension UIApplication: KingfisherCompatible { } +extension KingfisherWrapper where Base: UIApplication { + public static var shared: UIApplication? { + let selector = NSSelectorFromString("sharedApplication") + guard Base.responds(to: selector) else { return nil } + return Base.perform(selector).takeUnretainedValue() as? UIApplication + } +} +#endif + +extension String { + func computedKey(with identifier: String) -> String { + if identifier.isEmpty { + return self + } else { + return appending("@\(identifier)") + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift new file mode 100644 index 0000000..b8b474e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift @@ -0,0 +1,285 @@ +// +// MemoryStorage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a set of conception related to storage which stores a certain type of value in memory. +/// This is a namespace for the memory storage types. A `Backend` with a certain `Config` will be used to describe the +/// storage. See these composed types for more information. +public enum MemoryStorage { + + /// Represents a storage which stores a certain type of value in memory. It provides fast access, + /// but limited storing size. The stored value type needs to conform to `CacheCostCalculable`, + /// and its `cacheCost` will be used to determine the cost of size for the cache item. + /// + /// You can config a `MemoryStorage.Backend` in its initializer by passing a `MemoryStorage.Config` value. + /// or modifying the `config` property after it being created. The backend of `MemoryStorage` has + /// upper limitation on cost size in memory and item count. All items in the storage has an expiration + /// date. When retrieved, if the target item is already expired, it will be recognized as it does not + /// exist in the storage. The `MemoryStorage` also contains a scheduled self clean task, to evict expired + /// items from memory. + public class Backend { + let storage = NSCache>() + + // Keys trackes the objects once inside the storage. For object removing triggered by user, the corresponding + // key would be also removed. However, for the object removing triggered by cache rule/policy of system, the + // key will be remained there until next `removeExpired` happens. + // + // Breaking the strict tracking could save additional locking behaviors. + // See https://github.com/onevcat/Kingfisher/issues/1233 + var keys = Set() + + private var cleanTimer: Timer? = nil + private let lock = NSLock() + + /// The config used in this storage. It is a value you can set and + /// use to config the storage in air. + public var config: Config { + didSet { + storage.totalCostLimit = config.totalCostLimit + storage.countLimit = config.countLimit + } + } + + /// Creates a `MemoryStorage` with a given `config`. + /// + /// - Parameter config: The config used to create the storage. It determines the max size limitation, + /// default expiration setting and more. + public init(config: Config) { + self.config = config + storage.totalCostLimit = config.totalCostLimit + storage.countLimit = config.countLimit + + cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in + guard let self = self else { return } + self.removeExpired() + } + } + + /// Removes the expired values from the storage. + public func removeExpired() { + lock.lock() + defer { lock.unlock() } + for key in keys { + let nsKey = key as NSString + guard let object = storage.object(forKey: nsKey) else { + // This could happen if the object is moved by cache `totalCostLimit` or `countLimit` rule. + // We didn't remove the key yet until now, since we do not want to introduce additional lock. + // See https://github.com/onevcat/Kingfisher/issues/1233 + keys.remove(key) + continue + } + if object.estimatedExpiration.isPast { + storage.removeObject(forKey: nsKey) + keys.remove(key) + } + } + } + + /// Stores a value to the storage under the specified key and expiration policy. + /// - Parameters: + /// - value: The value to be stored. + /// - key: The key to which the `value` will be stored. + /// - expiration: The expiration policy used by this store action. + /// - Throws: No error will + public func store( + value: T, + forKey key: String, + expiration: StorageExpiration? = nil) + { + storeNoThrow(value: value, forKey: key, expiration: expiration) + } + + // The no throw version for storing value in cache. Kingfisher knows the detail so it + // could use this version to make syntax simpler internally. + func storeNoThrow( + value: T, + forKey key: String, + expiration: StorageExpiration? = nil) + { + lock.lock() + defer { lock.unlock() } + let expiration = expiration ?? config.expiration + // The expiration indicates that already expired, no need to store. + guard !expiration.isExpired else { return } + + let object: StorageObject + if config.keepWhenEnteringBackground { + object = BackgroundKeepingStorageObject(value, key: key, expiration: expiration) + } else { + object = StorageObject(value, key: key, expiration: expiration) + } + storage.setObject(object, forKey: key as NSString, cost: value.cacheCost) + keys.insert(key) + } + + /// Gets a value from the storage. + /// + /// - Parameters: + /// - key: The cache key of value. + /// - extendingExpiration: The expiration policy used by this getting action. + /// - Returns: The value under `key` if it is valid and found in the storage. Otherwise, `nil`. + public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? { + guard let object = storage.object(forKey: key as NSString) else { + return nil + } + if object.expired { + return nil + } + object.extendExpiration(extendingExpiration) + return object.value + } + + /// Whether there is valid cached data under a given key. + /// - Parameter key: The cache key of value. + /// - Returns: If there is valid data under the key, `true`. Otherwise, `false`. + public func isCached(forKey key: String) -> Bool { + guard let _ = value(forKey: key, extendingExpiration: .none) else { + return false + } + return true + } + + /// Removes a value from a specified key. + /// - Parameter key: The cache key of value. + public func remove(forKey key: String) { + lock.lock() + defer { lock.unlock() } + storage.removeObject(forKey: key as NSString) + keys.remove(key) + } + + /// Removes all values in this storage. + public func removeAll() { + lock.lock() + defer { lock.unlock() } + storage.removeAllObjects() + keys.removeAll() + } + } +} + +extension MemoryStorage { + /// Represents the config used in a `MemoryStorage`. + public struct Config { + + /// Total cost limit of the storage in bytes. + public var totalCostLimit: Int + + /// The item count limit of the memory storage. + public var countLimit: Int = .max + + /// The `StorageExpiration` used in this memory storage. Default is `.seconds(300)`, + /// means that the memory cache would expire in 5 minutes. + public var expiration: StorageExpiration = .seconds(300) + + /// The time interval between the storage do clean work for swiping expired items. + public var cleanInterval: TimeInterval + + /// Whether the newly added items to memory cache should be purged when the app goes to background. + /// + /// By default, the cached items in memory will be purged as soon as the app goes to background to ensure + /// least memory footprint. Enabling this would prevent this behavior and keep the items alive in cache even + /// when your app is not in foreground anymore. + /// + /// Default is `false`. After setting `true`, only the newly added cache objects are affected. Existing + /// objects which are already in the cache while this value was `false` will be still be purged when entering + /// background. + public var keepWhenEnteringBackground: Bool = false + + /// Creates a config from a given `totalCostLimit` value. + /// + /// - Parameters: + /// - totalCostLimit: Total cost limit of the storage in bytes. + /// - cleanInterval: The time interval between the storage do clean work for swiping expired items. + /// Default is 120, means the auto eviction happens once per two minutes. + /// + /// - Note: + /// Other members of `MemoryStorage.Config` will use their default values when created. + public init(totalCostLimit: Int, cleanInterval: TimeInterval = 120) { + self.totalCostLimit = totalCostLimit + self.cleanInterval = cleanInterval + } + } +} + +extension MemoryStorage { + + class BackgroundKeepingStorageObject: StorageObject, NSDiscardableContent { + var accessing = true + func beginContentAccess() -> Bool { + if value != nil { + accessing = true + } else { + accessing = false + } + return accessing + } + + func endContentAccess() { + accessing = false + } + + func discardContentIfPossible() { + value = nil + } + + func isContentDiscarded() -> Bool { + return value == nil + } + } + + class StorageObject { + var value: T? + let expiration: StorageExpiration + let key: String + + private(set) var estimatedExpiration: Date + + init(_ value: T, key: String, expiration: StorageExpiration) { + self.value = value + self.key = key + self.expiration = expiration + + self.estimatedExpiration = expiration.estimatedExpirationSinceNow + } + + func extendExpiration(_ extendingExpiration: ExpirationExtending = .cacheTime) { + switch extendingExpiration { + case .none: + return + case .cacheTime: + self.estimatedExpiration = expiration.estimatedExpirationSinceNow + case .expirationTime(let expirationTime): + self.estimatedExpiration = expirationTime.estimatedExpirationSinceNow + } + } + + var expired: Bool { + return estimatedExpiration.isPast + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/Storage.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/Storage.swift new file mode 100644 index 0000000..ca0f8bb --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/Storage.swift @@ -0,0 +1,113 @@ +// +// Storage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Constants for some time intervals +struct TimeConstants { + static let secondsInOneMinute = 60 + static let minutesInOneHour = 60 + static let hoursInOneDay = 24 + static let secondsInOneDay = 86_400 +} + +/// Represents the expiration strategy used in storage. +/// +/// - never: The item never expires. +/// - seconds: The item expires after a time duration of given seconds from now. +/// - days: The item expires after a time duration of given days from now. +/// - date: The item expires after a given date. +public enum StorageExpiration { + /// The item never expires. + case never + /// The item expires after a time duration of given seconds from now. + case seconds(TimeInterval) + /// The item expires after a time duration of given days from now. + case days(Int) + /// The item expires after a given date. + case date(Date) + /// Indicates the item is already expired. Use this to skip cache. + case expired + + func estimatedExpirationSince(_ date: Date) -> Date { + switch self { + case .never: return .distantFuture + case .seconds(let seconds): + return date.addingTimeInterval(seconds) + case .days(let days): + let duration: TimeInterval = TimeInterval(TimeConstants.secondsInOneDay) * TimeInterval(days) + return date.addingTimeInterval(duration) + case .date(let ref): + return ref + case .expired: + return .distantPast + } + } + + var estimatedExpirationSinceNow: Date { + return estimatedExpirationSince(Date()) + } + + var isExpired: Bool { + return timeInterval <= 0 + } + + var timeInterval: TimeInterval { + switch self { + case .never: return .infinity + case .seconds(let seconds): return seconds + case .days(let days): return TimeInterval(TimeConstants.secondsInOneDay) * TimeInterval(days) + case .date(let ref): return ref.timeIntervalSinceNow + case .expired: return -(.infinity) + } + } +} + +/// Represents the expiration extending strategy used in storage to after access. +/// +/// - none: The item expires after the original time, without extending after access. +/// - cacheTime: The item expiration extends by the original cache time after each access. +/// - expirationTime: The item expiration extends by the provided time after each access. +public enum ExpirationExtending { + /// The item expires after the original time, without extending after access. + case none + /// The item expiration extends by the original cache time after each access. + case cacheTime + /// The item expiration extends by the provided time after each access. + case expirationTime(_ expiration: StorageExpiration) +} + +/// Represents types which cost in memory can be calculated. +public protocol CacheCostCalculable { + var cacheCost: Int { get } +} + +/// Represents types which can be converted to and from data. +public protocol DataTransformable { + func toData() throws -> Data + static func fromData(_ data: Data) throws -> Self + static var empty: Self { get } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift new file mode 100644 index 0000000..d49c2f6 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift @@ -0,0 +1,258 @@ + +// +// CPListItem+Kingfisher.swift +// Kingfisher +// +// Created by Wayne Hartman on 2021-08-29. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +import CarPlay + +@available(iOS 14.0, *) +extension KingfisherWrapper where Base: CPListItem { + + // MARK: Setting Image + + /// Sets an image to the image view with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? [])) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + /** + * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 + * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, + * which allows >=14.5 SDK to set a `nil` image. This compile check allows + * newer SDK users to set the image to `nil`, while still allowing older SDK + * users to compile the framework. + */ + #if compiler(>=5.4) + self.base.setImage(placeholder) + #else + if let placeholder = placeholder { + self.base.setImage(placeholder) + } + #endif + + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + /** + * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 + * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, + * which allows >=14.5 SDK to set a `nil` image. This compile check allows + * newer SDK users to set the image to `nil`, while still allowing older SDK + * users to compile the framework. + */ + #if compiler(>=5.4) + self.base.setImage(placeholder) + #else // Let older SDK users deal with the older behavior. + if let placeholder = placeholder { + self.base.setImage(placeholder) + } + #endif + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.setImage(image) + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + self.base.setImage(value.image) + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + /** + * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 + * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, + * which allows >=14.5 SDK to set a `nil` image. This compile check allows + * newer SDK users to set the image to `nil`, while still allowing older SDK + * users to compile the framework. + */ + #if compiler(>=5.4) + self.base.setImage(image) + #else // Let older SDK users deal with the older behavior. + if let unwrapped = image { + self.base.setImage(unwrapped) + } + #endif + + } else { + #if compiler(>=5.4) + self.base.setImage(nil) + #endif + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Image + + /// Cancel the image download task bounded to the image view if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} + +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +extension KingfisherWrapper where Base: CPListItem { + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift new file mode 100644 index 0000000..6ead7d5 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift @@ -0,0 +1,545 @@ +// +// ImageView+Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if !os(watchOS) + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +extension KingfisherWrapper where Base: KFCrossPlatformImageView { + + // MARK: Setting Image + + /// Sets an image to the image view with a `Source`. + /// + /// - Parameters: + /// - source: The `Source` object defines data information from network or a data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ``` + /// // Set image from a network source. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: .network(url)) + /// + /// // Or set image from a data provider. + /// let provider = LocalFileImageDataProvider(fileURL: fileURL) + /// imageView.kf.setImage(with: .provider(provider)) + /// ``` + /// + /// For both `.network` and `.provider` source, there are corresponding view extension methods. So the code + /// above is equivalent to: + /// + /// ``` + /// imageView.kf.setImage(with: url) + /// imageView.kf.setImage(with: provider) + /// ``` + /// + /// Internally, this method will use `KingfisherManager` to get the source. + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage(with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler) + } + + /// Sets an image to the image view with a `Source`. + /// + /// - Parameters: + /// - source: The `Source` object defines data information from network or a data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ``` + /// // Set image from a network source. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: .network(url)) + /// + /// // Or set image from a data provider. + /// let provider = LocalFileImageDataProvider(fileURL: fileURL) + /// imageView.kf.setImage(with: .provider(provider)) + /// ``` + /// + /// For both `.network` and `.provider` source, there are corresponding view extension methods. So the code + /// above is equivalent to: + /// + /// ``` + /// imageView.kf.setImage(with: url) + /// imageView.kf.setImage(with: provider) + /// ``` + /// + /// Internally, this method will use `KingfisherManager` to get the source. + /// Since this method will perform UI changes, you must call it from the main thread. + /// The `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: source, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// This is the easiest way to use Kingfisher to boost the image setting process from network. Since all parameters + /// have a default value except the `resource`, you can set an image from a certain URL to an image view like this: + /// + /// ``` + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: url) + /// ``` + /// + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + /// Sets an image to the image view with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// This is the easiest way to use Kingfisher to boost the image setting process from network. Since all parameters + /// have a default value except the `resource`, you can set an image from a certain URL to an image view like this: + /// + /// ``` + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: url) + /// ``` + /// + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// The `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a data provider. + /// + /// - Parameters: + /// - provider: The `ImageDataProvider` object contains information about the data. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// Internally, this method will use `KingfisherManager` to get the image data, from either cache + /// or the data provider. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with provider: ImageDataProvider?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: provider.map { .provider($0) }, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + /// Sets an image to the image view with a data provider. + /// + /// - Parameters: + /// - provider: The `ImageDataProvider` object contains information about the data. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// Internally, this method will use `KingfisherManager` to get the image data, from either cache + /// or the data provider. Since this method will perform UI changes, you must call it from the main thread. + /// The `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with provider: ImageDataProvider?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: provider, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + + func setImage( + with source: Source?, + placeholder: Placeholder? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + mutatingSelf.placeholder = placeholder + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + + let isEmptyImage = base.image == nil && self.placeholder == nil + if !options.keepCurrentImageWhileLoading || isEmptyImage { + // Always set placeholder while there is no image/placeholder yet. + mutatingSelf.placeholder = placeholder + } + + let maybeIndicator = indicator + maybeIndicator?.startAnimatingView() + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if base.shouldPreloadAllAnimation() { + options.preloadAllAnimationData = true + } + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.image = image + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + maybeIndicator?.stopAnimatingView() + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + guard self.needsTransition(options: options, cacheType: value.cacheType) else { + mutatingSelf.placeholder = nil + self.base.image = value.image + completionHandler?(result) + return + } + + self.makeTransition(image: value.image, transition: options.transition) { + completionHandler?(result) + } + + case .failure: + if let image = options.onFailureImage { + mutatingSelf.placeholder = nil + self.base.image = image + } + completionHandler?(result) + } + } + } + ) + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Downloading Task + + /// Cancels the image download task of the image view if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } + + private func needsTransition(options: KingfisherParsedOptionsInfo, cacheType: CacheType) -> Bool { + switch options.transition { + case .none: + return false + #if os(macOS) + case .fade: // Fade is only a placeholder for SwiftUI on macOS. + return false + #else + default: + if options.forceTransition { return true } + if cacheType == .none { return true } + return false + #endif + } + } + + private func makeTransition(image: KFCrossPlatformImage, transition: ImageTransition, done: @escaping () -> Void) { + #if !os(macOS) + // Force hiding the indicator without transition first. + UIView.transition( + with: self.base, + duration: 0.0, + options: [], + animations: { self.indicator?.stopAnimatingView() }, + completion: { _ in + var mutatingSelf = self + mutatingSelf.placeholder = nil + UIView.transition( + with: self.base, + duration: transition.duration, + options: [transition.animationOptions, .allowUserInteraction], + animations: { transition.animations?(self.base, image) }, + completion: { finished in + transition.completion?(finished) + done() + } + ) + } + ) + #else + done() + #endif + } +} + +// MARK: - Associated Object +private var taskIdentifierKey: Void? +private var indicatorKey: Void? +private var indicatorTypeKey: Void? +private var placeholderKey: Void? +private var imageTaskKey: Void? + +extension KingfisherWrapper where Base: KFCrossPlatformImageView { + + // MARK: Properties + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + /// Holds which indicator type is going to be used. + /// Default is `.none`, means no indicator will be shown while downloading. + public var indicatorType: IndicatorType { + get { + return getAssociatedObject(base, &indicatorTypeKey) ?? .none + } + + set { + switch newValue { + case .none: indicator = nil + case .activity: indicator = ActivityIndicator() + case .image(let data): indicator = ImageIndicator(imageData: data) + case .custom(let anIndicator): indicator = anIndicator + } + + setRetainedAssociatedObject(base, &indicatorTypeKey, newValue) + } + } + + /// Holds any type that conforms to the protocol `Indicator`. + /// The protocol `Indicator` has a `view` property that will be shown when loading an image. + /// It will be `nil` if `indicatorType` is `.none`. + public private(set) var indicator: Indicator? { + get { + let box: Box? = getAssociatedObject(base, &indicatorKey) + return box?.value + } + + set { + // Remove previous + if let previousIndicator = indicator { + previousIndicator.view.removeFromSuperview() + } + + // Add new + if let newIndicator = newValue { + // Set default indicator layout + let view = newIndicator.view + + base.addSubview(view) + view.translatesAutoresizingMaskIntoConstraints = false + view.centerXAnchor.constraint( + equalTo: base.centerXAnchor, constant: newIndicator.centerOffset.x).isActive = true + view.centerYAnchor.constraint( + equalTo: base.centerYAnchor, constant: newIndicator.centerOffset.y).isActive = true + + switch newIndicator.sizeStrategy(in: base) { + case .intrinsicSize: + break + case .full: + view.heightAnchor.constraint(equalTo: base.heightAnchor, constant: 0).isActive = true + view.widthAnchor.constraint(equalTo: base.widthAnchor, constant: 0).isActive = true + case .size(let size): + view.heightAnchor.constraint(equalToConstant: size.height).isActive = true + view.widthAnchor.constraint(equalToConstant: size.width).isActive = true + } + + newIndicator.view.isHidden = true + } + + // Save in associated object + // Wrap newValue with Box to workaround an issue that Swift does not recognize + // and casting protocol for associate object correctly. https://github.com/onevcat/Kingfisher/issues/872 + setRetainedAssociatedObject(base, &indicatorKey, newValue.map(Box.init)) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } + + /// Represents the `Placeholder` used for this image view. A `Placeholder` will be shown in the view while + /// it is downloading an image. + public private(set) var placeholder: Placeholder? { + get { return getAssociatedObject(base, &placeholderKey) } + set { + if let previousPlaceholder = placeholder { + previousPlaceholder.remove(from: base) + } + + if let newPlaceholder = newValue { + newPlaceholder.add(to: base) + } else { + base.image = nil + } + setRetainedAssociatedObject(base, &placeholderKey, newValue) + } + } +} + + +extension KFCrossPlatformImageView { + @objc func shouldPreloadAllAnimation() -> Bool { return true } +} + +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift new file mode 100644 index 0000000..566eb1e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift @@ -0,0 +1,370 @@ +// +// NSButton+Kingfisher.swift +// Kingfisher +// +// Created by Jie Zhang on 14/04/2016. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) + +import AppKit + +extension KingfisherWrapper where Base: NSButton { + + // MARK: Setting Image + + /// Sets an image to the button with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about how to get the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested source. + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the button with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.image = placeholder + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.image = placeholder + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.image = image + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + self.base.image = value.image + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.image = image + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Downloading Task + + /// Cancels the image download task of the button if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelImageDownloadTask() { + imageTask?.cancel() + } + + // MARK: Setting Alternate Image + + @discardableResult + public func setAlternateImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setAlternateImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an alternate image to the button with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setAlternateImage( + with resource: Resource?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setAlternateImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setAlternateImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.alternateImage = placeholder + mutatingSelf.alternateTaskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.alternateImage = placeholder + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.alternateTaskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.alternateImage = image + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.alternateTaskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.alternateImageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.alternateTaskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.alternateImageTask = nil + mutatingSelf.alternateTaskIdentifier = nil + + switch result { + case .success(let value): + self.base.alternateImage = value.image + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.alternateImage = image + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.alternateImageTask = task + return task + } + + // MARK: Cancelling Alternate Image Downloading Task + + /// Cancels the alternate image download task of the button if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelAlternateImageDownloadTask() { + alternateImageTask?.cancel() + } +} + + +// MARK: - Associated Object +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +private var alternateTaskIdentifierKey: Void? +private var alternateImageTaskKey: Void? + +extension KingfisherWrapper where Base: NSButton { + + // MARK: Properties + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } + + public private(set) var alternateTaskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &alternateTaskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &alternateTaskIdentifierKey, box) + } + } + + private var alternateImageTask: DownloadTask? { + get { return getAssociatedObject(base, &alternateImageTaskKey) } + set { setRetainedAssociatedObject(base, &alternateImageTaskKey, newValue)} + } +} +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift new file mode 100644 index 0000000..adb5490 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift @@ -0,0 +1,279 @@ +// +// NSTextAttachment+Kingfisher.swift +// Kingfisher +// +// Created by Benjamin Briggs on 22/07/2019. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if !os(watchOS) + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +extension KingfisherWrapper where Base: NSTextAttachment { + + // MARK: Setting Image + + /// Sets an image to the text attachment with a source. + /// + /// - Parameters: + /// - source: The `Source` object defines data information from network or a data provider. + /// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// + /// The retrieved image will be set to `NSTextAttachment.image` property. Because it is not an image view based + /// rendering, options related to view, such as `.transition`, are not supported. + /// + /// Kingfisher will call `setNeedsDisplay` on the `attributedView` when the image task done. It gives the view a + /// chance to render the attributed string again for displaying the downloaded image. For example, if you set an + /// attributed with this `NSTextAttachment` to a `UILabel` object, pass it as the `attributedView` parameter. + /// + /// Here is a typical use case: + /// + /// ```swift + /// let attributedText = NSMutableAttributedString(string: "Hello World") + /// let textAttachment = NSTextAttachment() + /// + /// textAttachment.kf.setImage( + /// with: URL(string: "https://onevcat.com/assets/images/avatar.jpg")!, + /// attributedView: label, + /// options: [ + /// .processor( + /// ResizingImageProcessor(referenceSize: .init(width: 30, height: 30)) + /// |> RoundCornerImageProcessor(cornerRadius: 15)) + /// ] + /// ) + /// attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment)) + /// label.attributedText = attributedText + /// ``` + /// + @discardableResult + public func setImage( + with source: Source?, + attributedView: @autoclosure @escaping () -> KFCrossPlatformView, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + attributedView: attributedView, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the text attachment with a source. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// + /// The retrieved image will be set to `NSTextAttachment.image` property. Because it is not an image view based + /// rendering, options related to view, such as `.transition`, are not supported. + /// + /// Kingfisher will call `setNeedsDisplay` on the `attributedView` when the image task done. It gives the view a + /// chance to render the attributed string again for displaying the downloaded image. For example, if you set an + /// attributed with this `NSTextAttachment` to a `UILabel` object, pass it as the `attributedView` parameter. + /// + /// Here is a typical use case: + /// + /// ```swift + /// let attributedText = NSMutableAttributedString(string: "Hello World") + /// let textAttachment = NSTextAttachment() + /// + /// textAttachment.kf.setImage( + /// with: URL(string: "https://onevcat.com/assets/images/avatar.jpg")!, + /// attributedView: label, + /// options: [ + /// .processor( + /// ResizingImageProcessor(referenceSize: .init(width: 30, height: 30)) + /// |> RoundCornerImageProcessor(cornerRadius: 15)) + /// ] + /// ) + /// attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment)) + /// label.attributedText = attributedText + /// ``` + /// + @discardableResult + public func setImage( + with resource: Resource?, + attributedView: @autoclosure @escaping () -> KFCrossPlatformView, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: resource.map { .network($0) }, + attributedView: attributedView, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + func setImage( + with source: Source?, + attributedView: @escaping () -> KFCrossPlatformView, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.image = placeholder + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.image = placeholder + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.image = image + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + self.base.image = value.image + let view = attributedView() + #if canImport(UIKit) + view.setNeedsDisplay() + #else + view.setNeedsDisplay(view.bounds) + #endif + case .failure: + if let image = options.onFailureImage { + self.base.image = image + } + } + completionHandler?(result) + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Image + + /// Cancel the image download task bounded to the text attachment if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} + +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +extension KingfisherWrapper where Base: NSTextAttachment { + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} + +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/TVMonogramView+Kingfisher.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/TVMonogramView+Kingfisher.swift new file mode 100644 index 0000000..8f0948c --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/TVMonogramView+Kingfisher.swift @@ -0,0 +1,217 @@ +// +// TVMonogramView+Kingfisher.swift +// Kingfisher +// +// Created by Marvin Nazari on 2020-12-07. +// +// Copyright (c) 2020 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +#if canImport(TVUIKit) + +import TVUIKit + +@available(tvOS 12.0, *) +extension KingfisherWrapper where Base: TVMonogramView { + + // MARK: Setting Image + + /// Sets an image to the image view with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.image = placeholder + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.image = placeholder + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.image = image + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + self.base.image = value.image + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.image = image + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + /// Sets an image to the image view with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + // MARK: Cancelling Image + + /// Cancel the image download task bounded to the image view if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} + +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +@available(tvOS 12.0, *) +extension KingfisherWrapper where Base: TVMonogramView { + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} + +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift new file mode 100644 index 0000000..e30fdd6 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift @@ -0,0 +1,416 @@ +// +// UIButton+Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/13. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if !os(watchOS) + +#if canImport(UIKit) +import UIKit + +extension KingfisherWrapper where Base: UIButton { + + // MARK: Setting Image + /// Sets an image to the button for a specified state with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested source, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + for: state, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the button for a specified state with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + for: state, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + @discardableResult + public func setImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + guard let source = source else { + base.setImage(placeholder, for: state) + setTaskIdentifier(nil, for: state) + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.setImage(placeholder, for: state) + } + + var mutatingSelf = self + let issuedIdentifier = Source.Identifier.next() + setTaskIdentifier(issuedIdentifier, for: state) + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.setImage(image, for: state) + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier(for: state) } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.taskIdentifier(for: state) else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.setTaskIdentifier(nil, for: state) + + switch result { + case .success(let value): + self.base.setImage(value.image, for: state) + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.setImage(image, for: state) + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Downloading Task + + /// Cancels the image download task of the button if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelImageDownloadTask() { + imageTask?.cancel() + } + + // MARK: Setting Background Image + + /// Sets a background image to the button for a specified state with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setBackgroundImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setBackgroundImage( + with: source, + for: state, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets a background image to the button for a specified state with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setBackgroundImage( + with resource: Resource?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setBackgroundImage( + with: resource?.convertToSource(), + for: state, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setBackgroundImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + guard let source = source else { + base.setBackgroundImage(placeholder, for: state) + setBackgroundTaskIdentifier(nil, for: state) + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.setBackgroundImage(placeholder, for: state) + } + + var mutatingSelf = self + let issuedIdentifier = Source.Identifier.next() + setBackgroundTaskIdentifier(issuedIdentifier, for: state) + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.setBackgroundImage(image, for: state) + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.backgroundTaskIdentifier(for: state) } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.backgroundImageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.backgroundTaskIdentifier(for: state) else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.backgroundImageTask = nil + mutatingSelf.setBackgroundTaskIdentifier(nil, for: state) + + switch result { + case .success(let value): + self.base.setBackgroundImage(value.image, for: state) + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.setBackgroundImage(image, for: state) + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.backgroundImageTask = task + return task + } + + // MARK: Cancelling Background Downloading Task + + /// Cancels the background image download task of the button if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelBackgroundImageDownloadTask() { + backgroundImageTask?.cancel() + } +} + +// MARK: - Associated Object +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +extension KingfisherWrapper where Base: UIButton { + + private typealias TaskIdentifier = Box<[UInt: Source.Identifier.Value]> + + public func taskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? { + return taskIdentifierInfo.value[state.rawValue] + } + + private func setTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) { + taskIdentifierInfo.value[state.rawValue] = identifier + } + + private var taskIdentifierInfo: TaskIdentifier { + return getAssociatedObject(base, &taskIdentifierKey) ?? { + setRetainedAssociatedObject(base, &taskIdentifierKey, $0) + return $0 + } (TaskIdentifier([:])) + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} + + +private var backgroundTaskIdentifierKey: Void? +private var backgroundImageTaskKey: Void? + +// MARK: Background Properties +extension KingfisherWrapper where Base: UIButton { + + public func backgroundTaskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? { + return backgroundTaskIdentifierInfo.value[state.rawValue] + } + + private func setBackgroundTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) { + backgroundTaskIdentifierInfo.value[state.rawValue] = identifier + } + + private var backgroundTaskIdentifierInfo: TaskIdentifier { + return getAssociatedObject(base, &backgroundTaskIdentifierKey) ?? { + setRetainedAssociatedObject(base, &backgroundTaskIdentifierKey, $0) + return $0 + } (TaskIdentifier([:])) + } + + private var backgroundImageTask: DownloadTask? { + get { return getAssociatedObject(base, &backgroundImageTaskKey) } + mutating set { setRetainedAssociatedObject(base, &backgroundImageTaskKey, newValue) } + } +} +#endif + +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/WKInterfaceImage+Kingfisher.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/WKInterfaceImage+Kingfisher.swift new file mode 100644 index 0000000..ce424de --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/WKInterfaceImage+Kingfisher.swift @@ -0,0 +1,212 @@ +// +// WKInterfaceImage+Kingfisher.swift +// Kingfisher +// +// Created by Rodrigo Borges Soares on 04/05/18. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(WatchKit) + +import WatchKit + +extension KingfisherWrapper where Base: WKInterfaceImage { + + // MARK: Setting Image + + /// Sets an image to the image view with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.setImage(placeholder) + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.setImage(placeholder) + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.setImage(image) + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + self.base.setImage(value.image) + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.setImage(image) + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Image + + /// Cancel the image download task bounded to the image view if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} + +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +extension KingfisherWrapper where Base: WKInterfaceImage { + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift new file mode 100644 index 0000000..db5e2b4 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift @@ -0,0 +1,137 @@ +// +// AVAssetImageDataProvider.swift +// Kingfisher +// +// Created by onevcat on 2020/08/09. +// +// Copyright (c) 2020 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if !os(watchOS) + +import Foundation +import AVKit + +#if canImport(MobileCoreServices) +import MobileCoreServices +#else +import CoreServices +#endif + +/// A data provider to provide thumbnail data from a given AVKit asset. +public struct AVAssetImageDataProvider: ImageDataProvider { + + /// The possible error might be caused by the `AVAssetImageDataProvider`. + /// - userCancelled: The data provider process is cancelled. + /// - invalidImage: The retrieved image is invalid. + public enum AVAssetImageDataProviderError: Error { + case userCancelled + case invalidImage(_ image: CGImage?) + } + + /// The asset image generator bound to `self`. + public let assetImageGenerator: AVAssetImageGenerator + + /// The time at which the image should be generate in the asset. + public let time: CMTime + + private var internalKey: String { + return (assetImageGenerator.asset as? AVURLAsset)?.url.absoluteString ?? UUID().uuidString + } + + /// The cache key used by `self`. + public var cacheKey: String { + return "\(internalKey)_\(time.seconds)" + } + + /// Creates an asset image data provider. + /// - Parameters: + /// - assetImageGenerator: The asset image generator controls data providing behaviors. + /// - time: At which time in the asset the image should be generated. + public init(assetImageGenerator: AVAssetImageGenerator, time: CMTime) { + self.assetImageGenerator = assetImageGenerator + self.time = time + } + + /// Creates an asset image data provider. + /// - Parameters: + /// - assetURL: The URL of asset for providing image data. + /// - time: At which time in the asset the image should be generated. + /// + /// This method uses `assetURL` to create an `AVAssetImageGenerator` object and calls + /// the `init(assetImageGenerator:time:)` initializer. + /// + public init(assetURL: URL, time: CMTime) { + let asset = AVAsset(url: assetURL) + let generator = AVAssetImageGenerator(asset: asset) + self.init(assetImageGenerator: generator, time: time) + } + + /// Creates an asset image data provider. + /// + /// - Parameters: + /// - assetURL: The URL of asset for providing image data. + /// - seconds: At which time in seconds in the asset the image should be generated. + /// + /// This method uses `assetURL` to create an `AVAssetImageGenerator` object, uses `seconds` to create a `CMTime`, + /// and calls the `init(assetImageGenerator:time:)` initializer. + /// + public init(assetURL: URL, seconds: TimeInterval) { + let time = CMTime(seconds: seconds, preferredTimescale: 600) + self.init(assetURL: assetURL, time: time) + } + + public func data(handler: @escaping (Result) -> Void) { + assetImageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: time)]) { + (requestedTime, image, imageTime, result, error) in + if let error = error { + handler(.failure(error)) + return + } + + if result == .cancelled { + handler(.failure(AVAssetImageDataProviderError.userCancelled)) + return + } + + guard let cgImage = image, let data = cgImage.jpegData else { + handler(.failure(AVAssetImageDataProviderError.invalidImage(image))) + return + } + + handler(.success(data)) + } + } +} + +extension CGImage { + var jpegData: Data? { + guard let mutableData = CFDataCreateMutable(nil, 0), + let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) + else { + return nil + } + CGImageDestinationAddImage(destination, self, nil) + guard CGImageDestinationFinalize(destination) else { return nil } + return mutableData as Data + } +} + +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift new file mode 100644 index 0000000..e90ece6 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift @@ -0,0 +1,170 @@ +// +// ImageDataProvider.swift +// Kingfisher +// +// Created by onevcat on 2018/11/13. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a data provider to provide image data to Kingfisher when setting with +/// `Source.provider` source. Compared to `Source.network` member, it gives a chance +/// to load some image data in your own way, as long as you can provide the data +/// representation for the image. +public protocol ImageDataProvider { + + /// The key used in cache. + var cacheKey: String { get } + + /// Provides the data which represents image. Kingfisher uses the data you pass in the + /// handler to process images and caches it for later use. + /// + /// - Parameter handler: The handler you should call when you prepared your data. + /// If the data is loaded successfully, call the handler with + /// a `.success` with the data associated. Otherwise, call it + /// with a `.failure` and pass the error. + /// + /// - Note: + /// If the `handler` is called with a `.failure` with error, a `dataProviderError` of + /// `ImageSettingErrorReason` will be finally thrown out to you as the `KingfisherError` + /// from the framework. + func data(handler: @escaping (Result) -> Void) + + /// The content URL represents this provider, if exists. + var contentURL: URL? { get } +} + +public extension ImageDataProvider { + var contentURL: URL? { return nil } + func convertToSource() -> Source { + .provider(self) + } +} + +/// Represents an image data provider for loading from a local file URL on disk. +/// Uses this type for adding a disk image to Kingfisher. Compared to loading it +/// directly, you can get benefit of using Kingfisher's extension methods, as well +/// as applying `ImageProcessor`s and storing the image to `ImageCache` of Kingfisher. +public struct LocalFileImageDataProvider: ImageDataProvider { + + // MARK: Public Properties + + /// The file URL from which the image be loaded. + public let fileURL: URL + private let loadingQueue: ExecutionQueue + + // MARK: Initializers + + /// Creates an image data provider by supplying the target local file URL. + /// + /// - Parameters: + /// - fileURL: The file URL from which the image be loaded. + /// - cacheKey: The key is used for caching the image data. By default, + /// the `absoluteString` of `fileURL` is used. + /// - loadingQueue: The queue where the file loading should happen. By default, the dispatch queue of + /// `.global(qos: .userInitiated)` will be used. + public init( + fileURL: URL, + cacheKey: String? = nil, + loadingQueue: ExecutionQueue = .dispatch(DispatchQueue.global(qos: .userInitiated)) + ) { + self.fileURL = fileURL + self.cacheKey = cacheKey ?? fileURL.localFileCacheKey + self.loadingQueue = loadingQueue + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public var cacheKey: String + + public func data(handler:@escaping (Result) -> Void) { + loadingQueue.execute { + handler(Result(catching: { try Data(contentsOf: fileURL) })) + } + } + + /// The URL of the local file on the disk. + public var contentURL: URL? { + return fileURL + } +} + +/// Represents an image data provider for loading image from a given Base64 encoded string. +public struct Base64ImageDataProvider: ImageDataProvider { + + // MARK: Public Properties + /// The encoded Base64 string for the image. + public let base64String: String + + // MARK: Initializers + + /// Creates an image data provider by supplying the Base64 encoded string. + /// + /// - Parameters: + /// - base64String: The Base64 encoded string for an image. + /// - cacheKey: The key is used for caching the image data. You need a different key for any different image. + public init(base64String: String, cacheKey: String) { + self.base64String = base64String + self.cacheKey = cacheKey + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public var cacheKey: String + + public func data(handler: (Result) -> Void) { + let data = Data(base64Encoded: base64String)! + handler(.success(data)) + } +} + +/// Represents an image data provider for a raw data object. +public struct RawImageDataProvider: ImageDataProvider { + + // MARK: Public Properties + + /// The raw data object to provide to Kingfisher image loader. + public let data: Data + + // MARK: Initializers + + /// Creates an image data provider by the given raw `data` value and a `cacheKey` be used in Kingfisher cache. + /// + /// - Parameters: + /// - data: The raw data reprensents an image. + /// - cacheKey: The key is used for caching the image data. You need a different key for any different image. + public init(data: Data, cacheKey: String) { + self.data = data + self.cacheKey = cacheKey + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public var cacheKey: String + + public func data(handler: @escaping (Result) -> Void) { + handler(.success(data)) + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift new file mode 100644 index 0000000..1496f3d --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift @@ -0,0 +1,115 @@ +// +// Resource.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents an image resource at a certain url and a given cache key. +/// Kingfisher will use a `Resource` to download a resource from network and cache it with the cache key when +/// using `Source.network` as its image setting source. +public protocol Resource { + + /// The key used in cache. + var cacheKey: String { get } + + /// The target image URL. + var downloadURL: URL { get } +} + +extension Resource { + + /// Converts `self` to a valid `Source` based on its `downloadURL` scheme. A `.provider` with + /// `LocalFileImageDataProvider` associated will be returned if the URL points to a local file. Otherwise, + /// `.network` is returned. + public func convertToSource(overrideCacheKey: String? = nil) -> Source { + let key = overrideCacheKey ?? cacheKey + return downloadURL.isFileURL ? + .provider(LocalFileImageDataProvider(fileURL: downloadURL, cacheKey: key)) : + .network(ImageResource(downloadURL: downloadURL, cacheKey: key)) + } +} + +/// ImageResource is a simple combination of `downloadURL` and `cacheKey`. +/// When passed to image view set methods, Kingfisher will try to download the target +/// image from the `downloadURL`, and then store it with the `cacheKey` as the key in cache. +public struct ImageResource: Resource { + + // MARK: - Initializers + + /// Creates an image resource. + /// + /// - Parameters: + /// - downloadURL: The target image URL from where the image can be downloaded. + /// - cacheKey: The cache key. If `nil`, Kingfisher will use the `absoluteString` of `downloadURL` as the key. + /// Default is `nil`. + public init(downloadURL: URL, cacheKey: String? = nil) { + self.downloadURL = downloadURL + self.cacheKey = cacheKey ?? downloadURL.cacheKey + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public let cacheKey: String + + /// The target image URL. + public let downloadURL: URL +} + +/// URL conforms to `Resource` in Kingfisher. +/// The `absoluteString` of this URL is used as `cacheKey`. And the URL itself will be used as `downloadURL`. +/// If you need customize the url and/or cache key, use `ImageResource` instead. +extension URL: Resource { + public var cacheKey: String { return isFileURL ? localFileCacheKey : absoluteString } + public var downloadURL: URL { return self } +} + +extension URL { + static let localFileCacheKeyPrefix = "kingfisher.local.cacheKey" + + // The special version of cache key for a local file on disk. Every time the app is reinstalled on the disk, + // the system assigns a new container folder to hold the .app (and the extensions, .appex) folder. So the URL for + // the same image in bundle might be different. + // + // This getter only uses the fixed part in the URL (until the bundle name folder) to provide a stable cache key + // for the image under the same path inside the bundle. + // + // See #1825 (https://github.com/onevcat/Kingfisher/issues/1825) + var localFileCacheKey: String { + var validComponents: [String] = [] + for part in pathComponents.reversed() { + validComponents.append(part) + if part.hasSuffix(".app") || part.hasSuffix(".appex") { + break + } + } + let fixedPath = "\(Self.localFileCacheKeyPrefix)/\(validComponents.reversed().joined(separator: "/"))" + if let q = query { + return "\(fixedPath)?\(q)" + } else { + return fixedPath + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/Source.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/Source.swift new file mode 100644 index 0000000..0fcf28b --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/Source.swift @@ -0,0 +1,123 @@ +// +// Source.swift +// Kingfisher +// +// Created by onevcat on 2018/11/17. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents an image setting source for Kingfisher methods. +/// +/// A `Source` value indicates the way how the target image can be retrieved and cached. +/// +/// - network: The target image should be got from network remotely. The associated `Resource` +/// value defines detail information like image URL and cache key. +/// - provider: The target image should be provided in a data format. Normally, it can be an image +/// from local storage or in any other encoding format (like Base64). +public enum Source { + + /// Represents the source task identifier when setting an image to a view with extension methods. + public enum Identifier { + + /// The underlying value type of source identifier. + public typealias Value = UInt + static var current: Value = 0 + static func next() -> Value { + current += 1 + return current + } + } + + // MARK: Member Cases + + /// The target image should be got from network remotely. The associated `Resource` + /// value defines detail information like image URL and cache key. + case network(Resource) + + /// The target image should be provided in a data format. Normally, it can be an image + /// from local storage or in any other encoding format (like Base64). + case provider(ImageDataProvider) + + // MARK: Getting Properties + + /// The cache key defined for this source value. + public var cacheKey: String { + switch self { + case .network(let resource): return resource.cacheKey + case .provider(let provider): return provider.cacheKey + } + } + + /// The URL defined for this source value. + /// + /// For a `.network` source, it is the `downloadURL` of associated `Resource` instance. + /// For a `.provider` value, it is always `nil`. + public var url: URL? { + switch self { + case .network(let resource): return resource.downloadURL + case .provider(let provider): return provider.contentURL + } + } +} + +extension Source: Hashable { + public static func == (lhs: Source, rhs: Source) -> Bool { + switch (lhs, rhs) { + case (.network(let r1), .network(let r2)): + return r1.cacheKey == r2.cacheKey && r1.downloadURL == r2.downloadURL + case (.provider(let p1), .provider(let p2)): + return p1.cacheKey == p2.cacheKey && p1.contentURL == p2.contentURL + case (.provider(_), .network(_)): + return false + case (.network(_), .provider(_)): + return false + } + } + + public func hash(into hasher: inout Hasher) { + switch self { + case .network(let r): + hasher.combine(r.cacheKey) + hasher.combine(r.downloadURL) + case .provider(let p): + hasher.combine(p.cacheKey) + hasher.combine(p.contentURL) + } + } +} + +extension Source { + var asResource: Resource? { + guard case .network(let resource) = self else { + return nil + } + return resource + } + + var asProvider: ImageDataProvider? { + guard case .provider(let provider) = self else { + return nil + } + return provider + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KF.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KF.swift new file mode 100644 index 0000000..3b033b9 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KF.swift @@ -0,0 +1,442 @@ +// +// KF.swift +// Kingfisher +// +// Created by onevcat on 2020/09/21. +// +// Copyright (c) 2020 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(UIKit) +import UIKit +#endif + +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +import CarPlay +#endif + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif + +#if canImport(WatchKit) +import WatchKit +#endif + +#if canImport(TVUIKit) +import TVUIKit +#endif + +/// A helper type to create image setting tasks in a builder pattern. +/// Use methods in this type to create a `KF.Builder` instance and configure image tasks there. +public enum KF { + + /// Creates a builder for a given `Source`. + /// - Parameter source: The `Source` object defines data information from network or a data provider. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func source(_ source: Source?) -> KF.Builder { + Builder(source: source) + } + + /// Creates a builder for a given `Resource`. + /// - Parameter resource: The `Resource` object defines data information like key or URL. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func resource(_ resource: Resource?) -> KF.Builder { + source(resource?.convertToSource()) + } + + /// Creates a builder for a given `URL` and an optional cache key. + /// - Parameters: + /// - url: The URL where the image should be downloaded. + /// - cacheKey: The key used to store the downloaded image in cache. + /// If `nil`, the `absoluteString` of `url` is used as the cache key. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func url(_ url: URL?, cacheKey: String? = nil) -> KF.Builder { + source(url?.convertToSource(overrideCacheKey: cacheKey)) + } + + /// Creates a builder for a given `ImageDataProvider`. + /// - Parameter provider: The `ImageDataProvider` object contains information about the data. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func dataProvider(_ provider: ImageDataProvider?) -> KF.Builder { + source(provider?.convertToSource()) + } + + /// Creates a builder for some given raw data and a cache key. + /// - Parameters: + /// - data: The data object from which the image should be created. + /// - cacheKey: The key used to store the downloaded image in cache. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func data(_ data: Data?, cacheKey: String) -> KF.Builder { + if let data = data { + return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey)) + } else { + return dataProvider(nil) + } + } +} + + +extension KF { + + /// A builder class to configure an image retrieving task and set it to a holder view or component. + public class Builder { + private let source: Source? + + #if os(watchOS) + private var placeholder: KFCrossPlatformImage? + #else + private var placeholder: Placeholder? + #endif + + public var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions) + + public let onFailureDelegate = Delegate() + public let onSuccessDelegate = Delegate() + public let onProgressDelegate = Delegate<(Int64, Int64), Void>() + + init(source: Source?) { + self.source = source + } + + private var resultHandler: ((Result) -> Void)? { + { + switch $0 { + case .success(let result): + self.onSuccessDelegate(result) + case .failure(let error): + self.onFailureDelegate(error) + } + } + } + + private var progressBlock: DownloadProgressBlock { + { self.onProgressDelegate(($0, $1)) } + } + } +} + +extension KF.Builder { + #if !os(watchOS) + + /// Builds the image task request and sets it to an image view. + /// - Parameter imageView: The image view which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to imageView: KFCrossPlatformImageView) -> DownloadTask? { + imageView.kf.setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + /// Builds the image task request and sets it to an `NSTextAttachment` object. + /// - Parameters: + /// - attachment: The text attachment object which loads the task and should be set with the image. + /// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to attachment: NSTextAttachment, attributedView: @autoclosure @escaping () -> KFCrossPlatformView) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return attachment.kf.setImage( + with: source, + attributedView: attributedView, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + #if canImport(UIKit) + + /// Builds the image task request and sets it to a button. + /// - Parameters: + /// - button: The button which loads the task and should be set with the image. + /// - state: The button state to which the image should be set. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to button: UIButton, for state: UIControl.State) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setImage( + with: source, + for: state, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + /// Builds the image task request and sets it to the background image for a button. + /// - Parameters: + /// - button: The button which loads the task and should be set with the image. + /// - state: The button state to which the image should be set. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func setBackground(to button: UIButton, for state: UIControl.State) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setBackgroundImage( + with: source, + for: state, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(UIKit) + + #if canImport(CarPlay) && !targetEnvironment(macCatalyst) + + /// Builds the image task request and sets it to the image for a list item. + /// - Parameters: + /// - listItem: The list item which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @available(iOS 14.0, *) + @discardableResult + public func set(to listItem: CPListItem) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return listItem.kf.setImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + + } + + #endif + + #if canImport(AppKit) && !targetEnvironment(macCatalyst) + /// Builds the image task request and sets it to a button. + /// - Parameter button: The button which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to button: NSButton) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + /// Builds the image task request and sets it to the alternative image for a button. + /// - Parameter button: The button which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func setAlternative(to button: NSButton) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setAlternateImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(AppKit) + #endif // end of !os(watchOS) + + #if canImport(WatchKit) + /// Builds the image task request and sets it to a `WKInterfaceImage` object. + /// - Parameter interfaceImage: The watch interface image which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to interfaceImage: WKInterfaceImage) -> DownloadTask? { + return interfaceImage.kf.setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(WatchKit) + + #if canImport(TVUIKit) + /// Builds the image task request and sets it to a TV monogram view. + /// - Parameter monogramView: The monogram view which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @available(tvOS 12.0, *) + @discardableResult + public func set(to monogramView: TVMonogramView) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return monogramView.kf.setImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(TVUIKit) +} + +#if !os(watchOS) +extension KF.Builder { + #if os(iOS) || os(tvOS) + + /// Sets a placeholder which is used while retrieving the image. + /// - Parameter placeholder: A placeholder to show while retrieving the image from its source. + /// - Returns: A `KF.Builder` with changes applied. + public func placeholder(_ placeholder: Placeholder?) -> Self { + self.placeholder = placeholder + return self + } + #endif + + /// Sets a placeholder image which is used while retrieving the image. + /// - Parameter placeholder: An image to show while retrieving the image from its source. + /// - Returns: A `KF.Builder` with changes applied. + public func placeholder(_ image: KFCrossPlatformImage?) -> Self { + self.placeholder = image + return self + } +} +#endif + +extension KF.Builder { + + #if os(iOS) || os(tvOS) + /// Sets the transition for the image task. + /// - Parameter transition: The desired transition effect when setting the image to image view. + /// - Returns: A `KF.Builder` with changes applied. + /// + /// Kingfisher will use the `transition` to animate the image in if it is downloaded from web. + /// The transition will not happen when the + /// image is retrieved from either memory or disk cache by default. If you need to do the transition even when + /// the image being retrieved from cache, also call `forceRefresh()` on the returned `KF.Builder`. + public func transition(_ transition: ImageTransition) -> Self { + options.transition = transition + return self + } + + /// Sets a fade transition for the image task. + /// - Parameter duration: The duration of the fade transition. + /// - Returns: A `KF.Builder` with changes applied. + /// + /// Kingfisher will use the fade transition to animate the image in if it is downloaded from web. + /// The transition will not happen when the + /// image is retrieved from either memory or disk cache by default. If you need to do the transition even when + /// the image being retrieved from cache, also call `forceRefresh()` on the returned `KF.Builder`. + public func fade(duration: TimeInterval) -> Self { + options.transition = .fade(duration) + return self + } + #endif + + /// Sets whether keeping the existing image of image view while setting another image to it. + /// - Parameter enabled: Whether the existing image should be kept. + /// - Returns: A `KF.Builder` with changes applied. + /// + /// By setting this option, the placeholder image parameter of image view extension method + /// will be ignored and the current image will be kept while loading or downloading the new image. + /// + public func keepCurrentImageWhileLoading(_ enabled: Bool = true) -> Self { + options.keepCurrentImageWhileLoading = enabled + return self + } + + /// Sets whether only the first frame from an animated image file should be loaded as a single image. + /// - Parameter enabled: Whether the only the first frame should be loaded. + /// - Returns: A `KF.Builder` with changes applied. + /// + /// Loading an animated images may take too much memory. It will be useful when you want to display a + /// static preview of the first frame from an animated image. + /// + /// This option will be ignored if the target image is not animated image data. + /// + public func onlyLoadFirstFrame(_ enabled: Bool = true) -> Self { + options.onlyLoadFirstFrame = enabled + return self + } + + /// Enables progressive image loading with a specified `ImageProgressive` setting to process the + /// progressive JPEG data and display it in a progressive way. + /// - Parameter progressive: The progressive settings which is used while loading. + /// - Returns: A `KF.Builder` with changes applied. + public func progressiveJPEG(_ progressive: ImageProgressive? = .default) -> Self { + options.progressiveJPEG = progressive + return self + } +} + +// MARK: - Deprecated +extension KF.Builder { + /// Starts the loading process of `self` immediately. + /// + /// By default, a `KFImage` will not load its source until the `onAppear` is called. This is a lazily loading + /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a + /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once + /// could help avoiding the flickering, with some performance trade-off. + /// + /// - Deprecated: This is not necessary anymore since `@StateObject` is used for holding the image data. + /// It does nothing now and please just remove it. + /// + /// - Returns: The `Self` value with changes applied. + @available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.") + public func loadImmediately(_ start: Bool = true) -> Self { + return self + } +} + +// MARK: - Redirect Handler +extension KF { + + /// Represents the detail information when a task redirect happens. It is wrapping necessary information for a + /// `ImageDownloadRedirectHandler`. See that protocol for more information. + public struct RedirectPayload { + + /// The related session data task when the redirect happens. It is + /// the current `SessionDataTask` which triggers this redirect. + public let task: SessionDataTask + + /// The response received during redirection. + public let response: HTTPURLResponse + + /// The request for redirection which can be modified. + public let newRequest: URLRequest + + /// A closure for being called with modified request. + public let completionHandler: (URLRequest?) -> Void + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift new file mode 100644 index 0000000..c773593 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift @@ -0,0 +1,707 @@ +// +// KFOptionsSetter.swift +// Kingfisher +// +// Created by onevcat on 2020/12/22. +// +// Copyright (c) 2020 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CoreGraphics + +public protocol KFOptionSetter { + var options: KingfisherParsedOptionsInfo { get nonmutating set } + + var onFailureDelegate: Delegate { get } + var onSuccessDelegate: Delegate { get } + var onProgressDelegate: Delegate<(Int64, Int64), Void> { get } + + var delegateObserver: AnyObject { get } +} + +extension KF.Builder: KFOptionSetter { + public var delegateObserver: AnyObject { self } +} + +// MARK: - Life cycles +extension KFOptionSetter { + /// Sets the progress block to current builder. + /// - Parameter block: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. If `block` is `nil`, the callback + /// will be reset. + /// - Returns: A `Self` value with changes applied. + public func onProgress(_ block: DownloadProgressBlock?) -> Self { + onProgressDelegate.delegate(on: delegateObserver) { (observer, result) in + block?(result.0, result.1) + } + return self + } + + /// Sets the the done block to current builder. + /// - Parameter block: Called when the image task successfully completes and the the image set is done. If `block` + /// is `nil`, the callback will be reset. + /// - Returns: A `KF.Builder` with changes applied. + public func onSuccess(_ block: ((RetrieveImageResult) -> Void)?) -> Self { + onSuccessDelegate.delegate(on: delegateObserver) { (observer, result) in + block?(result) + } + return self + } + + /// Sets the catch block to current builder. + /// - Parameter block: Called when an error happens during the image task. If `block` + /// is `nil`, the callback will be reset. + /// - Returns: A `KF.Builder` with changes applied. + public func onFailure(_ block: ((KingfisherError) -> Void)?) -> Self { + onFailureDelegate.delegate(on: delegateObserver) { (observer, error) in + block?(error) + } + return self + } +} + +// MARK: - Basic options settings. +extension KFOptionSetter { + /// Sets the target image cache for this task. + /// - Parameter cache: The target cache is about to be used for the task. + /// - Returns: A `Self` value with changes applied. + /// + /// Kingfisher will use the associated `ImageCache` object when handling related operations, + /// including trying to retrieve the cached images and store the downloaded image to it. + /// + public func targetCache(_ cache: ImageCache) -> Self { + options.targetCache = cache + return self + } + + /// Sets the target image cache to store the original downloaded image for this task. + /// - Parameter cache: The target cache is about to be used for storing the original downloaded image from the task. + /// - Returns: A `Self` value with changes applied. + /// + /// The `ImageCache` for storing and retrieving original images. If `originalCache` is + /// contained in the options, it will be preferred for storing and retrieving original images. + /// If there is no `.originalCache` in the options, `.targetCache` will be used to store original images. + /// + /// When using KingfisherManager to download and store an image, if `cacheOriginalImage` is + /// applied in the option, the original image will be stored to this `originalCache`. At the + /// same time, if a requested final image (with processor applied) cannot be found in `targetCache`, + /// Kingfisher will try to search the original image to check whether it is already there. If found, + /// it will be used and applied with the given processor. It is an optimization for not downloading + /// the same image for multiple times. + /// + public func originalCache(_ cache: ImageCache) -> Self { + options.originalCache = cache + return self + } + + /// Sets the downloader used to perform the image download task. + /// - Parameter downloader: The downloader which is about to be used for downloading. + /// - Returns: A `Self` value with changes applied. + /// + /// Kingfisher will use the set `ImageDownloader` object to download the requested images. + public func downloader(_ downloader: ImageDownloader) -> Self { + options.downloader = downloader + return self + } + + /// Sets the download priority for the image task. + /// - Parameter priority: The download priority of image download task. + /// - Returns: A `Self` value with changes applied. + /// + /// The `priority` value will be set as the priority of the image download task. The value for it should be + /// between 0.0~1.0. You can choose a value between `URLSessionTask.defaultPriority`, `URLSessionTask.lowPriority` + /// or `URLSessionTask.highPriority`. If this option not set, the default value (`URLSessionTask.defaultPriority`) + /// will be used. + public func downloadPriority(_ priority: Float) -> Self { + options.downloadPriority = priority + return self + } + + /// Sets whether Kingfisher should ignore the cache and try to start a download task for the image source. + /// - Parameter enabled: Enable the force refresh or not. + /// - Returns: A `Self` value with changes applied. + public func forceRefresh(_ enabled: Bool = true) -> Self { + options.forceRefresh = enabled + return self + } + + /// Sets whether Kingfisher should try to retrieve the image from memory cache first. If not found, it ignores the + /// disk cache and starts a download task for the image source. + /// - Parameter enabled: Enable the memory-only cache searching or not. + /// - Returns: A `Self` value with changes applied. + /// + /// This is useful when you want to display a changeable image behind the same url at the same app session, while + /// avoiding download it for multiple times. + public func fromMemoryCacheOrRefresh(_ enabled: Bool = true) -> Self { + options.fromMemoryCacheOrRefresh = enabled + return self + } + + /// Sets whether the image should only be cached in memory but not in disk. + /// - Parameter enabled: Whether the image should be only cache in memory or not. + /// - Returns: A `Self` value with changes applied. + public func cacheMemoryOnly(_ enabled: Bool = true) -> Self { + options.cacheMemoryOnly = enabled + return self + } + + /// Sets whether Kingfisher should wait for caching operation to be completed before calling the + /// `onSuccess` or `onFailure` block. + /// - Parameter enabled: Whether Kingfisher should wait for caching operation. + /// - Returns: A `Self` value with changes applied. + public func waitForCache(_ enabled: Bool = true) -> Self { + options.waitForCache = enabled + return self + } + + /// Sets whether Kingfisher should only try to retrieve the image from cache, but not from network. + /// - Parameter enabled: Whether Kingfisher should only try to retrieve the image from cache. + /// - Returns: A `Self` value with changes applied. + /// + /// If the image is not in cache, the image retrieving will fail with the + /// `KingfisherError.cacheError` with `.imageNotExisting` as its reason. + public func onlyFromCache(_ enabled: Bool = true) -> Self { + options.onlyFromCache = enabled + return self + } + + /// Sets whether the image should be decoded in a background thread before using. + /// - Parameter enabled: Whether the image should be decoded in a background thread before using. + /// - Returns: A `Self` value with changes applied. + /// + /// Setting to `true` will decode the downloaded image data and do a off-screen rendering to extract pixel + /// information in background. This can speed up display, but will cost more time and memory to prepare the image + /// for using. + public func backgroundDecode(_ enabled: Bool = true) -> Self { + options.backgroundDecode = enabled + return self + } + + /// Sets the callback queue which is used as the target queue of dispatch callbacks when retrieving images from + /// cache. If not set, Kingfisher will use main queue for callbacks. + /// - Parameter queue: The target queue which the cache retrieving callback will be invoked on. + /// - Returns: A `Self` value with changes applied. + /// + /// - Note: + /// This option does not affect the callbacks for UI related extension methods or `KFImage` result handlers. + /// You will always get the callbacks called from main queue. + public func callbackQueue(_ queue: CallbackQueue) -> Self { + options.callbackQueue = queue + return self + } + + /// Sets the scale factor value when converting retrieved data to an image. + /// - Parameter factor: The scale factor value. + /// - Returns: A `Self` value with changes applied. + /// + /// Specify the image scale, instead of your screen scale. You may need to set the correct scale when you dealing + /// with 2x or 3x retina images. Otherwise, Kingfisher will convert the data to image object at `scale` 1.0. + /// + public func scaleFactor(_ factor: CGFloat) -> Self { + options.scaleFactor = factor + return self + } + + /// Sets whether the original image should be cached even when the original image has been processed by any other + /// `ImageProcessor`s. + /// - Parameter enabled: Whether the original image should be cached. + /// - Returns: A `Self` value with changes applied. + /// + /// If set and an `ImageProcessor` is used, Kingfisher will try to cache both the final result and original + /// image. Kingfisher will have a chance to use the original image when another processor is applied to the same + /// resource, instead of downloading it again. You can use `.originalCache` to specify a cache or the original + /// images if necessary. + /// + /// The original image will be only cached to disk storage. + /// + public func cacheOriginalImage(_ enabled: Bool = true) -> Self { + options.cacheOriginalImage = enabled + return self + } + + /// Sets writing options for an original image on a first write + /// - Parameter writingOptions: Options to control the writing of data to a disk storage. + /// - Returns: A `Self` value with changes applied. + /// If set, options will be passed the store operation for a new files. + /// + /// This is useful if you want to implement file enctyption on first write - eg [.completeFileProtection] + /// + public func diskStoreWriteOptions(_ writingOptions: Data.WritingOptions) -> Self { + options.diskStoreWriteOptions = writingOptions + return self + } + + /// Sets whether the disk storage loading should happen in the same calling queue. + /// - Parameter enabled: Whether the disk storage loading should happen in the same calling queue. + /// - Returns: A `Self` value with changes applied. + /// + /// By default, disk storage file loading + /// happens in its own queue with an asynchronous dispatch behavior. Although it provides better non-blocking disk + /// loading performance, it also causes a flickering when you reload an image from disk, if the image view already + /// has an image set. + /// + /// Set this options will stop that flickering by keeping all loading in the same queue (typically the UI queue + /// if you are using Kingfisher's extension methods to set an image), with a tradeoff of loading performance. + /// + public func loadDiskFileSynchronously(_ enabled: Bool = true) -> Self { + options.loadDiskFileSynchronously = enabled + return self + } + + /// Sets a queue on which the image processing should happen. + /// - Parameter queue: The queue on which the image processing should happen. + /// - Returns: A `Self` value with changes applied. + /// + /// By default, Kingfisher uses a pre-defined serial + /// queue to process images. Use this option to change this behavior. For example, specify a `.mainCurrentOrAsync` + /// to let the image be processed in main queue to prevent a possible flickering (but with a possibility of + /// blocking the UI, especially if the processor needs a lot of time to run). + public func processingQueue(_ queue: CallbackQueue?) -> Self { + options.processingQueue = queue + return self + } + + /// Sets the alternative sources that will be used when loading of the original input `Source` fails. + /// - Parameter sources: The alternative sources will be used. + /// - Returns: A `Self` value with changes applied. + /// + /// Values of the `sources` array will be used to start a new image loading task if the previous task + /// fails due to an error. The image source loading process will stop as soon as a source is loaded successfully. + /// If all `sources` are used but the loading is still failing, an `imageSettingError` with + /// `alternativeSourcesExhausted` as its reason will be given out in the `catch` block. + /// + /// This is useful if you want to implement a fallback solution for setting image. + /// + /// User cancellation will not trigger the alternative source loading. + public func alternativeSources(_ sources: [Source]?) -> Self { + options.alternativeSources = sources + return self + } + + /// Sets a retry strategy that will be used when something gets wrong during the image retrieving. + /// - Parameter strategy: The provided strategy to define how the retrying should happen. + /// - Returns: A `Self` value with changes applied. + public func retry(_ strategy: RetryStrategy?) -> Self { + options.retryStrategy = strategy + return self + } + + /// Sets a retry strategy with a max retry count and retrying interval. + /// - Parameters: + /// - maxCount: The maximum count before the retry stops. + /// - interval: The time interval between each retry attempt. + /// - Returns: A `Self` value with changes applied. + /// + /// This defines the simplest retry strategy, which retry a failing request for several times, with some certain + /// interval between each time. For example, `.retry(maxCount: 3, interval: .second(3))` means attempt for at most + /// three times, and wait for 3 seconds if a previous retry attempt fails, then start a new attempt. + public func retry(maxCount: Int, interval: DelayRetryStrategy.Interval = .seconds(3)) -> Self { + let strategy = DelayRetryStrategy(maxRetryCount: maxCount, retryInterval: interval) + options.retryStrategy = strategy + return self + } + + /// Sets the `Source` should be loaded when user enables Low Data Mode and the original source fails with an + /// `NSURLErrorNetworkUnavailableReason.constrained` error. + /// - Parameter source: The `Source` will be loaded under low data mode. + /// - Returns: A `Self` value with changes applied. + /// + /// When this option is set, the + /// `allowsConstrainedNetworkAccess` property of the request for the original source will be set to `false` and the + /// `Source` in associated value will be used to retrieve the image for low data mode. Usually, you can provide a + /// low-resolution version of your image or a local image provider to display a placeholder. + /// + /// If not set or the `source` is `nil`, the device Low Data Mode will be ignored and the original source will + /// be loaded following the system default behavior, in a normal way. + public func lowDataModeSource(_ source: Source?) -> Self { + options.lowDataModeSource = source + return self + } + + /// Sets whether the image setting for an image view should happen with transition even when retrieved from cache. + /// - Parameter enabled: Enable the force transition or not. + /// - Returns: A `Self` with changes applied. + public func forceTransition(_ enabled: Bool = true) -> Self { + options.forceTransition = enabled + return self + } + + /// Sets the image that will be used if an image retrieving task fails. + /// - Parameter image: The image that will be used when something goes wrong. + /// - Returns: A `Self` with changes applied. + /// + /// If set and an image retrieving error occurred Kingfisher will set provided image (or empty) + /// in place of requested one. It's useful when you don't want to show placeholder + /// during loading time but wants to use some default image when requests will be failed. + /// + public func onFailureImage(_ image: KFCrossPlatformImage?) -> Self { + options.onFailureImage = .some(image) + return self + } +} + +// MARK: - Request Modifier +extension KFOptionSetter { + /// Sets an `ImageDownloadRequestModifier` to change the image download request before it being sent. + /// - Parameter modifier: The modifier will be used to change the request before it being sent. + /// - Returns: A `Self` value with changes applied. + /// + /// This is the last chance you can modify the image download request. You can modify the request for some + /// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping. + /// + public func requestModifier(_ modifier: AsyncImageDownloadRequestModifier) -> Self { + options.requestModifier = modifier + return self + } + + /// Sets a block to change the image download request before it being sent. + /// - Parameter modifyBlock: The modifying block will be called to change the request before it being sent. + /// - Returns: A `Self` value with changes applied. + /// + /// This is the last chance you can modify the image download request. You can modify the request for some + /// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping. + /// + public func requestModifier(_ modifyBlock: @escaping (inout URLRequest) -> Void) -> Self { + options.requestModifier = AnyModifier { r -> URLRequest? in + var request = r + modifyBlock(&request) + return request + } + return self + } +} + +// MARK: - Redirect Handler +extension KFOptionSetter { + /// The `ImageDownloadRedirectHandler` argument will be used to change the request before redirection. + /// This is the possibility you can modify the image download request during redirect. You can modify the request for + /// some customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url + /// mapping. + /// The original redirection request will be sent without any modification by default. + /// - Parameter handler: The handler will be used for redirection. + /// - Returns: A `Self` value with changes applied. + public func redirectHandler(_ handler: ImageDownloadRedirectHandler) -> Self { + options.redirectHandler = handler + return self + } + + /// The `block` will be used to change the request before redirection. + /// This is the possibility you can modify the image download request during redirect. You can modify the request for + /// some customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url + /// mapping. + /// The original redirection request will be sent without any modification by default. + /// - Parameter block: The block will be used for redirection. + /// - Returns: A `Self` value with changes applied. + public func redirectHandler(_ block: @escaping (KF.RedirectPayload) -> Void) -> Self { + let redirectHandler = AnyRedirectHandler { (task, response, request, handler) in + let payload = KF.RedirectPayload( + task: task, response: response, newRequest: request, completionHandler: handler + ) + block(payload) + } + options.redirectHandler = redirectHandler + return self + } +} + +// MARK: - Processor +extension KFOptionSetter { + + /// Sets an image processor for the image task. It replaces the current image processor settings. + /// + /// - Parameter processor: The processor you want to use to process the image after it is downloaded. + /// - Returns: A `Self` value with changes applied. + /// + /// - Note: + /// To append a processor to current ones instead of replacing them all, use `appendProcessor(_:)`. + public func setProcessor(_ processor: ImageProcessor) -> Self { + options.processor = processor + return self + } + + /// Sets an array of image processors for the image task. It replaces the current image processor settings. + /// - Parameter processors: An array of processors. The processors inside this array will be concatenated one by one + /// to form a processor pipeline. + /// - Returns: A `Self` value with changes applied. + /// + /// - Note: + /// To append processors to current ones instead of replacing them all, concatenate them by `|>`, then use + /// `appendProcessor(_:)`. + public func setProcessors(_ processors: [ImageProcessor]) -> Self { + switch processors.count { + case 0: + options.processor = DefaultImageProcessor.default + case 1...: + options.processor = processors.dropFirst().reduce(processors[0]) { $0 |> $1 } + default: + assertionFailure("Never happen") + } + return self + } + + /// Appends a processor to the current set processors. + /// - Parameter processor: The processor which will be appended to current processor settings. + /// - Returns: A `Self` value with changes applied. + public func appendProcessor(_ processor: ImageProcessor) -> Self { + options.processor = options.processor |> processor + return self + } + + /// Appends a `RoundCornerImageProcessor` to current processors. + /// - Parameters: + /// - radius: The radius will be applied in processing. Specify a certain point value with `.point`, or a fraction + /// of the target image with `.widthFraction`. or `.heightFraction`. For example, given a square image + /// with width and height equals, `.widthFraction(0.5)` means use half of the length of size and makes + /// the final image a round one. + /// - targetSize: Target size of output image should be. If `nil`, the image will keep its original size after processing. + /// - corners: The target corners which will be applied rounding. + /// - backgroundColor: Background color of the output image. If `nil`, it will use a transparent background. + /// - Returns: A `Self` value with changes applied. + public func roundCorner( + radius: Radius, + targetSize: CGSize? = nil, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) -> Self + { + let processor = RoundCornerImageProcessor( + radius: radius, + targetSize: targetSize, + roundingCorners: corners, + backgroundColor: backgroundColor + ) + return appendProcessor(processor) + } + + /// Appends a `BlurImageProcessor` to current processors. + /// - Parameter radius: Blur radius for the simulated Gaussian blur. + /// - Returns: A `Self` value with changes applied. + public func blur(radius: CGFloat) -> Self { + appendProcessor( + BlurImageProcessor(blurRadius: radius) + ) + } + + /// Appends a `OverlayImageProcessor` to current processors. + /// - Parameters: + /// - color: Overlay color will be used to overlay the input image. + /// - fraction: Fraction will be used when overlay the color to image. + /// - Returns: A `Self` value with changes applied. + public func overlay(color: KFCrossPlatformColor, fraction: CGFloat = 0.5) -> Self { + appendProcessor( + OverlayImageProcessor(overlay: color, fraction: fraction) + ) + } + + /// Appends a `TintImageProcessor` to current processors. + /// - Parameter color: Tint color will be used to tint the input image. + /// - Returns: A `Self` value with changes applied. + public func tint(color: KFCrossPlatformColor) -> Self { + appendProcessor( + TintImageProcessor(tint: color) + ) + } + + /// Appends a `BlackWhiteProcessor` to current processors. + /// - Returns: A `Self` value with changes applied. + public func blackWhite() -> Self { + appendProcessor( + BlackWhiteProcessor() + ) + } + + /// Appends a `CroppingImageProcessor` to current processors. + /// - Parameters: + /// - size: Target size of output image should be. + /// - anchor: Anchor point from which the output size should be calculate. The anchor point is consisted by two + /// values between 0.0 and 1.0. It indicates a related point in current image. + /// See `CroppingImageProcessor.init(size:anchor:)` for more. + /// - Returns: A `Self` value with changes applied. + public func cropping(size: CGSize, anchor: CGPoint = .init(x: 0.5, y: 0.5)) -> Self { + appendProcessor( + CroppingImageProcessor(size: size, anchor: anchor) + ) + } + + /// Appends a `DownsamplingImageProcessor` to current processors. + /// + /// Compared to `ResizingImageProcessor`, the `DownsamplingImageProcessor` does not render the original images and + /// then resize it. Instead, it downsamples the input data directly to a thumbnail image. So it is a more efficient + /// than `ResizingImageProcessor`. Prefer to use `DownsamplingImageProcessor` as possible + /// as you can than the `ResizingImageProcessor`. + /// + /// Only CG-based images are supported. Animated images (like GIF) is not supported. + /// + /// - Parameter size: Target size of output image should be. It should be smaller than the size of input image. + /// If it is larger, the result image will be the same size of input data without downsampling. + /// - Returns: A `Self` value with changes applied. + public func downsampling(size: CGSize) -> Self { + let processor = DownsamplingImageProcessor(size: size) + if options.processor == DefaultImageProcessor.default { + return setProcessor(processor) + } else { + return appendProcessor(processor) + } + } + + + /// Appends a `ResizingImageProcessor` to current processors. + /// + /// If you need to resize a data represented image to a smaller size, use `DownsamplingImageProcessor` + /// instead, which is more efficient and uses less memory. + /// + /// - Parameters: + /// - referenceSize: The reference size for resizing operation in point. + /// - mode: Target content mode of output image should be. Default is `.none`. + /// - Returns: A `Self` value with changes applied. + public func resizing(referenceSize: CGSize, mode: ContentMode = .none) -> Self { + appendProcessor( + ResizingImageProcessor(referenceSize: referenceSize, mode: mode) + ) + } +} + +// MARK: - Cache Serializer +extension KFOptionSetter { + + /// Uses a given `CacheSerializer` to convert some data to an image object for retrieving from disk cache or vice + /// versa for storing to disk cache. + /// - Parameter cacheSerializer: The `CacheSerializer` which will be used. + /// - Returns: A `Self` value with changes applied. + public func serialize(by cacheSerializer: CacheSerializer) -> Self { + options.cacheSerializer = cacheSerializer + return self + } + + /// Uses a given format to serializer the image data to disk. It converts the image object to the give data format. + /// - Parameters: + /// - format: The desired data encoding format when store the image on disk. + /// - jpegCompressionQuality: If the format is `.JPEG`, it specify the compression quality when converting the + /// image to a JPEG data. Otherwise, it is ignored. + /// - Returns: A `Self` value with changes applied. + public func serialize(as format: ImageFormat, jpegCompressionQuality: CGFloat? = nil) -> Self { + let cacheSerializer: FormatIndicatedCacheSerializer + switch format { + case .JPEG: + cacheSerializer = .jpeg(compressionQuality: jpegCompressionQuality ?? 1.0) + case .PNG: + cacheSerializer = .png + case .GIF: + cacheSerializer = .gif + case .unknown: + cacheSerializer = .png + } + options.cacheSerializer = cacheSerializer + return self + } +} + +// MARK: - Image Modifier +extension KFOptionSetter { + + /// Sets an `ImageModifier` to the image task. Use this to modify the fetched image object properties if needed. + /// + /// If the image was fetched directly from the downloader, the modifier will run directly after the + /// `ImageProcessor`. If the image is being fetched from a cache, the modifier will run after the `CacheSerializer`. + /// - Parameter modifier: The `ImageModifier` which will be used to modify the image object. + /// - Returns: A `Self` value with changes applied. + public func imageModifier(_ modifier: ImageModifier?) -> Self { + options.imageModifier = modifier + return self + } + + /// Sets a block to modify the image object. Use this to modify the fetched image object properties if needed. + /// + /// If the image was fetched directly from the downloader, the modifier block will run directly after the + /// `ImageProcessor`. If the image is being fetched from a cache, the modifier will run after the `CacheSerializer`. + /// + /// - Parameter block: The block which is used to modify the image object. + /// - Returns: A `Self` value with changes applied. + public func imageModifier(_ block: @escaping (inout KFCrossPlatformImage) throws -> Void) -> Self { + let modifier = AnyImageModifier { image -> KFCrossPlatformImage in + var image = image + try block(&image) + return image + } + options.imageModifier = modifier + return self + } +} + + +// MARK: - Cache Expiration +extension KFOptionSetter { + + /// Sets the expiration setting for memory cache of this image task. + /// + /// By default, the underlying `MemoryStorage.Backend` uses the + /// expiration in its config for all items. If set, the `MemoryStorage.Backend` will use this value to overwrite + /// the config setting for this caching item. + /// + /// - Parameter expiration: The expiration setting used in cache storage. + /// - Returns: A `Self` value with changes applied. + public func memoryCacheExpiration(_ expiration: StorageExpiration?) -> Self { + options.memoryCacheExpiration = expiration + return self + } + + /// Sets the expiration extending setting for memory cache. The item expiration time will be incremented by this + /// value after access. + /// + /// By default, the underlying `MemoryStorage.Backend` uses the initial cache expiration as extending + /// value: .cacheTime. + /// + /// To disable extending option at all, sets `.none` to it. + /// + /// - Parameter extending: The expiration extending setting used in cache storage. + /// - Returns: A `Self` value with changes applied. + public func memoryCacheAccessExtending(_ extending: ExpirationExtending) -> Self { + options.memoryCacheAccessExtendingExpiration = extending + return self + } + + /// Sets the expiration setting for disk cache of this image task. + /// + /// By default, the underlying `DiskStorage.Backend` uses the expiration in its config for all items. If set, + /// the `DiskStorage.Backend` will use this value to overwrite the config setting for this caching item. + /// + /// - Parameter expiration: The expiration setting used in cache storage. + /// - Returns: A `Self` value with changes applied. + public func diskCacheExpiration(_ expiration: StorageExpiration?) -> Self { + options.diskCacheExpiration = expiration + return self + } + + /// Sets the expiration extending setting for disk cache. The item expiration time will be incremented by this + /// value after access. + /// + /// By default, the underlying `DiskStorage.Backend` uses the initial cache expiration as extending + /// value: .cacheTime. + /// + /// To disable extending option at all, sets `.none` to it. + /// + /// - Parameter extending: The expiration extending setting used in cache storage. + /// - Returns: A `Self` value with changes applied. + public func diskCacheAccessExtending(_ extending: ExpirationExtending) -> Self { + options.diskCacheAccessExtendingExpiration = extending + return self + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/Kingfisher.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/Kingfisher.swift new file mode 100644 index 0000000..f875e2a --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/Kingfisher.swift @@ -0,0 +1,106 @@ +// +// Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 16/9/14. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import ImageIO + +#if os(macOS) +import AppKit +public typealias KFCrossPlatformImage = NSImage +public typealias KFCrossPlatformView = NSView +public typealias KFCrossPlatformColor = NSColor +public typealias KFCrossPlatformImageView = NSImageView +public typealias KFCrossPlatformButton = NSButton +#else +import UIKit +public typealias KFCrossPlatformImage = UIImage +public typealias KFCrossPlatformColor = UIColor +#if !os(watchOS) +public typealias KFCrossPlatformImageView = UIImageView +public typealias KFCrossPlatformView = UIView +public typealias KFCrossPlatformButton = UIButton +#if canImport(TVUIKit) +import TVUIKit +#endif +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +import CarPlay +#endif +#else +import WatchKit +#endif +#endif + +/// Wrapper for Kingfisher compatible types. This type provides an extension point for +/// convenience methods in Kingfisher. +public struct KingfisherWrapper { + public let base: Base + public init(_ base: Base) { + self.base = base + } +} + +/// Represents an object type that is compatible with Kingfisher. You can use `kf` property to get a +/// value in the namespace of Kingfisher. +public protocol KingfisherCompatible: AnyObject { } + +/// Represents a value type that is compatible with Kingfisher. You can use `kf` property to get a +/// value in the namespace of Kingfisher. +public protocol KingfisherCompatibleValue {} + +extension KingfisherCompatible { + /// Gets a namespace holder for Kingfisher compatible types. + public var kf: KingfisherWrapper { + get { return KingfisherWrapper(self) } + set { } + } +} + +extension KingfisherCompatibleValue { + /// Gets a namespace holder for Kingfisher compatible types. + public var kf: KingfisherWrapper { + get { return KingfisherWrapper(self) } + set { } + } +} + +extension KFCrossPlatformImage: KingfisherCompatible { } +#if !os(watchOS) +extension KFCrossPlatformImageView: KingfisherCompatible { } +extension KFCrossPlatformButton: KingfisherCompatible { } +extension NSTextAttachment: KingfisherCompatible { } +#else +extension WKInterfaceImage: KingfisherCompatible { } +#endif + +#if os(tvOS) && canImport(TVUIKit) +@available(tvOS 12.0, *) +extension TVMonogramView: KingfisherCompatible { } +#endif + +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +@available(iOS 14.0, *) +extension CPListItem: KingfisherCompatible { } +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KingfisherError.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KingfisherError.swift new file mode 100644 index 0000000..83d20a1 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KingfisherError.swift @@ -0,0 +1,461 @@ +// +// KingfisherError.swift +// Kingfisher +// +// Created by onevcat on 2018/09/26. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +extension Never {} + +/// Represents all the errors which can happen in Kingfisher framework. +/// Kingfisher related methods always throw a `KingfisherError` or invoke the callback with `KingfisherError` +/// as its error type. To handle errors from Kingfisher, you switch over the error to get a reason catalog, +/// then switch over the reason to know error detail. +public enum KingfisherError: Error { + + // MARK: Error Reason Types + + /// Represents the error reason during networking request phase. + /// + /// - emptyRequest: The request is empty. Code 1001. + /// - invalidURL: The URL of request is invalid. Code 1002. + /// - taskCancelled: The downloading task is cancelled by user. Code 1003. + public enum RequestErrorReason { + + /// The request is empty. Code 1001. + case emptyRequest + + /// The URL of request is invalid. Code 1002. + /// - request: The request is tend to be sent but its URL is invalid. + case invalidURL(request: URLRequest) + + /// The downloading task is cancelled by user. Code 1003. + /// - task: The session data task which is cancelled. + /// - token: The cancel token which is used for cancelling the task. + case taskCancelled(task: SessionDataTask, token: SessionDataTask.CancelToken) + } + + /// Represents the error reason during networking response phase. + /// + /// - invalidURLResponse: The response is not a valid URL response. Code 2001. + /// - invalidHTTPStatusCode: The response contains an invalid HTTP status code. Code 2002. + /// - URLSessionError: An error happens in the system URL session. Code 2003. + /// - dataModifyingFailed: Data modifying fails on returning a valid data. Code 2004. + /// - noURLResponse: The task is done but no URL response found. Code 2005. + public enum ResponseErrorReason { + + /// The response is not a valid URL response. Code 2001. + /// - response: The received invalid URL response. + /// The response is expected to be an HTTP response, but it is not. + case invalidURLResponse(response: URLResponse) + + /// The response contains an invalid HTTP status code. Code 2002. + /// - Note: + /// By default, status code 200..<400 is recognized as valid. You can override + /// this behavior by conforming to the `ImageDownloaderDelegate`. + /// - response: The received response. + case invalidHTTPStatusCode(response: HTTPURLResponse) + + /// An error happens in the system URL session. Code 2003. + /// - error: The underlying URLSession error object. + case URLSessionError(error: Error) + + /// Data modifying fails on returning a valid data. Code 2004. + /// - task: The failed task. + case dataModifyingFailed(task: SessionDataTask) + + /// The task is done but no URL response found. Code 2005. + /// - task: The failed task. + case noURLResponse(task: SessionDataTask) + } + + /// Represents the error reason during Kingfisher caching system. + /// + /// - fileEnumeratorCreationFailed: Cannot create a file enumerator for a certain disk URL. Code 3001. + /// - invalidFileEnumeratorContent: Cannot get correct file contents from a file enumerator. Code 3002. + /// - invalidURLResource: The file at target URL exists, but its URL resource is unavailable. Code 3003. + /// - cannotLoadDataFromDisk: The file at target URL exists, but the data cannot be loaded from it. Code 3004. + /// - cannotCreateDirectory: Cannot create a folder at a given path. Code 3005. + /// - imageNotExisting: The requested image does not exist in cache. Code 3006. + /// - cannotConvertToData: Cannot convert an object to data for storing. Code 3007. + /// - cannotSerializeImage: Cannot serialize an image to data for storing. Code 3008. + /// - cannotCreateCacheFile: Cannot create the cache file at a certain fileURL under a key. Code 3009. + /// - cannotSetCacheFileAttribute: Cannot set file attributes to a cached file. Code 3010. + public enum CacheErrorReason { + + /// Cannot create a file enumerator for a certain disk URL. Code 3001. + /// - url: The target disk URL from which the file enumerator should be created. + case fileEnumeratorCreationFailed(url: URL) + + /// Cannot get correct file contents from a file enumerator. Code 3002. + /// - url: The target disk URL from which the content of a file enumerator should be got. + case invalidFileEnumeratorContent(url: URL) + + /// The file at target URL exists, but its URL resource is unavailable. Code 3003. + /// - error: The underlying error thrown by file manager. + /// - key: The key used to getting the resource from cache. + /// - url: The disk URL where the target cached file exists. + case invalidURLResource(error: Error, key: String, url: URL) + + /// The file at target URL exists, but the data cannot be loaded from it. Code 3004. + /// - url: The disk URL where the target cached file exists. + /// - error: The underlying error which describes why this error happens. + case cannotLoadDataFromDisk(url: URL, error: Error) + + /// Cannot create a folder at a given path. Code 3005. + /// - path: The disk path where the directory creating operation fails. + /// - error: The underlying error which describes why this error happens. + case cannotCreateDirectory(path: String, error: Error) + + /// The requested image does not exist in cache. Code 3006. + /// - key: Key of the requested image in cache. + case imageNotExisting(key: String) + + /// Cannot convert an object to data for storing. Code 3007. + /// - object: The object which needs be convert to data. + case cannotConvertToData(object: Any, error: Error) + + /// Cannot serialize an image to data for storing. Code 3008. + /// - image: The input image needs to be serialized to cache. + /// - original: The original image data, if exists. + /// - serializer: The `CacheSerializer` used for the image serializing. + case cannotSerializeImage(image: KFCrossPlatformImage?, original: Data?, serializer: CacheSerializer) + + /// Cannot create the cache file at a certain fileURL under a key. Code 3009. + /// - fileURL: The url where the cache file should be created. + /// - key: The cache key used for the cache. When caching a file through `KingfisherManager` and Kingfisher's + /// extension method, it is the resolved cache key based on your input `Source` and the image processors. + /// - data: The data to be cached. + /// - error: The underlying error originally thrown by Foundation when writing the `data` to the disk file at + /// `fileURL`. + case cannotCreateCacheFile(fileURL: URL, key: String, data: Data, error: Error) + + /// Cannot set file attributes to a cached file. Code 3010. + /// - filePath: The path of target cache file. + /// - attributes: The file attribute to be set to the target file. + /// - error: The underlying error originally thrown by Foundation when setting the `attributes` to the disk + /// file at `filePath`. + case cannotSetCacheFileAttribute(filePath: String, attributes: [FileAttributeKey : Any], error: Error) + + /// The disk storage of cache is not ready. Code 3011. + /// + /// This is usually due to extremely lack of space on disk storage, and + /// Kingfisher failed even when creating the cache folder. The disk storage will be in unusable state. Normally, + /// ask user to free some spaces and restart the app to make the disk storage work again. + /// - cacheURL: The intended URL which should be the storage folder. + case diskStorageIsNotReady(cacheURL: URL) + } + + + /// Represents the error reason during image processing phase. + /// + /// - processingFailed: Image processing fails. There is no valid output image from the processor. Code 4001. + public enum ProcessorErrorReason { + /// Image processing fails. There is no valid output image from the processor. Code 4001. + /// - processor: The `ImageProcessor` used to process the image or its data in `item`. + /// - item: The image or its data content. + case processingFailed(processor: ImageProcessor, item: ImageProcessItem) + } + + /// Represents the error reason during image setting in a view related class. + /// + /// - emptySource: The input resource is empty or `nil`. Code 5001. + /// - notCurrentSourceTask: The source task is finished, but it is not the one expected now. Code 5002. + /// - dataProviderError: An error happens during getting data from an `ImageDataProvider`. Code 5003. + public enum ImageSettingErrorReason { + + /// The input resource is empty or `nil`. Code 5001. + case emptySource + + /// The resource task is finished, but it is not the one expected now. This usually happens when you set another + /// resource on the view without cancelling the current on-going one. The previous setting task will fail with + /// this `.notCurrentSourceTask` error when a result got, regardless of it being successful or not for that task. + /// The result of this original task is contained in the associated value. + /// Code 5002. + /// - result: The `RetrieveImageResult` if the source task is finished without problem. `nil` if an error + /// happens. + /// - error: The `Error` if an issue happens during image setting task. `nil` if the task finishes without + /// problem. + /// - source: The original source value of the task. + case notCurrentSourceTask(result: RetrieveImageResult?, error: Error?, source: Source) + + /// An error happens during getting data from an `ImageDataProvider`. Code 5003. + case dataProviderError(provider: ImageDataProvider, error: Error) + + /// No more alternative `Source` can be used in current loading process. It means that the + /// `.alternativeSources` are used and Kingfisher tried to recovery from the original error, but still + /// fails for all the given alternative sources. The associated value holds all the errors encountered during + /// the load process, including the original source loading error and all the alternative sources errors. + /// Code 5004. + case alternativeSourcesExhausted([PropagationError]) + } + + // MARK: Member Cases + + /// Represents the error reason during networking request phase. + case requestError(reason: RequestErrorReason) + /// Represents the error reason during networking response phase. + case responseError(reason: ResponseErrorReason) + /// Represents the error reason during Kingfisher caching system. + case cacheError(reason: CacheErrorReason) + /// Represents the error reason during image processing phase. + case processorError(reason: ProcessorErrorReason) + /// Represents the error reason during image setting in a view related class. + case imageSettingError(reason: ImageSettingErrorReason) + + // MARK: Helper Properties & Methods + + /// Helper property to check whether this error is a `RequestErrorReason.taskCancelled` or not. + public var isTaskCancelled: Bool { + if case .requestError(reason: .taskCancelled) = self { + return true + } + return false + } + + /// Helper method to check whether this error is a `ResponseErrorReason.invalidHTTPStatusCode` and the + /// associated value is a given status code. + /// + /// - Parameter code: The given status code. + /// - Returns: If `self` is a `ResponseErrorReason.invalidHTTPStatusCode` error + /// and its status code equals to `code`, `true` is returned. Otherwise, `false`. + public func isInvalidResponseStatusCode(_ code: Int) -> Bool { + if case .responseError(reason: .invalidHTTPStatusCode(let response)) = self { + return response.statusCode == code + } + return false + } + + public var isInvalidResponseStatusCode: Bool { + if case .responseError(reason: .invalidHTTPStatusCode) = self { + return true + } + return false + } + + /// Helper property to check whether this error is a `ImageSettingErrorReason.notCurrentSourceTask` or not. + /// When a new image setting task starts while the old one is still running, the new task identifier will be + /// set and the old one is overwritten. A `.notCurrentSourceTask` error will be raised when the old task finishes + /// to let you know the setting process finishes with a certain result, but the image view or button is not set. + public var isNotCurrentTask: Bool { + if case .imageSettingError(reason: .notCurrentSourceTask(_, _, _)) = self { + return true + } + return false + } + + var isLowDataModeConstrained: Bool { + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *), + case .responseError(reason: .URLSessionError(let sessionError)) = self, + let urlError = sessionError as? URLError, + urlError.networkUnavailableReason == .constrained + { + return true + } + return false + } + +} + +// MARK: - LocalizedError Conforming +extension KingfisherError: LocalizedError { + + /// A localized message describing what error occurred. + public var errorDescription: String? { + switch self { + case .requestError(let reason): return reason.errorDescription + case .responseError(let reason): return reason.errorDescription + case .cacheError(let reason): return reason.errorDescription + case .processorError(let reason): return reason.errorDescription + case .imageSettingError(let reason): return reason.errorDescription + } + } +} + + +// MARK: - CustomNSError Conforming +extension KingfisherError: CustomNSError { + + /// The error domain of `KingfisherError`. All errors from Kingfisher is under this domain. + public static let domain = "com.onevcat.Kingfisher.Error" + + /// The error code within the given domain. + public var errorCode: Int { + switch self { + case .requestError(let reason): return reason.errorCode + case .responseError(let reason): return reason.errorCode + case .cacheError(let reason): return reason.errorCode + case .processorError(let reason): return reason.errorCode + case .imageSettingError(let reason): return reason.errorCode + } + } +} + +extension KingfisherError.RequestErrorReason { + var errorDescription: String? { + switch self { + case .emptyRequest: + return "The request is empty or `nil`." + case .invalidURL(let request): + return "The request contains an invalid or empty URL. Request: \(request)." + case .taskCancelled(let task, let token): + return "The session task was cancelled. Task: \(task), cancel token: \(token)." + } + } + + var errorCode: Int { + switch self { + case .emptyRequest: return 1001 + case .invalidURL: return 1002 + case .taskCancelled: return 1003 + } + } +} + +extension KingfisherError.ResponseErrorReason { + var errorDescription: String? { + switch self { + case .invalidURLResponse(let response): + return "The URL response is invalid: \(response)" + case .invalidHTTPStatusCode(let response): + return "The HTTP status code in response is invalid. Code: \(response.statusCode), response: \(response)." + case .URLSessionError(let error): + return "A URL session error happened. The underlying error: \(error)" + case .dataModifyingFailed(let task): + return "The data modifying delegate returned `nil` for the downloaded data. Task: \(task)." + case .noURLResponse(let task): + return "No URL response received. Task: \(task)," + } + } + + var errorCode: Int { + switch self { + case .invalidURLResponse: return 2001 + case .invalidHTTPStatusCode: return 2002 + case .URLSessionError: return 2003 + case .dataModifyingFailed: return 2004 + case .noURLResponse: return 2005 + } + } +} + +extension KingfisherError.CacheErrorReason { + var errorDescription: String? { + switch self { + case .fileEnumeratorCreationFailed(let url): + return "Cannot create file enumerator for URL: \(url)." + case .invalidFileEnumeratorContent(let url): + return "Cannot get contents from the file enumerator at URL: \(url)." + case .invalidURLResource(let error, let key, let url): + return "Cannot get URL resource values or data for the given URL: \(url). " + + "Cache key: \(key). Underlying error: \(error)" + case .cannotLoadDataFromDisk(let url, let error): + return "Cannot load data from disk at URL: \(url). Underlying error: \(error)" + case .cannotCreateDirectory(let path, let error): + return "Cannot create directory at given path: Path: \(path). Underlying error: \(error)" + case .imageNotExisting(let key): + return "The image is not in cache, but you requires it should only be " + + "from cache by enabling the `.onlyFromCache` option. Key: \(key)." + case .cannotConvertToData(let object, let error): + return "Cannot convert the input object to a `Data` object when storing it to disk cache. " + + "Object: \(object). Underlying error: \(error)" + case .cannotSerializeImage(let image, let originalData, let serializer): + return "Cannot serialize an image due to the cache serializer returning `nil`. " + + "Image: \(String(describing:image)), original data: \(String(describing: originalData)), " + + "serializer: \(serializer)." + case .cannotCreateCacheFile(let fileURL, let key, let data, let error): + return "Cannot create cache file at url: \(fileURL), key: \(key), data length: \(data.count). " + + "Underlying foundation error: \(error)." + case .cannotSetCacheFileAttribute(let filePath, let attributes, let error): + return "Cannot set file attribute for the cache file at path: \(filePath), attributes: \(attributes)." + + "Underlying foundation error: \(error)." + case .diskStorageIsNotReady(let cacheURL): + return "The disk storage is not ready to use yet at URL: '\(cacheURL)'. " + + "This is usually caused by extremely lack of disk space. Ask users to free up some space and restart the app." + } + } + + var errorCode: Int { + switch self { + case .fileEnumeratorCreationFailed: return 3001 + case .invalidFileEnumeratorContent: return 3002 + case .invalidURLResource: return 3003 + case .cannotLoadDataFromDisk: return 3004 + case .cannotCreateDirectory: return 3005 + case .imageNotExisting: return 3006 + case .cannotConvertToData: return 3007 + case .cannotSerializeImage: return 3008 + case .cannotCreateCacheFile: return 3009 + case .cannotSetCacheFileAttribute: return 3010 + case .diskStorageIsNotReady: return 3011 + } + } +} + +extension KingfisherError.ProcessorErrorReason { + var errorDescription: String? { + switch self { + case .processingFailed(let processor, let item): + return "Processing image failed. Processor: \(processor). Processing item: \(item)." + } + } + + var errorCode: Int { + switch self { + case .processingFailed: return 4001 + } + } +} + +extension KingfisherError.ImageSettingErrorReason { + var errorDescription: String? { + switch self { + case .emptySource: + return "The input resource is empty." + case .notCurrentSourceTask(let result, let error, let resource): + if let result = result { + return "Retrieving resource succeeded, but this source is " + + "not the one currently expected. Result: \(result). Resource: \(resource)." + } else if let error = error { + return "Retrieving resource failed, and this resource is " + + "not the one currently expected. Error: \(error). Resource: \(resource)." + } else { + return nil + } + case .dataProviderError(let provider, let error): + return "Image data provider fails to provide data. Provider: \(provider), error: \(error)" + case .alternativeSourcesExhausted(let errors): + return "Image setting from alternaive sources failed: \(errors)" + } + } + + var errorCode: Int { + switch self { + case .emptySource: return 5001 + case .notCurrentSourceTask: return 5002 + case .dataProviderError: return 5003 + case .alternativeSourcesExhausted: return 5004 + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KingfisherManager.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KingfisherManager.swift new file mode 100644 index 0000000..dd4c315 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KingfisherManager.swift @@ -0,0 +1,739 @@ +// +// KingfisherManager.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +import Foundation + +/// The downloading progress block type. +/// The parameter value is the `receivedSize` of current response. +/// The second parameter is the total expected data length from response's "Content-Length" header. +/// If the expected length is not available, this block will not be called. +public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> Void) + +/// Represents the result of a Kingfisher retrieving image task. +public struct RetrieveImageResult { + + /// Gets the image object of this result. + public let image: KFCrossPlatformImage + + /// Gets the cache source of the image. It indicates from which layer of cache this image is retrieved. + /// If the image is just downloaded from network, `.none` will be returned. + public let cacheType: CacheType + + /// The `Source` which this result is related to. This indicated where the `image` of `self` is referring. + public let source: Source + + /// The original `Source` from which the retrieve task begins. It can be different from the `source` property. + /// When an alternative source loading happened, the `source` will be the replacing loading target, while the + /// `originalSource` will be kept as the initial `source` which issued the image loading process. + public let originalSource: Source +} + +/// A struct that stores some related information of an `KingfisherError`. It provides some context information for +/// a pure error so you can identify the error easier. +public struct PropagationError { + + /// The `Source` to which current `error` is bound. + public let source: Source + + /// The actual error happens in framework. + public let error: KingfisherError +} + + +/// The downloading task updated block type. The parameter `newTask` is the updated new task of image setting process. +/// It is a `nil` if the image loading does not require an image downloading process. If an image downloading is issued, +/// this value will contain the actual `DownloadTask` for you to keep and cancel it later if you need. +public typealias DownloadTaskUpdatedBlock = ((_ newTask: DownloadTask?) -> Void) + +/// Main manager class of Kingfisher. It connects Kingfisher downloader and cache, +/// to provide a set of convenience methods to use Kingfisher for tasks. +/// You can use this class to retrieve an image via a specified URL from web or cache. +public class KingfisherManager { + + /// Represents a shared manager used across Kingfisher. + /// Use this instance for getting or storing images with Kingfisher. + public static let shared = KingfisherManager() + + // Mark: Public Properties + /// The `ImageCache` used by this manager. It is `ImageCache.default` by default. + /// If a cache is specified in `KingfisherManager.defaultOptions`, the value in `defaultOptions` will be + /// used instead. + public var cache: ImageCache + + /// The `ImageDownloader` used by this manager. It is `ImageDownloader.default` by default. + /// If a downloader is specified in `KingfisherManager.defaultOptions`, the value in `defaultOptions` will be + /// used instead. + public var downloader: ImageDownloader + + /// Default options used by the manager. This option will be used in + /// Kingfisher manager related methods, as well as all view extension methods. + /// You can also passing other options for each image task by sending an `options` parameter + /// to Kingfisher's APIs. The per image options will overwrite the default ones, + /// if the option exists in both. + public var defaultOptions = KingfisherOptionsInfo.empty + + // Use `defaultOptions` to overwrite the `downloader` and `cache`. + private var currentDefaultOptions: KingfisherOptionsInfo { + return [.downloader(downloader), .targetCache(cache)] + defaultOptions + } + + private let processingQueue: CallbackQueue + + private convenience init() { + self.init(downloader: .default, cache: .default) + } + + /// Creates an image setting manager with specified downloader and cache. + /// + /// - Parameters: + /// - downloader: The image downloader used to download images. + /// - cache: The image cache which stores memory and disk images. + public init(downloader: ImageDownloader, cache: ImageCache) { + self.downloader = downloader + self.cache = cache + + let processQueueName = "com.onevcat.Kingfisher.KingfisherManager.processQueue.\(UUID().uuidString)" + processingQueue = .dispatch(DispatchQueue(label: processQueueName)) + } + + // MARK: - Getting Images + + /// Gets an image from a given resource. + /// - Parameters: + /// - resource: The `Resource` object defines data information like key or URL. + /// - options: Options to use when creating the image. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. `progressBlock` is always called in + /// main queue. + /// - downloadTaskUpdated: Called when a new image downloading task is created for current image retrieving. This + /// usually happens when an alternative source is used to replace the original (failed) + /// task. You can update your reference of `DownloadTask` if you want to manually `cancel` + /// the new task. + /// - completionHandler: Called when the image retrieved and set finished. This completion handler will be invoked + /// from the `options.callbackQueue`. If not specified, the main queue will be used. + /// - Returns: A task represents the image downloading. If there is a download task starts for `.network` resource, + /// the started `DownloadTask` is returned. Otherwise, `nil` is returned. + /// + /// - Note: + /// This method will first check whether the requested `resource` is already in cache or not. If cached, + /// it returns `nil` and invoke the `completionHandler` after the cached image retrieved. Otherwise, it + /// will download the `resource`, store it in cache, then call `completionHandler`. + @discardableResult + public func retrieveImage( + with resource: Resource, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + return retrieveImage( + with: resource.convertToSource(), + options: options, + progressBlock: progressBlock, + downloadTaskUpdated: downloadTaskUpdated, + completionHandler: completionHandler + ) + } + + /// Gets an image from a given resource. + /// + /// - Parameters: + /// - source: The `Source` object defines data information from network or a data provider. + /// - options: Options to use when creating the image. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. `progressBlock` is always called in + /// main queue. + /// - downloadTaskUpdated: Called when a new image downloading task is created for current image retrieving. This + /// usually happens when an alternative source is used to replace the original (failed) + /// task. You can update your reference of `DownloadTask` if you want to manually `cancel` + /// the new task. + /// - completionHandler: Called when the image retrieved and set finished. This completion handler will be invoked + /// from the `options.callbackQueue`. If not specified, the main queue will be used. + /// - Returns: A task represents the image downloading. If there is a download task starts for `.network` resource, + /// the started `DownloadTask` is returned. Otherwise, `nil` is returned. + /// + /// - Note: + /// This method will first check whether the requested `source` is already in cache or not. If cached, + /// it returns `nil` and invoke the `completionHandler` after the cached image retrieved. Otherwise, it + /// will try to load the `source`, store it in cache, then call `completionHandler`. + /// + public func retrieveImage( + with source: Source, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + let options = currentDefaultOptions + (options ?? .empty) + let info = KingfisherParsedOptionsInfo(options) + return retrieveImage( + with: source, + options: info, + progressBlock: progressBlock, + downloadTaskUpdated: downloadTaskUpdated, + completionHandler: completionHandler) + } + + func retrieveImage( + with source: Source, + options: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + var info = options + if let block = progressBlock { + info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + return retrieveImage( + with: source, + options: info, + downloadTaskUpdated: downloadTaskUpdated, + completionHandler: completionHandler) + } + + func retrieveImage( + with source: Source, + options: KingfisherParsedOptionsInfo, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + let retrievingContext = RetrievingContext(options: options, originalSource: source) + var retryContext: RetryContext? + + func startNewRetrieveTask( + with source: Source, + downloadTaskUpdated: DownloadTaskUpdatedBlock? + ) { + let newTask = self.retrieveImage(with: source, context: retrievingContext) { result in + handler(currentSource: source, result: result) + } + downloadTaskUpdated?(newTask) + } + + func failCurrentSource(_ source: Source, with error: KingfisherError) { + // Skip alternative sources if the user cancelled it. + guard !error.isTaskCancelled else { + completionHandler?(.failure(error)) + return + } + // When low data mode constrained error, retry with the low data mode source instead of use alternative on fly. + guard !error.isLowDataModeConstrained else { + if let source = retrievingContext.options.lowDataModeSource { + retrievingContext.options.lowDataModeSource = nil + startNewRetrieveTask(with: source, downloadTaskUpdated: downloadTaskUpdated) + } else { + // This should not happen. + completionHandler?(.failure(error)) + } + return + } + if let nextSource = retrievingContext.popAlternativeSource() { + retrievingContext.appendError(error, to: source) + startNewRetrieveTask(with: nextSource, downloadTaskUpdated: downloadTaskUpdated) + } else { + // No other alternative source. Finish with error. + if retrievingContext.propagationErrors.isEmpty { + completionHandler?(.failure(error)) + } else { + retrievingContext.appendError(error, to: source) + let finalError = KingfisherError.imageSettingError( + reason: .alternativeSourcesExhausted(retrievingContext.propagationErrors) + ) + completionHandler?(.failure(finalError)) + } + } + } + + func handler(currentSource: Source, result: (Result)) -> Void { + switch result { + case .success: + completionHandler?(result) + case .failure(let error): + if let retryStrategy = options.retryStrategy { + let context = retryContext?.increaseRetryCount() ?? RetryContext(source: source, error: error) + retryContext = context + + retryStrategy.retry(context: context) { decision in + switch decision { + case .retry(let userInfo): + retryContext?.userInfo = userInfo + startNewRetrieveTask(with: source, downloadTaskUpdated: downloadTaskUpdated) + case .stop: + failCurrentSource(currentSource, with: error) + } + } + } else { + failCurrentSource(currentSource, with: error) + } + } + } + + return retrieveImage( + with: source, + context: retrievingContext) + { + result in + handler(currentSource: source, result: result) + } + + } + + private func retrieveImage( + with source: Source, + context: RetrievingContext, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + let options = context.options + if options.forceRefresh { + return loadAndCacheImage( + source: source, + context: context, + completionHandler: completionHandler)?.value + + } else { + let loadedFromCache = retrieveImageFromCache( + source: source, + context: context, + completionHandler: completionHandler) + + if loadedFromCache { + return nil + } + + if options.onlyFromCache { + let error = KingfisherError.cacheError(reason: .imageNotExisting(key: source.cacheKey)) + completionHandler?(.failure(error)) + return nil + } + + return loadAndCacheImage( + source: source, + context: context, + completionHandler: completionHandler)?.value + } + } + + func provideImage( + provider: ImageDataProvider, + options: KingfisherParsedOptionsInfo, + completionHandler: ((Result) -> Void)?) + { + guard let completionHandler = completionHandler else { return } + provider.data { result in + switch result { + case .success(let data): + (options.processingQueue ?? self.processingQueue).execute { + let processor = options.processor + let processingItem = ImageProcessItem.data(data) + guard let image = processor.process(item: processingItem, options: options) else { + options.callbackQueue.execute { + let error = KingfisherError.processorError( + reason: .processingFailed(processor: processor, item: processingItem)) + completionHandler(.failure(error)) + } + return + } + + options.callbackQueue.execute { + let result = ImageLoadingResult(image: image, url: nil, originalData: data) + completionHandler(.success(result)) + } + } + case .failure(let error): + options.callbackQueue.execute { + let error = KingfisherError.imageSettingError( + reason: .dataProviderError(provider: provider, error: error)) + completionHandler(.failure(error)) + } + + } + } + } + + private func cacheImage( + source: Source, + options: KingfisherParsedOptionsInfo, + context: RetrievingContext, + result: Result, + completionHandler: ((Result) -> Void)? + ) + { + switch result { + case .success(let value): + let needToCacheOriginalImage = options.cacheOriginalImage && + options.processor != DefaultImageProcessor.default + let coordinator = CacheCallbackCoordinator( + shouldWaitForCache: options.waitForCache, shouldCacheOriginal: needToCacheOriginalImage) + let result = RetrieveImageResult( + image: options.imageModifier?.modify(value.image) ?? value.image, + cacheType: .none, + source: source, + originalSource: context.originalSource + ) + // Add image to cache. + let targetCache = options.targetCache ?? self.cache + targetCache.store( + value.image, + original: value.originalData, + forKey: source.cacheKey, + options: options, + toDisk: !options.cacheMemoryOnly) + { + _ in + coordinator.apply(.cachingImage) { + completionHandler?(.success(result)) + } + } + + // Add original image to cache if necessary. + + if needToCacheOriginalImage { + let originalCache = options.originalCache ?? targetCache + originalCache.storeToDisk( + value.originalData, + forKey: source.cacheKey, + processorIdentifier: DefaultImageProcessor.default.identifier, + expiration: options.diskCacheExpiration) + { + _ in + coordinator.apply(.cachingOriginalImage) { + completionHandler?(.success(result)) + } + } + } + + coordinator.apply(.cacheInitiated) { + completionHandler?(.success(result)) + } + + case .failure(let error): + completionHandler?(.failure(error)) + } + } + + @discardableResult + func loadAndCacheImage( + source: Source, + context: RetrievingContext, + completionHandler: ((Result) -> Void)?) -> DownloadTask.WrappedTask? + { + let options = context.options + func _cacheImage(_ result: Result) { + cacheImage( + source: source, + options: options, + context: context, + result: result, + completionHandler: completionHandler + ) + } + + switch source { + case .network(let resource): + let downloader = options.downloader ?? self.downloader + let task = downloader.downloadImage( + with: resource.downloadURL, options: options, completionHandler: _cacheImage + ) + + + // The code below is neat, but it fails the Swift 5.2 compiler with a runtime crash when + // `BUILD_LIBRARY_FOR_DISTRIBUTION` is turned on. I believe it is a bug in the compiler. + // Let's fallback to a traditional style before it can be fixed in Swift. + // + // https://github.com/onevcat/Kingfisher/issues/1436 + // + // return task.map(DownloadTask.WrappedTask.download) + + if let task = task { + return .download(task) + } else { + return nil + } + + case .provider(let provider): + provideImage(provider: provider, options: options, completionHandler: _cacheImage) + return .dataProviding + } + } + + /// Retrieves image from memory or disk cache. + /// + /// - Parameters: + /// - source: The target source from which to get image. + /// - key: The key to use when caching the image. + /// - url: Image request URL. This is not used when retrieving image from cache. It is just used for + /// `RetrieveImageResult` callback compatibility. + /// - options: Options on how to get the image from image cache. + /// - completionHandler: Called when the image retrieving finishes, either with succeeded + /// `RetrieveImageResult` or an error. + /// - Returns: `true` if the requested image or the original image before being processed is existing in cache. + /// Otherwise, this method returns `false`. + /// + /// - Note: + /// The image retrieving could happen in either memory cache or disk cache. The `.processor` option in + /// `options` will be considered when searching in the cache. If no processed image is found, Kingfisher + /// will try to check whether an original version of that image is existing or not. If there is already an + /// original, Kingfisher retrieves it from cache and processes it. Then, the processed image will be store + /// back to cache for later use. + func retrieveImageFromCache( + source: Source, + context: RetrievingContext, + completionHandler: ((Result) -> Void)?) -> Bool + { + let options = context.options + // 1. Check whether the image was already in target cache. If so, just get it. + let targetCache = options.targetCache ?? cache + let key = source.cacheKey + let targetImageCached = targetCache.imageCachedType( + forKey: key, processorIdentifier: options.processor.identifier) + + let validCache = targetImageCached.cached && + (options.fromMemoryCacheOrRefresh == false || targetImageCached == .memory) + if validCache { + targetCache.retrieveImage(forKey: key, options: options) { result in + guard let completionHandler = completionHandler else { return } + options.callbackQueue.execute { + result.match( + onSuccess: { cacheResult in + let value: Result + if var image = cacheResult.image { + if image.kf.imageFrameCount != nil && image.kf.imageFrameCount != 1, let data = image.kf.animatedImageData { + // Always recreate animated image representation since it is possible to be loaded in different options. + // https://github.com/onevcat/Kingfisher/issues/1923 + image = KingfisherWrapper.animatedImage(data: data, options: options.imageCreatingOptions) ?? .init() + } + if let modifier = options.imageModifier { + image = modifier.modify(image) + } + value = result.map { + RetrieveImageResult( + image: image, + cacheType: $0.cacheType, + source: source, + originalSource: context.originalSource + ) + } + } else { + value = .failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))) + } + completionHandler(value) + }, + onFailure: { _ in + completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key)))) + } + ) + } + } + return true + } + + // 2. Check whether the original image exists. If so, get it, process it, save to storage and return. + let originalCache = options.originalCache ?? targetCache + // No need to store the same file in the same cache again. + if originalCache === targetCache && options.processor == DefaultImageProcessor.default { + return false + } + + // Check whether the unprocessed image existing or not. + let originalImageCacheType = originalCache.imageCachedType( + forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier) + let canAcceptDiskCache = !options.fromMemoryCacheOrRefresh + + let canUseOriginalImageCache = + (canAcceptDiskCache && originalImageCacheType.cached) || + (!canAcceptDiskCache && originalImageCacheType == .memory) + + if canUseOriginalImageCache { + // Now we are ready to get found the original image from cache. We need the unprocessed image, so remove + // any processor from options first. + var optionsWithoutProcessor = options + optionsWithoutProcessor.processor = DefaultImageProcessor.default + originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { result in + + result.match( + onSuccess: { cacheResult in + guard let image = cacheResult.image else { + assertionFailure("The image (under key: \(key) should be existing in the original cache.") + return + } + + let processor = options.processor + (options.processingQueue ?? self.processingQueue).execute { + let item = ImageProcessItem.image(image) + guard let processedImage = processor.process(item: item, options: options) else { + let error = KingfisherError.processorError( + reason: .processingFailed(processor: processor, item: item)) + options.callbackQueue.execute { completionHandler?(.failure(error)) } + return + } + + var cacheOptions = options + cacheOptions.callbackQueue = .untouch + + let coordinator = CacheCallbackCoordinator( + shouldWaitForCache: options.waitForCache, shouldCacheOriginal: false) + + let result = RetrieveImageResult( + image: options.imageModifier?.modify(processedImage) ?? processedImage, + cacheType: .none, + source: source, + originalSource: context.originalSource + ) + + targetCache.store( + processedImage, + forKey: key, + options: cacheOptions, + toDisk: !options.cacheMemoryOnly) + { + _ in + coordinator.apply(.cachingImage) { + options.callbackQueue.execute { completionHandler?(.success(result)) } + } + } + + coordinator.apply(.cacheInitiated) { + options.callbackQueue.execute { completionHandler?(.success(result)) } + } + } + }, + onFailure: { _ in + // This should not happen actually, since we already confirmed `originalImageCached` is `true`. + // Just in case... + options.callbackQueue.execute { + completionHandler?( + .failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))) + ) + } + } + ) + } + return true + } + + return false + } +} + +class RetrievingContext { + + var options: KingfisherParsedOptionsInfo + + let originalSource: Source + var propagationErrors: [PropagationError] = [] + + init(options: KingfisherParsedOptionsInfo, originalSource: Source) { + self.originalSource = originalSource + self.options = options + } + + func popAlternativeSource() -> Source? { + guard var alternativeSources = options.alternativeSources, !alternativeSources.isEmpty else { + return nil + } + let nextSource = alternativeSources.removeFirst() + options.alternativeSources = alternativeSources + return nextSource + } + + @discardableResult + func appendError(_ error: KingfisherError, to source: Source) -> [PropagationError] { + let item = PropagationError(source: source, error: error) + propagationErrors.append(item) + return propagationErrors + } +} + +class CacheCallbackCoordinator { + + enum State { + case idle + case imageCached + case originalImageCached + case done + } + + enum Action { + case cacheInitiated + case cachingImage + case cachingOriginalImage + } + + private let shouldWaitForCache: Bool + private let shouldCacheOriginal: Bool + private let stateQueue: DispatchQueue + private var threadSafeState: State = .idle + + private (set) var state: State { + set { stateQueue.sync { threadSafeState = newValue } } + get { stateQueue.sync { threadSafeState } } + } + + init(shouldWaitForCache: Bool, shouldCacheOriginal: Bool) { + self.shouldWaitForCache = shouldWaitForCache + self.shouldCacheOriginal = shouldCacheOriginal + let stateQueueName = "com.onevcat.Kingfisher.CacheCallbackCoordinator.stateQueue.\(UUID().uuidString)" + self.stateQueue = DispatchQueue(label: stateQueueName) + } + + func apply(_ action: Action, trigger: () -> Void) { + switch (state, action) { + case (.done, _): + break + + // From .idle + case (.idle, .cacheInitiated): + if !shouldWaitForCache { + state = .done + trigger() + } + case (.idle, .cachingImage): + if shouldCacheOriginal { + state = .imageCached + } else { + state = .done + trigger() + } + case (.idle, .cachingOriginalImage): + state = .originalImageCached + + // From .imageCached + case (.imageCached, .cachingOriginalImage): + state = .done + trigger() + + // From .originalImageCached + case (.originalImageCached, .cachingImage): + state = .done + trigger() + + default: + assertionFailure("This case should not happen in CacheCallbackCoordinator: \(state) - \(action)") + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift new file mode 100644 index 0000000..5f2aea6 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift @@ -0,0 +1,400 @@ +// +// KingfisherOptionsInfo.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/23. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + + +/// KingfisherOptionsInfo is a typealias for [KingfisherOptionsInfoItem]. +/// You can use the enum of option item with value to control some behaviors of Kingfisher. +public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem] + +extension Array where Element == KingfisherOptionsInfoItem { + static let empty: KingfisherOptionsInfo = [] +} + +/// Represents the available option items could be used in `KingfisherOptionsInfo`. +public enum KingfisherOptionsInfoItem { + + /// Kingfisher will use the associated `ImageCache` object when handling related operations, + /// including trying to retrieve the cached images and store the downloaded image to it. + case targetCache(ImageCache) + + /// The `ImageCache` for storing and retrieving original images. If `originalCache` is + /// contained in the options, it will be preferred for storing and retrieving original images. + /// If there is no `.originalCache` in the options, `.targetCache` will be used to store original images. + /// + /// When using KingfisherManager to download and store an image, if `cacheOriginalImage` is + /// applied in the option, the original image will be stored to this `originalCache`. At the + /// same time, if a requested final image (with processor applied) cannot be found in `targetCache`, + /// Kingfisher will try to search the original image to check whether it is already there. If found, + /// it will be used and applied with the given processor. It is an optimization for not downloading + /// the same image for multiple times. + case originalCache(ImageCache) + + /// Kingfisher will use the associated `ImageDownloader` object to download the requested images. + case downloader(ImageDownloader) + + /// Member for animation transition when using `UIImageView`. Kingfisher will use the `ImageTransition` of + /// this enum to animate the image in if it is downloaded from web. The transition will not happen when the + /// image is retrieved from either memory or disk cache by default. If you need to do the transition even when + /// the image being retrieved from cache, set `.forceRefresh` as well. + case transition(ImageTransition) + + /// Associated `Float` value will be set as the priority of image download task. The value for it should be + /// between 0.0~1.0. If this option not set, the default value (`URLSessionTask.defaultPriority`) will be used. + case downloadPriority(Float) + + /// If set, Kingfisher will ignore the cache and try to start a download task for the image source. + case forceRefresh + + /// If set, Kingfisher will try to retrieve the image from memory cache first. If the image is not in memory + /// cache, then it will ignore the disk cache but download the image again from network. This is useful when + /// you want to display a changeable image behind the same url at the same app session, while avoiding download + /// it for multiple times. + case fromMemoryCacheOrRefresh + + /// If set, setting the image to an image view will happen with transition even when retrieved from cache. + /// See `.transition` option for more. + case forceTransition + + /// If set, Kingfisher will only cache the value in memory but not in disk. + case cacheMemoryOnly + + /// If set, Kingfisher will wait for caching operation to be completed before calling the completion block. + case waitForCache + + /// If set, Kingfisher will only try to retrieve the image from cache, but not from network. If the image is not in + /// cache, the image retrieving will fail with the `KingfisherError.cacheError` with `.imageNotExisting` as its + /// reason. + case onlyFromCache + + /// Decode the image in background thread before using. It will decode the downloaded image data and do a off-screen + /// rendering to extract pixel information in background. This can speed up display, but will cost more time to + /// prepare the image for using. + case backgroundDecode + + /// The associated value will be used as the target queue of dispatch callbacks when retrieving images from + /// cache. If not set, Kingfisher will use `.mainCurrentOrAsync` for callbacks. + /// + /// - Note: + /// This option does not affect the callbacks for UI related extension methods. You will always get the + /// callbacks called from main queue. + case callbackQueue(CallbackQueue) + + /// The associated value will be used as the scale factor when converting retrieved data to an image. + /// Specify the image scale, instead of your screen scale. You may need to set the correct scale when you dealing + /// with 2x or 3x retina images. Otherwise, Kingfisher will convert the data to image object at `scale` 1.0. + case scaleFactor(CGFloat) + + /// Whether all the animated image data should be preloaded. Default is `false`, which means only following frames + /// will be loaded on need. If `true`, all the animated image data will be loaded and decoded into memory. + /// + /// This option is mainly used for back compatibility internally. You should not set it directly. Instead, + /// you should choose the image view class to control the GIF data loading. There are two classes in Kingfisher + /// support to display a GIF image. `AnimatedImageView` does not preload all data, it takes much less memory, but + /// uses more CPU when display. While a normal image view (`UIImageView` or `NSImageView`) loads all data at once, + /// which uses more memory but only decode image frames once. + case preloadAllAnimationData + + /// The `ImageDownloadRequestModifier` contained will be used to change the request before it being sent. + /// This is the last chance you can modify the image download request. You can modify the request for some + /// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping. + /// The original request will be sent without any modification by default. + case requestModifier(AsyncImageDownloadRequestModifier) + + /// The `ImageDownloadRedirectHandler` contained will be used to change the request before redirection. + /// This is the possibility you can modify the image download request during redirect. You can modify the request for + /// some customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url + /// mapping. + /// The original redirection request will be sent without any modification by default. + case redirectHandler(ImageDownloadRedirectHandler) + + /// Processor for processing when the downloading finishes, a processor will convert the downloaded data to an image + /// and/or apply some filter on it. If a cache is connected to the downloader (it happens when you are using + /// KingfisherManager or any of the view extension methods), the converted image will also be sent to cache as well. + /// If not set, the `DefaultImageProcessor.default` will be used. + case processor(ImageProcessor) + + /// Provides a `CacheSerializer` to convert some data to an image object for + /// retrieving from disk cache or vice versa for storing to disk cache. + /// If not set, the `DefaultCacheSerializer.default` will be used. + case cacheSerializer(CacheSerializer) + + /// An `ImageModifier` is for modifying an image as needed right before it is used. If the image was fetched + /// directly from the downloader, the modifier will run directly after the `ImageProcessor`. If the image is being + /// fetched from a cache, the modifier will run after the `CacheSerializer`. + /// + /// Use `ImageModifier` when you need to set properties that do not persist when caching the image on a concrete + /// type of `Image`, such as the `renderingMode` or the `alignmentInsets` of `UIImage`. + case imageModifier(ImageModifier) + + /// Keep the existing image of image view while setting another image to it. + /// By setting this option, the placeholder image parameter of image view extension method + /// will be ignored and the current image will be kept while loading or downloading the new image. + case keepCurrentImageWhileLoading + + /// If set, Kingfisher will only load the first frame from an animated image file as a single image. + /// Loading an animated images may take too much memory. It will be useful when you want to display a + /// static preview of the first frame from an animated image. + /// + /// This option will be ignored if the target image is not animated image data. + case onlyLoadFirstFrame + + /// If set and an `ImageProcessor` is used, Kingfisher will try to cache both the final result and original + /// image. Kingfisher will have a chance to use the original image when another processor is applied to the same + /// resource, instead of downloading it again. You can use `.originalCache` to specify a cache or the original + /// images if necessary. + /// + /// The original image will be only cached to disk storage. + case cacheOriginalImage + + /// If set and an image retrieving error occurred Kingfisher will set provided image (or empty) + /// in place of requested one. It's useful when you don't want to show placeholder + /// during loading time but wants to use some default image when requests will be failed. + case onFailureImage(KFCrossPlatformImage?) + + /// If set and used in `ImagePrefetcher`, the prefetching operation will load the images into memory storage + /// aggressively. By default this is not contained in the options, that means if the requested image is already + /// in disk cache, Kingfisher will not try to load it to memory. + case alsoPrefetchToMemory + + /// If set, the disk storage loading will happen in the same calling queue. By default, disk storage file loading + /// happens in its own queue with an asynchronous dispatch behavior. Although it provides better non-blocking disk + /// loading performance, it also causes a flickering when you reload an image from disk, if the image view already + /// has an image set. + /// + /// Set this options will stop that flickering by keeping all loading in the same queue (typically the UI queue + /// if you are using Kingfisher's extension methods to set an image), with a tradeoff of loading performance. + case loadDiskFileSynchronously + + /// Options to control the writing of data to disk storage + /// If set, options will be passed the store operation for a new files. + case diskStoreWriteOptions(Data.WritingOptions) + + /// The expiration setting for memory cache. By default, the underlying `MemoryStorage.Backend` uses the + /// expiration in its config for all items. If set, the `MemoryStorage.Backend` will use this associated + /// value to overwrite the config setting for this caching item. + case memoryCacheExpiration(StorageExpiration) + + /// The expiration extending setting for memory cache. The item expiration time will be incremented by this + /// value after access. + /// By default, the underlying `MemoryStorage.Backend` uses the initial cache expiration as extending + /// value: .cacheTime. + /// + /// To disable extending option at all add memoryCacheAccessExtendingExpiration(.none) to options. + case memoryCacheAccessExtendingExpiration(ExpirationExtending) + + /// The expiration setting for disk cache. By default, the underlying `DiskStorage.Backend` uses the + /// expiration in its config for all items. If set, the `DiskStorage.Backend` will use this associated + /// value to overwrite the config setting for this caching item. + case diskCacheExpiration(StorageExpiration) + + /// The expiration extending setting for disk cache. The item expiration time will be incremented by this value after access. + /// By default, the underlying `DiskStorage.Backend` uses the initial cache expiration as extending value: .cacheTime. + /// To disable extending option at all add diskCacheAccessExtendingExpiration(.none) to options. + case diskCacheAccessExtendingExpiration(ExpirationExtending) + + /// Decides on which queue the image processing should happen. By default, Kingfisher uses a pre-defined serial + /// queue to process images. Use this option to change this behavior. For example, specify a `.mainCurrentOrAsync` + /// to let the image be processed in main queue to prevent a possible flickering (but with a possibility of + /// blocking the UI, especially if the processor needs a lot of time to run). + case processingQueue(CallbackQueue) + + /// Enable progressive image loading, Kingfisher will use the associated `ImageProgressive` value to process the + /// progressive JPEG data and display it in a progressive way. + case progressiveJPEG(ImageProgressive) + + /// The alternative sources will be used when the original input `Source` fails. The `Source`s in the associated + /// array will be used to start a new image loading task if the previous task fails due to an error. The image + /// source loading process will stop as soon as a source is loaded successfully. If all `[Source]`s are used but + /// the loading is still failing, an `imageSettingError` with `alternativeSourcesExhausted` as its reason will be + /// thrown out. + /// + /// This option is useful if you want to implement a fallback solution for setting image. + /// + /// User cancellation will not trigger the alternative source loading. + case alternativeSources([Source]) + + /// Provide a retry strategy which will be used when something gets wrong during the image retrieving process from + /// `KingfisherManager`. You can define a strategy by create a type conforming to the `RetryStrategy` protocol. + /// + /// - Note: + /// + /// All extension methods of Kingfisher (`kf` extensions on `UIImageView` or `UIButton`) retrieve images through + /// `KingfisherManager`, so the retry strategy also applies when using them. However, this option does not apply + /// when pass to an `ImageDownloader` or `ImageCache`. + /// + case retryStrategy(RetryStrategy) + + /// The `Source` should be loaded when user enables Low Data Mode and the original source fails with an + /// `NSURLErrorNetworkUnavailableReason.constrained` error. When this option is set, the + /// `allowsConstrainedNetworkAccess` property of the request for the original source will be set to `false` and the + /// `Source` in associated value will be used to retrieve the image for low data mode. Usually, you can provide a + /// low-resolution version of your image or a local image provider to display a placeholder. + /// + /// If not set or the `source` is `nil`, the device Low Data Mode will be ignored and the original source will + /// be loaded following the system default behavior, in a normal way. + case lowDataMode(Source?) +} + +// Improve performance by parsing the input `KingfisherOptionsInfo` (self) first. +// So we can prevent the iterating over the options array again and again. +/// The parsed options info used across Kingfisher methods. Each property in this type corresponds a case member +/// in `KingfisherOptionsInfoItem`. When a `KingfisherOptionsInfo` sent to Kingfisher related methods, it will be +/// parsed and converted to a `KingfisherParsedOptionsInfo` first, and pass through the internal methods. +public struct KingfisherParsedOptionsInfo { + + public var targetCache: ImageCache? = nil + public var originalCache: ImageCache? = nil + public var downloader: ImageDownloader? = nil + public var transition: ImageTransition = .none + public var downloadPriority: Float = URLSessionTask.defaultPriority + public var forceRefresh = false + public var fromMemoryCacheOrRefresh = false + public var forceTransition = false + public var cacheMemoryOnly = false + public var waitForCache = false + public var onlyFromCache = false + public var backgroundDecode = false + public var preloadAllAnimationData = false + public var callbackQueue: CallbackQueue = .mainCurrentOrAsync + public var scaleFactor: CGFloat = 1.0 + public var requestModifier: AsyncImageDownloadRequestModifier? = nil + public var redirectHandler: ImageDownloadRedirectHandler? = nil + public var processor: ImageProcessor = DefaultImageProcessor.default + public var imageModifier: ImageModifier? = nil + public var cacheSerializer: CacheSerializer = DefaultCacheSerializer.default + public var keepCurrentImageWhileLoading = false + public var onlyLoadFirstFrame = false + public var cacheOriginalImage = false + public var onFailureImage: Optional = .none + public var alsoPrefetchToMemory = false + public var loadDiskFileSynchronously = false + public var diskStoreWriteOptions: Data.WritingOptions = [] + public var memoryCacheExpiration: StorageExpiration? = nil + public var memoryCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime + public var diskCacheExpiration: StorageExpiration? = nil + public var diskCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime + public var processingQueue: CallbackQueue? = nil + public var progressiveJPEG: ImageProgressive? = nil + public var alternativeSources: [Source]? = nil + public var retryStrategy: RetryStrategy? = nil + public var lowDataModeSource: Source? = nil + + var onDataReceived: [DataReceivingSideEffect]? = nil + + public init(_ info: KingfisherOptionsInfo?) { + guard let info = info else { return } + for option in info { + switch option { + case .targetCache(let value): targetCache = value + case .originalCache(let value): originalCache = value + case .downloader(let value): downloader = value + case .transition(let value): transition = value + case .downloadPriority(let value): downloadPriority = value + case .forceRefresh: forceRefresh = true + case .fromMemoryCacheOrRefresh: fromMemoryCacheOrRefresh = true + case .forceTransition: forceTransition = true + case .cacheMemoryOnly: cacheMemoryOnly = true + case .waitForCache: waitForCache = true + case .onlyFromCache: onlyFromCache = true + case .backgroundDecode: backgroundDecode = true + case .preloadAllAnimationData: preloadAllAnimationData = true + case .callbackQueue(let value): callbackQueue = value + case .scaleFactor(let value): scaleFactor = value + case .requestModifier(let value): requestModifier = value + case .redirectHandler(let value): redirectHandler = value + case .processor(let value): processor = value + case .imageModifier(let value): imageModifier = value + case .cacheSerializer(let value): cacheSerializer = value + case .keepCurrentImageWhileLoading: keepCurrentImageWhileLoading = true + case .onlyLoadFirstFrame: onlyLoadFirstFrame = true + case .cacheOriginalImage: cacheOriginalImage = true + case .onFailureImage(let value): onFailureImage = .some(value) + case .alsoPrefetchToMemory: alsoPrefetchToMemory = true + case .loadDiskFileSynchronously: loadDiskFileSynchronously = true + case .diskStoreWriteOptions(let options): diskStoreWriteOptions = options + case .memoryCacheExpiration(let expiration): memoryCacheExpiration = expiration + case .memoryCacheAccessExtendingExpiration(let expirationExtending): memoryCacheAccessExtendingExpiration = expirationExtending + case .diskCacheExpiration(let expiration): diskCacheExpiration = expiration + case .diskCacheAccessExtendingExpiration(let expirationExtending): diskCacheAccessExtendingExpiration = expirationExtending + case .processingQueue(let queue): processingQueue = queue + case .progressiveJPEG(let value): progressiveJPEG = value + case .alternativeSources(let sources): alternativeSources = sources + case .retryStrategy(let strategy): retryStrategy = strategy + case .lowDataMode(let source): lowDataModeSource = source + } + } + + if originalCache == nil { + originalCache = targetCache + } + } +} + +extension KingfisherParsedOptionsInfo { + var imageCreatingOptions: ImageCreatingOptions { + return ImageCreatingOptions( + scale: scaleFactor, + duration: 0.0, + preloadAll: preloadAllAnimationData, + onlyFirstFrame: onlyLoadFirstFrame) + } +} + +protocol DataReceivingSideEffect: AnyObject { + var onShouldApply: () -> Bool { get set } + func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) +} + +class ImageLoadingProgressSideEffect: DataReceivingSideEffect { + + var onShouldApply: () -> Bool = { return true } + + let block: DownloadProgressBlock + + init(_ block: @escaping DownloadProgressBlock) { + self.block = block + } + + func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) { + guard self.onShouldApply() else { return } + guard let expectedContentLength = task.task.response?.expectedContentLength, + expectedContentLength != -1 else + { + return + } + + let dataLength = Int64(task.mutableData.count) + DispatchQueue.main.async { + self.block(dataLength, expectedContentLength) + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/Filter.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/Filter.swift new file mode 100644 index 0000000..6e4b386 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/Filter.swift @@ -0,0 +1,146 @@ +// +// Filter.swift +// Kingfisher +// +// Created by Wei Wang on 2016/08/31. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if !os(watchOS) + +import CoreImage + +// Reuse the same CI Context for all CI drawing. +private let ciContext = CIContext(options: nil) + +/// Represents the type of transformer method, which will be used in to provide a `Filter`. +public typealias Transformer = (CIImage) -> CIImage? + +/// Represents a processor based on a `CIImage` `Filter`. +/// It requires a filter to create an `ImageProcessor`. +public protocol CIImageProcessor: ImageProcessor { + var filter: Filter { get } +} + +extension CIImageProcessor { + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.apply(filter) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// A wrapper struct for a `Transformer` of CIImage filters. A `Filter` +/// value could be used to create a `CIImage` processor. +public struct Filter { + + let transform: Transformer + + public init(transform: @escaping Transformer) { + self.transform = transform + } + + /// Tint filter which will apply a tint color to images. + public static var tint: (KFCrossPlatformColor) -> Filter = { + color in + Filter { + input in + + let colorFilter = CIFilter(name: "CIConstantColorGenerator")! + colorFilter.setValue(CIColor(color: color), forKey: kCIInputColorKey) + + let filter = CIFilter(name: "CISourceOverCompositing")! + + let colorImage = colorFilter.outputImage + filter.setValue(colorImage, forKey: kCIInputImageKey) + filter.setValue(input, forKey: kCIInputBackgroundImageKey) + + return filter.outputImage?.cropped(to: input.extent) + } + } + + /// Represents color control elements. It is a tuple of + /// `(brightness, contrast, saturation, inputEV)` + public typealias ColorElement = (CGFloat, CGFloat, CGFloat, CGFloat) + + /// Color control filter which will apply color control change to images. + public static var colorControl: (ColorElement) -> Filter = { arg -> Filter in + let (brightness, contrast, saturation, inputEV) = arg + return Filter { input in + let paramsColor = [kCIInputBrightnessKey: brightness, + kCIInputContrastKey: contrast, + kCIInputSaturationKey: saturation] + let blackAndWhite = input.applyingFilter("CIColorControls", parameters: paramsColor) + let paramsExposure = [kCIInputEVKey: inputEV] + return blackAndWhite.applyingFilter("CIExposureAdjust", parameters: paramsExposure) + } + } +} + +extension KingfisherWrapper where Base: KFCrossPlatformImage { + + /// Applies a `Filter` containing `CIImage` transformer to `self`. + /// + /// - Parameter filter: The filter used to transform `self`. + /// - Returns: A transformed image by input `Filter`. + /// + /// - Note: + /// Only CG-based images are supported. If any error happens + /// during transforming, `self` will be returned. + public func apply(_ filter: Filter) -> KFCrossPlatformImage { + + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Tint image only works for CG-based image.") + return base + } + + let inputImage = CIImage(cgImage: cgImage) + guard let outputImage = filter.transform(inputImage) else { + return base + } + + guard let result = ciContext.createCGImage(outputImage, from: outputImage.extent) else { + assertionFailure("[Kingfisher] Can not make an tint image within context.") + return base + } + + #if os(macOS) + return fixedForRetinaPixel(cgImage: result, to: size) + #else + return KFCrossPlatformImage(cgImage: result, scale: base.scale, orientation: base.imageOrientation) + #endif + } + +} + +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift new file mode 100644 index 0000000..8b2480f --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift @@ -0,0 +1,121 @@ +// +// AnimatedImage.swift +// Kingfisher +// +// Created by onevcat on 2018/09/26. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import ImageIO + +/// Represents a set of image creating options used in Kingfisher. +public struct ImageCreatingOptions { + + /// The target scale of image needs to be created. + public let scale: CGFloat + + /// The expected animation duration if an animated image being created. + public let duration: TimeInterval + + /// For an animated image, whether or not all frames should be loaded before displaying. + public let preloadAll: Bool + + /// For an animated image, whether or not only the first image should be + /// loaded as a static image. It is useful for preview purpose of an animated image. + public let onlyFirstFrame: Bool + + /// Creates an `ImageCreatingOptions` object. + /// + /// - Parameters: + /// - scale: The target scale of image needs to be created. Default is `1.0`. + /// - duration: The expected animation duration if an animated image being created. + /// A value less or equal to `0.0` means the animated image duration will + /// be determined by the frame data. Default is `0.0`. + /// - preloadAll: For an animated image, whether or not all frames should be loaded before displaying. + /// Default is `false`. + /// - onlyFirstFrame: For an animated image, whether or not only the first image should be + /// loaded as a static image. It is useful for preview purpose of an animated image. + /// Default is `false`. + public init( + scale: CGFloat = 1.0, + duration: TimeInterval = 0.0, + preloadAll: Bool = false, + onlyFirstFrame: Bool = false) + { + self.scale = scale + self.duration = duration + self.preloadAll = preloadAll + self.onlyFirstFrame = onlyFirstFrame + } +} + +/// Represents the decoding for a GIF image. This class extracts frames from an `imageSource`, then +/// hold the images for later use. +public class GIFAnimatedImage { + let images: [KFCrossPlatformImage] + let duration: TimeInterval + + init?(from imageSource: CGImageSource, for info: [String: Any], options: ImageCreatingOptions) { + let frameCount = CGImageSourceGetCount(imageSource) + var images = [KFCrossPlatformImage]() + var gifDuration = 0.0 + + for i in 0 ..< frameCount { + guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, info as CFDictionary) else { + return nil + } + + if frameCount == 1 { + gifDuration = .infinity + } else { + // Get current animated GIF frame duration + gifDuration += GIFAnimatedImage.getFrameDuration(from: imageSource, at: i) + } + images.append(KingfisherWrapper.image(cgImage: imageRef, scale: options.scale, refImage: nil)) + if options.onlyFirstFrame { break } + } + self.images = images + self.duration = gifDuration + } + + /// Calculates frame duration for a gif frame out of the kCGImagePropertyGIFDictionary dictionary. + public static func getFrameDuration(from gifInfo: [String: Any]?) -> TimeInterval { + let defaultFrameDuration = 0.1 + guard let gifInfo = gifInfo else { return defaultFrameDuration } + + let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber + let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber + let duration = unclampedDelayTime ?? delayTime + + guard let frameDuration = duration else { return defaultFrameDuration } + return frameDuration.doubleValue > 0.011 ? frameDuration.doubleValue : defaultFrameDuration + } + + /// Calculates frame duration at a specific index for a gif from an `imageSource`. + public static func getFrameDuration(from imageSource: CGImageSource, at index: Int) -> TimeInterval { + guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, index, nil) + as? [String: Any] else { return 0.0 } + + let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any] + return getFrameDuration(from: gifInfo) + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/GraphicsContext.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/GraphicsContext.swift new file mode 100644 index 0000000..6d8443c --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/GraphicsContext.swift @@ -0,0 +1,88 @@ +// +// GraphicsContext.swift +// Kingfisher +// +// Created by taras on 19/04/2021. +// +// Copyright (c) 2021 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif +#if canImport(UIKit) +import UIKit +#endif + +enum GraphicsContext { + static func begin(size: CGSize, scale: CGFloat) { + #if os(macOS) + NSGraphicsContext.saveGraphicsState() + #else + UIGraphicsBeginImageContextWithOptions(size, false, scale) + #endif + } + + static func current(size: CGSize, scale: CGFloat, inverting: Bool, cgImage: CGImage?) -> CGContext? { + #if os(macOS) + guard let rep = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: Int(size.width), + pixelsHigh: Int(size.height), + bitsPerSample: cgImage?.bitsPerComponent ?? 8, + samplesPerPixel: 4, + hasAlpha: true, + isPlanar: false, + colorSpaceName: .calibratedRGB, + bytesPerRow: 0, + bitsPerPixel: 0) else + { + assertionFailure("[Kingfisher] Image representation cannot be created.") + return nil + } + rep.size = size + guard let context = NSGraphicsContext(bitmapImageRep: rep) else { + assertionFailure("[Kingfisher] Image context cannot be created.") + return nil + } + + NSGraphicsContext.current = context + return context.cgContext + #else + guard let context = UIGraphicsGetCurrentContext() else { + return nil + } + if inverting { // If drawing a CGImage, we need to make context flipped. + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: 0, y: -size.height) + } + return context + #endif + } + + static func end() { + #if os(macOS) + NSGraphicsContext.restoreGraphicsState() + #else + UIGraphicsEndImageContext() + #endif + } +} + diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/Image.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/Image.swift new file mode 100644 index 0000000..68373fc --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/Image.swift @@ -0,0 +1,377 @@ +// +// Image.swift +// Kingfisher +// +// Created by Wei Wang on 16/1/6. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +#if os(macOS) +import AppKit +private var imagesKey: Void? +private var durationKey: Void? +#else +import UIKit +import MobileCoreServices +private var imageSourceKey: Void? +#endif + +#if !os(watchOS) +import CoreImage +#endif + +import CoreGraphics +import ImageIO + +private var animatedImageDataKey: Void? +private var imageFrameCountKey: Void? + +// MARK: - Image Properties +extension KingfisherWrapper where Base: KFCrossPlatformImage { + private(set) var animatedImageData: Data? { + get { return getAssociatedObject(base, &animatedImageDataKey) } + set { setRetainedAssociatedObject(base, &animatedImageDataKey, newValue) } + } + + public var imageFrameCount: Int? { + get { return getAssociatedObject(base, &imageFrameCountKey) } + set { setRetainedAssociatedObject(base, &imageFrameCountKey, newValue) } + } + + #if os(macOS) + var cgImage: CGImage? { + return base.cgImage(forProposedRect: nil, context: nil, hints: nil) + } + + var scale: CGFloat { + return 1.0 + } + + private(set) var images: [KFCrossPlatformImage]? { + get { return getAssociatedObject(base, &imagesKey) } + set { setRetainedAssociatedObject(base, &imagesKey, newValue) } + } + + private(set) var duration: TimeInterval { + get { return getAssociatedObject(base, &durationKey) ?? 0.0 } + set { setRetainedAssociatedObject(base, &durationKey, newValue) } + } + + var size: CGSize { + return base.representations.reduce(.zero) { size, rep in + let width = max(size.width, CGFloat(rep.pixelsWide)) + let height = max(size.height, CGFloat(rep.pixelsHigh)) + return CGSize(width: width, height: height) + } + } + #else + var cgImage: CGImage? { return base.cgImage } + var scale: CGFloat { return base.scale } + var images: [KFCrossPlatformImage]? { return base.images } + var duration: TimeInterval { return base.duration } + var size: CGSize { return base.size } + + /// The image source reference of current image. + public private(set) var imageSource: CGImageSource? { + get { return getAssociatedObject(base, &imageSourceKey) } + set { setRetainedAssociatedObject(base, &imageSourceKey, newValue) } + } + #endif + + // Bitmap memory cost with bytes. + var cost: Int { + let pixel = Int(size.width * size.height * scale * scale) + guard let cgImage = cgImage else { + return pixel * 4 + } + let bytesPerPixel = cgImage.bitsPerPixel / 8 + guard let imageCount = images?.count else { + return pixel * bytesPerPixel + } + return pixel * bytesPerPixel * imageCount + } +} + +// MARK: - Image Conversion +extension KingfisherWrapper where Base: KFCrossPlatformImage { + #if os(macOS) + static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage { + return KFCrossPlatformImage(cgImage: cgImage, size: .zero) + } + + /// Normalize the image. This getter does nothing on macOS but return the image itself. + public var normalized: KFCrossPlatformImage { return base } + + #else + /// Creating an image from a give `CGImage` at scale and orientation for refImage. The method signature is for + /// compatibility of macOS version. + static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage { + return KFCrossPlatformImage(cgImage: cgImage, scale: scale, orientation: refImage?.imageOrientation ?? .up) + } + + /// Returns normalized image for current `base` image. + /// This method will try to redraw an image with orientation and scale considered. + public var normalized: KFCrossPlatformImage { + // prevent animated image (GIF) lose it's images + guard images == nil else { return base.copy() as! KFCrossPlatformImage } + // No need to do anything if already up + guard base.imageOrientation != .up else { return base.copy() as! KFCrossPlatformImage } + + return draw(to: size, inverting: true, refImage: KFCrossPlatformImage()) { + fixOrientation(in: $0) + return true + } + } + + func fixOrientation(in context: CGContext) { + + var transform = CGAffineTransform.identity + + let orientation = base.imageOrientation + + switch orientation { + case .down, .downMirrored: + transform = transform.translatedBy(x: size.width, y: size.height) + transform = transform.rotated(by: .pi) + case .left, .leftMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.rotated(by: .pi / 2.0) + case .right, .rightMirrored: + transform = transform.translatedBy(x: 0, y: size.height) + transform = transform.rotated(by: .pi / -2.0) + case .up, .upMirrored: + break + #if compiler(>=5) + @unknown default: + break + #endif + } + + //Flip image one more time if needed to, this is to prevent flipped image + switch orientation { + case .upMirrored, .downMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + case .leftMirrored, .rightMirrored: + transform = transform.translatedBy(x: size.height, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + case .up, .down, .left, .right: + break + #if compiler(>=5) + @unknown default: + break + #endif + } + + context.concatenate(transform) + switch orientation { + case .left, .leftMirrored, .right, .rightMirrored: + context.draw(cgImage!, in: CGRect(x: 0, y: 0, width: size.height, height: size.width)) + default: + context.draw(cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) + } + } + #endif +} + +// MARK: - Image Representation +extension KingfisherWrapper where Base: KFCrossPlatformImage { + /// Returns PNG representation of `base` image. + /// + /// - Returns: PNG data of image. + public func pngRepresentation() -> Data? { + #if os(macOS) + guard let cgImage = cgImage else { + return nil + } + let rep = NSBitmapImageRep(cgImage: cgImage) + return rep.representation(using: .png, properties: [:]) + #else + return base.pngData() + #endif + } + + /// Returns JPEG representation of `base` image. + /// + /// - Parameter compressionQuality: The compression quality when converting image to JPEG data. + /// - Returns: JPEG data of image. + public func jpegRepresentation(compressionQuality: CGFloat) -> Data? { + #if os(macOS) + guard let cgImage = cgImage else { + return nil + } + let rep = NSBitmapImageRep(cgImage: cgImage) + return rep.representation(using:.jpeg, properties: [.compressionFactor: compressionQuality]) + #else + return base.jpegData(compressionQuality: compressionQuality) + #endif + } + + /// Returns GIF representation of `base` image. + /// + /// - Returns: Original GIF data of image. + public func gifRepresentation() -> Data? { + return animatedImageData + } + + /// Returns a data representation for `base` image, with the `format` as the format indicator. + /// - Parameters: + /// - format: The format in which the output data should be. If `unknown`, the `base` image will be + /// converted in the PNG representation. + /// - compressionQuality: The compression quality when converting image to a lossy format data. + /// + /// - Returns: The output data representing. + public func data(format: ImageFormat, compressionQuality: CGFloat = 1.0) -> Data? { + return autoreleasepool { () -> Data? in + let data: Data? + switch format { + case .PNG: data = pngRepresentation() + case .JPEG: data = jpegRepresentation(compressionQuality: compressionQuality) + case .GIF: data = gifRepresentation() + case .unknown: data = normalized.kf.pngRepresentation() + } + + return data + } + } +} + +// MARK: - Creating Images +extension KingfisherWrapper where Base: KFCrossPlatformImage { + + /// Creates an animated image from a given data and options. Currently only GIF data is supported. + /// + /// - Parameters: + /// - data: The animated image data. + /// - options: Options to use when creating the animated image. + /// - Returns: An `Image` object represents the animated image. It is in form of an array of image frames with a + /// certain duration. `nil` if anything wrong when creating animated image. + public static func animatedImage(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? { + let info: [String: Any] = [ + kCGImageSourceShouldCache as String: true, + kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF + ] + + guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else { + return nil + } + + #if os(macOS) + guard let animatedImage = GIFAnimatedImage(from: imageSource, for: info, options: options) else { + return nil + } + var image: KFCrossPlatformImage? + if options.onlyFirstFrame { + image = animatedImage.images.first + } else { + image = KFCrossPlatformImage(data: data) + var kf = image?.kf + kf?.images = animatedImage.images + kf?.duration = animatedImage.duration + } + image?.kf.animatedImageData = data + image?.kf.imageFrameCount = Int(CGImageSourceGetCount(imageSource)) + return image + #else + + var image: KFCrossPlatformImage? + if options.preloadAll || options.onlyFirstFrame { + // Use `images` image if you want to preload all animated data + guard let animatedImage = GIFAnimatedImage(from: imageSource, for: info, options: options) else { + return nil + } + if options.onlyFirstFrame { + image = animatedImage.images.first + } else { + let duration = options.duration <= 0.0 ? animatedImage.duration : options.duration + image = .animatedImage(with: animatedImage.images, duration: duration) + } + image?.kf.animatedImageData = data + } else { + image = KFCrossPlatformImage(data: data, scale: options.scale) + var kf = image?.kf + kf?.imageSource = imageSource + kf?.animatedImageData = data + } + + image?.kf.imageFrameCount = Int(CGImageSourceGetCount(imageSource)) + return image + #endif + } + + /// Creates an image from a given data and options. `.JPEG`, `.PNG` or `.GIF` is supported. For other + /// image format, image initializer from system will be used. If no image object could be created from + /// the given `data`, `nil` will be returned. + /// + /// - Parameters: + /// - data: The image data representation. + /// - options: Options to use when creating the image. + /// - Returns: An `Image` object represents the image if created. If the `data` is invalid or not supported, `nil` + /// will be returned. + public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? { + var image: KFCrossPlatformImage? + switch data.kf.imageFormat { + case .JPEG: + image = KFCrossPlatformImage(data: data, scale: options.scale) + case .PNG: + image = KFCrossPlatformImage(data: data, scale: options.scale) + case .GIF: + image = KingfisherWrapper.animatedImage(data: data, options: options) + case .unknown: + image = KFCrossPlatformImage(data: data, scale: options.scale) + } + return image + } + + /// Creates a downsampled image from given data to a certain size and scale. + /// + /// - Parameters: + /// - data: The image data contains a JPEG or PNG image. + /// - pointSize: The target size in point to which the image should be downsampled. + /// - scale: The scale of result image. + /// - Returns: A downsampled `Image` object following the input conditions. + /// + /// - Note: + /// Different from image `resize` methods, downsampling will not render the original + /// input image in pixel format. It does downsampling from the image data, so it is much + /// more memory efficient and friendly. Choose to use downsampling as possible as you can. + /// + /// The input size should be smaller than the size of input image. If it is larger than the + /// original image size, the result image will be the same size of input without downsampling. + public static func downsampledImage(data: Data, to pointSize: CGSize, scale: CGFloat) -> KFCrossPlatformImage? { + let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary + guard let imageSource = CGImageSourceCreateWithData(data as CFData, imageSourceOptions) else { + return nil + } + + let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale + let downsampleOptions = [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary + guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else { + return nil + } + return KingfisherWrapper.image(cgImage: downsampledImage, scale: scale, refImage: nil) + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageDrawing.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageDrawing.swift new file mode 100644 index 0000000..fce627c --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageDrawing.swift @@ -0,0 +1,632 @@ +// +// ImageDrawing.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Accelerate + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif +#if canImport(UIKit) +import UIKit +#endif + +// MARK: - Image Transforming +extension KingfisherWrapper where Base: KFCrossPlatformImage { + // MARK: Blend Mode + /// Create image from `base` image and apply blend mode. + /// + /// - parameter blendMode: The blend mode of creating image. + /// - parameter alpha: The alpha should be used for image. + /// - parameter backgroundColor: The background color for the output image. + /// + /// - returns: An image with blend mode applied. + /// + /// - Note: This method only works for CG-based image. + #if !os(macOS) + public func image(withBlendMode blendMode: CGBlendMode, + alpha: CGFloat = 1.0, + backgroundColor: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage + { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Blend mode image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: .zero, size: size) + return draw(to: rect.size, inverting: false) { _ in + if let backgroundColor = backgroundColor { + backgroundColor.setFill() + UIRectFill(rect) + } + + base.draw(in: rect, blendMode: blendMode, alpha: alpha) + return false + } + } + #endif + + #if os(macOS) + // MARK: Compositing + /// Creates image from `base` image and apply compositing operation. + /// + /// - Parameters: + /// - compositingOperation: The compositing operation of creating image. + /// - alpha: The alpha should be used for image. + /// - backgroundColor: The background color for the output image. + /// - Returns: An image with compositing operation applied. + /// + /// - Note: This method only works for CG-based image. For any non-CG-based image, `base` itself is returned. + public func image(withCompositingOperation compositingOperation: NSCompositingOperation, + alpha: CGFloat = 1.0, + backgroundColor: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage + { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Compositing Operation image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: .zero, size: size) + return draw(to: rect.size, inverting: false) { _ in + if let backgroundColor = backgroundColor { + backgroundColor.setFill() + rect.fill() + } + base.draw(in: rect, from: .zero, operation: compositingOperation, fraction: alpha) + return false + } + } + #endif + + // MARK: Round Corner + + /// Creates a round corner image from on `base` image. + /// + /// - Parameters: + /// - radius: The round corner radius of creating image. + /// - size: The target size of creating image. + /// - corners: The target corners which will be applied rounding. + /// - backgroundColor: The background color for the output image + /// - Returns: An image with round corner of `self`. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func image( + withRadius radius: Radius, + fit size: CGSize, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) -> KFCrossPlatformImage + { + + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Round corner image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) + return draw(to: size, inverting: false) { _ in + #if os(macOS) + if let backgroundColor = backgroundColor { + let rectPath = NSBezierPath(rect: rect) + backgroundColor.setFill() + rectPath.fill() + } + + let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners) + path.addClip() + base.draw(in: rect) + #else + guard let context = UIGraphicsGetCurrentContext() else { + assertionFailure("[Kingfisher] Failed to create CG context for image.") + return false + } + + if let backgroundColor = backgroundColor { + let rectPath = UIBezierPath(rect: rect) + backgroundColor.setFill() + rectPath.fill() + } + + let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners) + context.addPath(path.cgPath) + context.clip() + base.draw(in: rect) + #endif + return false + } + } + + /// Creates a round corner image from on `base` image. + /// + /// - Parameters: + /// - radius: The round corner radius of creating image. + /// - size: The target size of creating image. + /// - corners: The target corners which will be applied rounding. + /// - backgroundColor: The background color for the output image + /// - Returns: An image with round corner of `self`. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func image( + withRoundRadius radius: CGFloat, + fit size: CGSize, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) -> KFCrossPlatformImage + { + image(withRadius: .point(radius), fit: size, roundingCorners: corners, backgroundColor: backgroundColor) + } + + #if os(macOS) + func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> NSBezierPath { + let cornerRadius = radius.compute(with: rect.size) + let path = NSBezierPath(roundedRect: rect, byRoundingCorners: corners, radius: cornerRadius - offsetBase / 2) + path.windingRule = .evenOdd + return path + } + #else + func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> UIBezierPath { + let cornerRadius = radius.compute(with: rect.size) + return UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners.uiRectCorner, + cornerRadii: CGSize( + width: cornerRadius - offsetBase / 2, + height: cornerRadius - offsetBase / 2 + ) + ) + } + #endif + + #if os(iOS) || os(tvOS) + func resize(to size: CGSize, for contentMode: UIView.ContentMode) -> KFCrossPlatformImage { + switch contentMode { + case .scaleAspectFit: + return resize(to: size, for: .aspectFit) + case .scaleAspectFill: + return resize(to: size, for: .aspectFill) + default: + return resize(to: size) + } + } + #endif + + // MARK: Resizing + /// Resizes `base` image to an image with new size. + /// + /// - Parameter size: The target size in point. + /// - Returns: An image with new size. + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func resize(to size: CGSize) -> KFCrossPlatformImage { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Resize only works for CG-based image.") + return base + } + + let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) + return draw(to: size, inverting: false) { _ in + #if os(macOS) + base.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0) + #else + base.draw(in: rect) + #endif + return false + } + } + + /// Resizes `base` image to an image of new size, respecting the given content mode. + /// + /// - Parameters: + /// - targetSize: The target size in point. + /// - contentMode: Content mode of output image should be. + /// - Returns: An image with new size. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func resize(to targetSize: CGSize, for contentMode: ContentMode) -> KFCrossPlatformImage { + let newSize = size.kf.resize(to: targetSize, for: contentMode) + return resize(to: newSize) + } + + // MARK: Cropping + /// Crops `base` image to a new size with a given anchor. + /// + /// - Parameters: + /// - size: The target size. + /// - anchor: The anchor point from which the size should be calculated. + /// - Returns: An image with new size. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func crop(to size: CGSize, anchorOn anchor: CGPoint) -> KFCrossPlatformImage { + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Crop only works for CG-based image.") + return base + } + + let rect = self.size.kf.constrainedRect(for: size, anchor: anchor) + guard let image = cgImage.cropping(to: rect.scaled(scale)) else { + assertionFailure("[Kingfisher] Cropping image failed.") + return base + } + + return KingfisherWrapper.image(cgImage: image, scale: scale, refImage: base) + } + + // MARK: Blur + /// Creates an image with blur effect based on `base` image. + /// + /// - Parameter radius: The blur radius should be used when creating blur effect. + /// - Returns: An image with blur effect applied. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func blurred(withRadius radius: CGFloat) -> KFCrossPlatformImage { + + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Blur only works for CG-based image.") + return base + } + + // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement + // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5) + // if d is odd, use three box-blurs of size 'd', centered on the output pixel. + let s = max(radius, 2.0) + // We will do blur on a resized image (*0.5), so the blur radius could be half as well. + + // Fix the slow compiling time for Swift 3. + // See https://github.com/onevcat/Kingfisher/issues/611 + let pi2 = 2 * CGFloat.pi + let sqrtPi2 = sqrt(pi2) + var targetRadius = floor(s * 3.0 * sqrtPi2 / 4.0 + 0.5) + + if targetRadius.isEven { targetRadius += 1 } + + // Determine necessary iteration count by blur radius. + let iterations: Int + if radius < 0.5 { + iterations = 1 + } else if radius < 1.5 { + iterations = 2 + } else { + iterations = 3 + } + + let w = Int(size.width) + let h = Int(size.height) + + func createEffectBuffer(_ context: CGContext) -> vImage_Buffer { + let data = context.data + let width = vImagePixelCount(context.width) + let height = vImagePixelCount(context.height) + let rowBytes = context.bytesPerRow + + return vImage_Buffer(data: data, height: height, width: width, rowBytes: rowBytes) + } + GraphicsContext.begin(size: size, scale: scale) + guard let context = GraphicsContext.current(size: size, scale: scale, inverting: true, cgImage: cgImage) else { + assertionFailure("[Kingfisher] Failed to create CG context for blurring image.") + return base + } + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: w, height: h)) + GraphicsContext.end() + + var inBuffer = createEffectBuffer(context) + + GraphicsContext.begin(size: size, scale: scale) + guard let outContext = GraphicsContext.current(size: size, scale: scale, inverting: true, cgImage: cgImage) else { + assertionFailure("[Kingfisher] Failed to create CG context for blurring image.") + return base + } + defer { GraphicsContext.end() } + var outBuffer = createEffectBuffer(outContext) + + for _ in 0 ..< iterations { + let flag = vImage_Flags(kvImageEdgeExtend) + vImageBoxConvolve_ARGB8888( + &inBuffer, &outBuffer, nil, 0, 0, UInt32(targetRadius), UInt32(targetRadius), nil, flag) + // Next inBuffer should be the outButter of current iteration + (inBuffer, outBuffer) = (outBuffer, inBuffer) + } + + #if os(macOS) + let result = outContext.makeImage().flatMap { + fixedForRetinaPixel(cgImage: $0, to: size) + } + #else + let result = outContext.makeImage().flatMap { + KFCrossPlatformImage(cgImage: $0, scale: base.scale, orientation: base.imageOrientation) + } + #endif + guard let blurredImage = result else { + assertionFailure("[Kingfisher] Can not make an blurred image within this context.") + return base + } + + return blurredImage + } + + public func addingBorder(_ border: Border) -> KFCrossPlatformImage + { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Blend mode image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: .zero, size: size) + return draw(to: rect.size, inverting: false) { context in + + #if os(macOS) + base.draw(in: rect) + #else + base.draw(in: rect, blendMode: .normal, alpha: 1.0) + #endif + + + let strokeRect = rect.insetBy(dx: border.lineWidth / 2, dy: border.lineWidth / 2) + context.setStrokeColor(border.color.cgColor) + context.setAlpha(border.color.rgba.a) + + let line = pathForRoundCorner( + rect: strokeRect, + radius: border.radius, + corners: border.roundingCorners, + offsetBase: border.lineWidth + ) + line.lineCapStyle = .square + line.lineWidth = border.lineWidth + line.stroke() + + return false + } + } + + // MARK: Overlay + /// Creates an image from `base` image with a color overlay layer. + /// + /// - Parameters: + /// - color: The color should be use to overlay. + /// - fraction: Fraction of input color. From 0.0 to 1.0. 0.0 means solid color, + /// 1.0 means transparent overlay. + /// - Returns: An image with a color overlay applied. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func overlaying(with color: KFCrossPlatformColor, fraction: CGFloat) -> KFCrossPlatformImage { + + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Overlaying only works for CG-based image.") + return base + } + + let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) + return draw(to: rect.size, inverting: false) { context in + #if os(macOS) + base.draw(in: rect) + if fraction > 0 { + color.withAlphaComponent(1 - fraction).set() + rect.fill(using: .sourceAtop) + } + #else + color.set() + UIRectFill(rect) + base.draw(in: rect, blendMode: .destinationIn, alpha: 1.0) + + if fraction > 0 { + base.draw(in: rect, blendMode: .sourceAtop, alpha: fraction) + } + #endif + return false + } + } + + // MARK: Tint + /// Creates an image from `base` image with a color tint. + /// + /// - Parameter color: The color should be used to tint `base` + /// - Returns: An image with a color tint applied. + public func tinted(with color: KFCrossPlatformColor) -> KFCrossPlatformImage { + #if os(watchOS) + return base + #else + return apply(.tint(color)) + #endif + } + + // MARK: Color Control + + /// Create an image from `self` with color control. + /// + /// - Parameters: + /// - brightness: Brightness changing to image. + /// - contrast: Contrast changing to image. + /// - saturation: Saturation changing to image. + /// - inputEV: InputEV changing to image. + /// - Returns: An image with color control applied. + public func adjusted(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) -> KFCrossPlatformImage { + #if os(watchOS) + return base + #else + return apply(.colorControl((brightness, contrast, saturation, inputEV))) + #endif + } + + /// Return an image with given scale. + /// + /// - Parameter scale: Target scale factor the new image should have. + /// - Returns: The image with target scale. If the base image is already in the scale, `base` will be returned. + public func scaled(to scale: CGFloat) -> KFCrossPlatformImage { + guard scale != self.scale else { + return base + } + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Scaling only works for CG-based image.") + return base + } + return KingfisherWrapper.image(cgImage: cgImage, scale: scale, refImage: base) + } +} + +// MARK: - Decoding Image +extension KingfisherWrapper where Base: KFCrossPlatformImage { + + /// Returns the decoded image of the `base` image. It will draw the image in a plain context and return the data + /// from it. This could improve the drawing performance when an image is just created from data but not yet + /// displayed for the first time. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image or animated image, `base` itself is returned. + public var decoded: KFCrossPlatformImage { return decoded(scale: scale) } + + /// Returns decoded image of the `base` image at a given scale. It will draw the image in a plain context and + /// return the data from it. This could improve the drawing performance when an image is just created from + /// data but not yet displayed for the first time. + /// + /// - Parameter scale: The given scale of target image should be. + /// - Returns: The decoded image ready to be displayed. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image or animated image, `base` itself is returned. + public func decoded(scale: CGFloat) -> KFCrossPlatformImage { + // Prevent animated image (GIF) losing it's images + #if os(iOS) + if imageSource != nil { return base } + #else + if images != nil { return base } + #endif + + guard let imageRef = cgImage else { + assertionFailure("[Kingfisher] Decoding only works for CG-based image.") + return base + } + + let size = CGSize(width: CGFloat(imageRef.width) / scale, height: CGFloat(imageRef.height) / scale) + return draw(to: size, inverting: true, scale: scale) { context in + context.draw(imageRef, in: CGRect(origin: .zero, size: size)) + return true + } + } + + /// Returns decoded image of the `base` image at a given scale. It will draw the image in a plain context and + /// return the data from it. This could improve the drawing performance when an image is just created from + /// data but not yet displayed for the first time. + /// + /// - Parameter context: The context for drawing. + /// - Returns: The decoded image ready to be displayed. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image or animated image, `base` itself is returned. + public func decoded(on context: CGContext) -> KFCrossPlatformImage { + // Prevent animated image (GIF) losing it's images + #if os(iOS) + if imageSource != nil { return base } + #else + if images != nil { return base } + #endif + + guard let refImage = cgImage else { + assertionFailure("[Kingfisher] Decoding only works for CG-based image.") + return base + } + + let size = CGSize(width: CGFloat(refImage.width) / scale, height: CGFloat(refImage.height) / scale) + + context.draw(refImage, in: CGRect(origin: .zero, size: size)) + + guard let cgImage = context.makeImage() else { + return base + } + + return KingfisherWrapper.image(cgImage: cgImage, scale: scale, refImage: base) + } +} + +extension KingfisherWrapper where Base: KFCrossPlatformImage { + func draw( + to size: CGSize, + inverting: Bool, + scale: CGFloat? = nil, + refImage: KFCrossPlatformImage? = nil, + draw: (CGContext) -> Bool // Whether use the refImage (`true`) or ignore image orientation (`false`) + ) -> KFCrossPlatformImage + { + #if os(macOS) || os(watchOS) + let targetScale = scale ?? self.scale + GraphicsContext.begin(size: size, scale: targetScale) + guard let context = GraphicsContext.current(size: size, scale: targetScale, inverting: inverting, cgImage: cgImage) else { + assertionFailure("[Kingfisher] Failed to create CG context for blurring image.") + return base + } + defer { GraphicsContext.end() } + let useRefImage = draw(context) + guard let cgImage = context.makeImage() else { + return base + } + let ref = useRefImage ? (refImage ?? base) : nil + return KingfisherWrapper.image(cgImage: cgImage, scale: targetScale, refImage: ref) + #else + + let format = UIGraphicsImageRendererFormat.preferred() + format.scale = scale ?? self.scale + let renderer = UIGraphicsImageRenderer(size: size, format: format) + + var useRefImage: Bool = false + let image = renderer.image { rendererContext in + + let context = rendererContext.cgContext + if inverting { // If drawing a CGImage, we need to make context flipped. + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: 0, y: -size.height) + } + + useRefImage = draw(context) + } + if useRefImage { + guard let cgImage = image.cgImage else { + return base + } + let ref = refImage ?? base + return KingfisherWrapper.image(cgImage: cgImage, scale: format.scale, refImage: ref) + } else { + return image + } + #endif + } + + #if os(macOS) + func fixedForRetinaPixel(cgImage: CGImage, to size: CGSize) -> KFCrossPlatformImage { + + let image = KFCrossPlatformImage(cgImage: cgImage, size: base.size) + let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) + + return draw(to: self.size, inverting: false) { context in + image.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0) + return false + } + } + #endif +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageFormat.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageFormat.swift new file mode 100644 index 0000000..464a855 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageFormat.swift @@ -0,0 +1,130 @@ +// +// ImageFormat.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents image format. +/// +/// - unknown: The format cannot be recognized or not supported yet. +/// - PNG: PNG image format. +/// - JPEG: JPEG image format. +/// - GIF: GIF image format. +public enum ImageFormat { + /// The format cannot be recognized or not supported yet. + case unknown + /// PNG image format. + case PNG + /// JPEG image format. + case JPEG + /// GIF image format. + case GIF + + struct HeaderData { + static var PNG: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] + static var JPEG_SOI: [UInt8] = [0xFF, 0xD8] + static var JPEG_IF: [UInt8] = [0xFF] + static var GIF: [UInt8] = [0x47, 0x49, 0x46] + } + + /// https://en.wikipedia.org/wiki/JPEG + public enum JPEGMarker { + case SOF0 //baseline + case SOF2 //progressive + case DHT //Huffman Table + case DQT //Quantization Table + case DRI //Restart Interval + case SOS //Start Of Scan + case RSTn(UInt8) //Restart + case APPn //Application-specific + case COM //Comment + case EOI //End Of Image + + var bytes: [UInt8] { + switch self { + case .SOF0: return [0xFF, 0xC0] + case .SOF2: return [0xFF, 0xC2] + case .DHT: return [0xFF, 0xC4] + case .DQT: return [0xFF, 0xDB] + case .DRI: return [0xFF, 0xDD] + case .SOS: return [0xFF, 0xDA] + case .RSTn(let n): return [0xFF, 0xD0 + n] + case .APPn: return [0xFF, 0xE0] + case .COM: return [0xFF, 0xFE] + case .EOI: return [0xFF, 0xD9] + } + } + } +} + + +extension Data: KingfisherCompatibleValue {} + +// MARK: - Misc Helpers +extension KingfisherWrapper where Base == Data { + /// Gets the image format corresponding to the data. + public var imageFormat: ImageFormat { + guard base.count > 8 else { return .unknown } + + var buffer = [UInt8](repeating: 0, count: 8) + base.copyBytes(to: &buffer, count: 8) + + if buffer == ImageFormat.HeaderData.PNG { + return .PNG + + } else if buffer[0] == ImageFormat.HeaderData.JPEG_SOI[0], + buffer[1] == ImageFormat.HeaderData.JPEG_SOI[1], + buffer[2] == ImageFormat.HeaderData.JPEG_IF[0] + { + return .JPEG + + } else if buffer[0] == ImageFormat.HeaderData.GIF[0], + buffer[1] == ImageFormat.HeaderData.GIF[1], + buffer[2] == ImageFormat.HeaderData.GIF[2] + { + return .GIF + } + + return .unknown + } + + public func contains(jpeg marker: ImageFormat.JPEGMarker) -> Bool { + guard imageFormat == .JPEG else { + return false + } + + let bytes = [UInt8](base) + let markerBytes = marker.bytes + for (index, item) in bytes.enumerated() where bytes.count > index + 1 { + guard + item == markerBytes.first, + bytes[index + 1] == markerBytes[1] else { + continue + } + return true + } + return false + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageProcessor.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageProcessor.swift new file mode 100644 index 0000000..2123822 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageProcessor.swift @@ -0,0 +1,935 @@ +// +// ImageProcessor.swift +// Kingfisher +// +// Created by Wei Wang on 2016/08/26. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CoreGraphics + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif + +/// Represents an item which could be processed by an `ImageProcessor`. +/// +/// - image: Input image. The processor should provide a way to apply +/// processing on this `image` and return the result image. +/// - data: Input data. The processor should provide a way to apply +/// processing on this `data` and return the result image. +public enum ImageProcessItem { + + /// Input image. The processor should provide a way to apply + /// processing on this `image` and return the result image. + case image(KFCrossPlatformImage) + + /// Input data. The processor should provide a way to apply + /// processing on this `data` and return the result image. + case data(Data) +} + +/// An `ImageProcessor` would be used to convert some downloaded data to an image. +public protocol ImageProcessor { + /// Identifier of the processor. It will be used to identify the processor when + /// caching and retrieving an image. You might want to make sure that processors with + /// same properties/functionality have the same identifiers, so correct processed images + /// could be retrieved with proper key. + /// + /// - Note: Do not supply an empty string for a customized processor, which is already reserved by + /// the `DefaultImageProcessor`. It is recommended to use a reverse domain name notation string of + /// your own for the identifier. + var identifier: String { get } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: The parsed options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: The return value should be `nil` if processing failed while converting an input item to image. + /// If `nil` received by the processing caller, an error will be reported and the process flow stops. + /// If the processing flow is not critical for your flow, then when the input item is already an image + /// (`.image` case) and there is any errors in the processing, you could return the input image itself + /// to keep the processing pipeline continuing. + /// - Note: Most processor only supports CG-based images. watchOS is not supported for processors containing + /// a filter, the input image will be returned directly on watchOS. + func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? +} + +extension ImageProcessor { + + /// Appends an `ImageProcessor` to another. The identifier of the new `ImageProcessor` + /// will be "\(self.identifier)|>\(another.identifier)". + /// + /// - Parameter another: An `ImageProcessor` you want to append to `self`. + /// - Returns: The new `ImageProcessor` will process the image in the order + /// of the two processors concatenated. + public func append(another: ImageProcessor) -> ImageProcessor { + let newIdentifier = identifier.appending("|>\(another.identifier)") + return GeneralProcessor(identifier: newIdentifier) { + item, options in + if let image = self.process(item: item, options: options) { + return another.process(item: .image(image), options: options) + } else { + return nil + } + } + } +} + +func ==(left: ImageProcessor, right: ImageProcessor) -> Bool { + return left.identifier == right.identifier +} + +func !=(left: ImageProcessor, right: ImageProcessor) -> Bool { + return !(left == right) +} + +typealias ProcessorImp = ((ImageProcessItem, KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?) +struct GeneralProcessor: ImageProcessor { + let identifier: String + let p: ProcessorImp + func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return p(item, options) + } +} + +/// The default processor. It converts the input data to a valid image. +/// Images of .PNG, .JPEG and .GIF format are supported. +/// If an image item is given as `.image` case, `DefaultImageProcessor` will +/// do nothing on it and return the associated image. +public struct DefaultImageProcessor: ImageProcessor { + + /// A default `DefaultImageProcessor` could be used across. + public static let `default` = DefaultImageProcessor() + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier = "" + + /// Creates a `DefaultImageProcessor`. Use `DefaultImageProcessor.default` to get an instance, + /// if you do not have a good reason to create your own `DefaultImageProcessor`. + public init() {} + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + case .data(let data): + return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) + } + } +} + +/// Represents the rect corner setting when processing a round corner image. +public struct RectCorner: OptionSet { + + /// Raw value of the rect corner. + public let rawValue: Int + + /// Represents the top left corner. + public static let topLeft = RectCorner(rawValue: 1 << 0) + + /// Represents the top right corner. + public static let topRight = RectCorner(rawValue: 1 << 1) + + /// Represents the bottom left corner. + public static let bottomLeft = RectCorner(rawValue: 1 << 2) + + /// Represents the bottom right corner. + public static let bottomRight = RectCorner(rawValue: 1 << 3) + + /// Represents all corners. + public static let all: RectCorner = [.topLeft, .topRight, .bottomLeft, .bottomRight] + + /// Creates a `RectCorner` option set with a given value. + /// + /// - Parameter rawValue: The value represents a certain corner option. + public init(rawValue: Int) { + self.rawValue = rawValue + } + + var cornerIdentifier: String { + if self == .all { + return "" + } + return "_corner(\(rawValue))" + } +} + +#if !os(macOS) +/// Processor for adding an blend mode to images. Only CG-based images are supported. +public struct BlendImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Blend Mode will be used to blend the input image. + public let blendMode: CGBlendMode + + /// Alpha will be used when blend image. + public let alpha: CGFloat + + /// Background color of the output image. If `nil`, it will stay transparent. + public let backgroundColor: KFCrossPlatformColor? + + /// Creates a `BlendImageProcessor`. + /// + /// - Parameters: + /// - blendMode: Blend Mode will be used to blend the input image. + /// - alpha: Alpha will be used when blend image. From 0.0 to 1.0. 1.0 means solid image, + /// 0.0 means transparent image (not visible at all). Default is 1.0. + /// - backgroundColor: Background color to apply for the output image. Default is `nil`. + public init(blendMode: CGBlendMode, alpha: CGFloat = 1.0, backgroundColor: KFCrossPlatformColor? = nil) { + self.blendMode = blendMode + self.alpha = alpha + self.backgroundColor = backgroundColor + var identifier = "com.onevcat.Kingfisher.BlendImageProcessor(\(blendMode.rawValue),\(alpha))" + if let color = backgroundColor { + identifier.append("_\(color.rgbaDescription)") + } + self.identifier = identifier + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.image(withBlendMode: blendMode, alpha: alpha, backgroundColor: backgroundColor) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} +#endif + +#if os(macOS) +/// Processor for adding an compositing operation to images. Only CG-based images are supported in macOS. +public struct CompositingImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Compositing operation will be used to the input image. + public let compositingOperation: NSCompositingOperation + + /// Alpha will be used when compositing image. + public let alpha: CGFloat + + /// Background color of the output image. If `nil`, it will stay transparent. + public let backgroundColor: KFCrossPlatformColor? + + /// Creates a `CompositingImageProcessor` + /// + /// - Parameters: + /// - compositingOperation: Compositing operation will be used to the input image. + /// - alpha: Alpha will be used when compositing image. + /// From 0.0 to 1.0. 1.0 means solid image, 0.0 means transparent image. + /// Default is 1.0. + /// - backgroundColor: Background color to apply for the output image. Default is `nil`. + public init(compositingOperation: NSCompositingOperation, + alpha: CGFloat = 1.0, + backgroundColor: KFCrossPlatformColor? = nil) + { + self.compositingOperation = compositingOperation + self.alpha = alpha + self.backgroundColor = backgroundColor + var identifier = "com.onevcat.Kingfisher.CompositingImageProcessor(\(compositingOperation.rawValue),\(alpha))" + if let color = backgroundColor { + identifier.append("_\(color.rgbaDescription)") + } + self.identifier = identifier + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.image( + withCompositingOperation: compositingOperation, + alpha: alpha, + backgroundColor: backgroundColor) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} +#endif + +/// Represents a radius specified in a `RoundCornerImageProcessor`. +public enum Radius { + /// The radius should be calculated as a fraction of the image width. Typically the associated value should be + /// between 0 and 0.5, where 0 represents no radius and 0.5 represents using half of the image width. + case widthFraction(CGFloat) + /// The radius should be calculated as a fraction of the image height. Typically the associated value should be + /// between 0 and 0.5, where 0 represents no radius and 0.5 represents using half of the image height. + case heightFraction(CGFloat) + /// Use a fixed point value as the round corner radius. + case point(CGFloat) + + var radiusIdentifier: String { + switch self { + case .widthFraction(let f): + return "w_frac_\(f)" + case .heightFraction(let f): + return "h_frac_\(f)" + case .point(let p): + return p.description + } + } + + public func compute(with size: CGSize) -> CGFloat { + let cornerRadius: CGFloat + switch self { + case .point(let point): + cornerRadius = point + case .widthFraction(let widthFraction): + cornerRadius = size.width * widthFraction + case .heightFraction(let heightFraction): + cornerRadius = size.height * heightFraction + } + return cornerRadius + } +} + +/// Processor for making round corner images. Only CG-based images are supported in macOS, +/// if a non-CG image passed in, the processor will do nothing. +/// +/// - Note: The input image will be rendered with round corner pixels removed. If the image itself does not contain +/// alpha channel (for example, a JPEG image), the processed image will contain an alpha channel in memory in order +/// to show correctly. However, when cached to disk, Kingfisher respects the original image format by default. That +/// means the alpha channel will be removed for these images. When you load the processed image from cache again, you +/// will lose transparent corner. +/// +/// You could use `FormatIndicatedCacheSerializer.png` to force Kingfisher to serialize the image to PNG format in this +/// case. +/// +public struct RoundCornerImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// The radius will be applied in processing. Specify a certain point value with `.point`, or a fraction of the + /// target image with `.widthFraction`. or `.heightFraction`. For example, given a square image with width and + /// height equals, `.widthFraction(0.5)` means use half of the length of size and makes the final image a round one. + public let radius: Radius + + /// The target corners which will be applied rounding. + public let roundingCorners: RectCorner + + /// Target size of output image should be. If `nil`, the image will keep its original size after processing. + public let targetSize: CGSize? + + /// Background color of the output image. If `nil`, it will use a transparent background. + public let backgroundColor: KFCrossPlatformColor? + + /// Creates a `RoundCornerImageProcessor`. + /// + /// - Parameters: + /// - cornerRadius: Corner radius in point will be applied in processing. + /// - targetSize: Target size of output image should be. If `nil`, + /// the image will keep its original size after processing. + /// Default is `nil`. + /// - corners: The target corners which will be applied rounding. Default is `.all`. + /// - backgroundColor: Background color to apply for the output image. Default is `nil`. + /// + /// - Note: + /// + /// This initializer accepts a concrete point value for `cornerRadius`. If you do not know the image size, but still + /// want to apply a full round-corner (making the final image a round one), or specify the corner radius as a + /// fraction of one dimension of the target image, use the `Radius` version instead. + /// + public init( + cornerRadius: CGFloat, + targetSize: CGSize? = nil, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) + { + let radius = Radius.point(cornerRadius) + self.init(radius: radius, targetSize: targetSize, roundingCorners: corners, backgroundColor: backgroundColor) + } + + /// Creates a `RoundCornerImageProcessor`. + /// + /// - Parameters: + /// - radius: The radius will be applied in processing. + /// - targetSize: Target size of output image should be. If `nil`, + /// the image will keep its original size after processing. + /// Default is `nil`. + /// - corners: The target corners which will be applied rounding. Default is `.all`. + /// - backgroundColor: Background color to apply for the output image. Default is `nil`. + public init( + radius: Radius, + targetSize: CGSize? = nil, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) + { + self.radius = radius + self.targetSize = targetSize + self.roundingCorners = corners + self.backgroundColor = backgroundColor + + self.identifier = { + var identifier = "" + + if let size = targetSize { + identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" + + "(\(radius.radiusIdentifier)_\(size)\(corners.cornerIdentifier))" + } else { + identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" + + "(\(radius.radiusIdentifier)\(corners.cornerIdentifier))" + } + if let backgroundColor = backgroundColor { + identifier += "_\(backgroundColor)" + } + + return identifier + }() + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + let size = targetSize ?? image.kf.size + return image.kf.scaled(to: options.scaleFactor) + .kf.image( + withRadius: radius, + fit: size, + roundingCorners: roundingCorners, + backgroundColor: backgroundColor) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +public struct Border { + public var color: KFCrossPlatformColor + public var lineWidth: CGFloat + + /// The radius will be applied in processing. Specify a certain point value with `.point`, or a fraction of the + /// target image with `.widthFraction`. or `.heightFraction`. For example, given a square image with width and + /// height equals, `.widthFraction(0.5)` means use half of the length of size and makes the final image a round one. + public var radius: Radius + + /// The target corners which will be applied rounding. + public var roundingCorners: RectCorner + + public init( + color: KFCrossPlatformColor = .black, + lineWidth: CGFloat = 4, + radius: Radius = .point(0), + roundingCorners: RectCorner = .all + ) { + self.color = color + self.lineWidth = lineWidth + self.radius = radius + self.roundingCorners = roundingCorners + } + + var identifier: String { + "\(color.rgbaDescription)_\(lineWidth)_\(radius.radiusIdentifier)_\(roundingCorners.cornerIdentifier)" + } +} + +public struct BorderImageProcessor: ImageProcessor { + public var identifier: String { "com.onevcat.Kingfisher.RoundCornerImageProcessor(\(border)" } + public let border: Border + + public init(border: Border) { + self.border = border + } + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.addingBorder(border) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Represents how a size adjusts itself to fit a target size. +/// +/// - none: Not scale the content. +/// - aspectFit: Scales the content to fit the size of the view by maintaining the aspect ratio. +/// - aspectFill: Scales the content to fill the size of the view. +public enum ContentMode { + /// Not scale the content. + case none + /// Scales the content to fit the size of the view by maintaining the aspect ratio. + case aspectFit + /// Scales the content to fill the size of the view. + case aspectFill +} + +/// Processor for resizing images. +/// If you need to resize a data represented image to a smaller size, use `DownsamplingImageProcessor` +/// instead, which is more efficient and uses less memory. +public struct ResizingImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// The reference size for resizing operation in point. + public let referenceSize: CGSize + + /// Target content mode of output image should be. + /// Default is `.none`. + public let targetContentMode: ContentMode + + /// Creates a `ResizingImageProcessor`. + /// + /// - Parameters: + /// - referenceSize: The reference size for resizing operation in point. + /// - mode: Target content mode of output image should be. + /// + /// - Note: + /// The instance of `ResizingImageProcessor` will follow its `mode` property + /// and try to resizing the input images to fit or fill the `referenceSize`. + /// That means if you are using a `mode` besides of `.none`, you may get an + /// image with its size not be the same as the `referenceSize`. + /// + /// **Example**: With input image size: {100, 200}, + /// `referenceSize`: {100, 100}, `mode`: `.aspectFit`, + /// you will get an output image with size of {50, 100}, which "fit"s + /// the `referenceSize`. + /// + /// If you need an output image exactly to be a specified size, append or use + /// a `CroppingImageProcessor`. + public init(referenceSize: CGSize, mode: ContentMode = .none) { + self.referenceSize = referenceSize + self.targetContentMode = mode + + if mode == .none { + self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(referenceSize))" + } else { + self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(referenceSize), \(mode))" + } + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.resize(to: referenceSize, for: targetContentMode) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for adding blur effect to images. `Accelerate.framework` is used underhood for +/// a better performance. A simulated Gaussian blur with specified blur radius will be applied. +public struct BlurImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Blur radius for the simulated Gaussian blur. + public let blurRadius: CGFloat + + /// Creates a `BlurImageProcessor` + /// + /// - parameter blurRadius: Blur radius for the simulated Gaussian blur. + public init(blurRadius: CGFloat) { + self.blurRadius = blurRadius + self.identifier = "com.onevcat.Kingfisher.BlurImageProcessor(\(blurRadius))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + let radius = blurRadius * options.scaleFactor + return image.kf.scaled(to: options.scaleFactor) + .kf.blurred(withRadius: radius) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for adding an overlay to images. Only CG-based images are supported in macOS. +public struct OverlayImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Overlay color will be used to overlay the input image. + public let overlay: KFCrossPlatformColor + + /// Fraction will be used when overlay the color to image. + public let fraction: CGFloat + + /// Creates an `OverlayImageProcessor` + /// + /// - parameter overlay: Overlay color will be used to overlay the input image. + /// - parameter fraction: Fraction will be used when overlay the color to image. + /// From 0.0 to 1.0. 0.0 means solid color, 1.0 means transparent overlay. + public init(overlay: KFCrossPlatformColor, fraction: CGFloat = 0.5) { + self.overlay = overlay + self.fraction = fraction + self.identifier = "com.onevcat.Kingfisher.OverlayImageProcessor(\(overlay.rgbaDescription)_\(fraction))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.overlaying(with: overlay, fraction: fraction) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for tint images with color. Only CG-based images are supported. +public struct TintImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Tint color will be used to tint the input image. + public let tint: KFCrossPlatformColor + + /// Creates a `TintImageProcessor` + /// + /// - parameter tint: Tint color will be used to tint the input image. + public init(tint: KFCrossPlatformColor) { + self.tint = tint + self.identifier = "com.onevcat.Kingfisher.TintImageProcessor(\(tint.rgbaDescription))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.tinted(with: tint) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for applying some color control to images. Only CG-based images are supported. +/// watchOS is not supported. +public struct ColorControlsProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Brightness changing to image. + public let brightness: CGFloat + + /// Contrast changing to image. + public let contrast: CGFloat + + /// Saturation changing to image. + public let saturation: CGFloat + + /// InputEV changing to image. + public let inputEV: CGFloat + + /// Creates a `ColorControlsProcessor` + /// + /// - Parameters: + /// - brightness: Brightness changing to image. + /// - contrast: Contrast changing to image. + /// - saturation: Saturation changing to image. + /// - inputEV: InputEV changing to image. + public init(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) { + self.brightness = brightness + self.contrast = contrast + self.saturation = saturation + self.inputEV = inputEV + self.identifier = "com.onevcat.Kingfisher.ColorControlsProcessor(\(brightness)_\(contrast)_\(saturation)_\(inputEV))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.adjusted(brightness: brightness, contrast: contrast, saturation: saturation, inputEV: inputEV) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for applying black and white effect to images. Only CG-based images are supported. +/// watchOS is not supported. +public struct BlackWhiteProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier = "com.onevcat.Kingfisher.BlackWhiteProcessor" + + /// Creates a `BlackWhiteProcessor` + public init() {} + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return ColorControlsProcessor(brightness: 0.0, contrast: 1.0, saturation: 0.0, inputEV: 0.7) + .process(item: item, options: options) + } +} + +/// Processor for cropping an image. Only CG-based images are supported. +/// watchOS is not supported. +public struct CroppingImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Target size of output image should be. + public let size: CGSize + + /// Anchor point from which the output size should be calculate. + /// The anchor point is consisted by two values between 0.0 and 1.0. + /// It indicates a related point in current image. + /// See `CroppingImageProcessor.init(size:anchor:)` for more. + public let anchor: CGPoint + + /// Creates a `CroppingImageProcessor`. + /// + /// - Parameters: + /// - size: Target size of output image should be. + /// - anchor: The anchor point from which the size should be calculated. + /// Default is `CGPoint(x: 0.5, y: 0.5)`, which means the center of input image. + /// - Note: + /// The anchor point is consisted by two values between 0.0 and 1.0. + /// It indicates a related point in current image, eg: (0.0, 0.0) for top-left + /// corner, (0.5, 0.5) for center and (1.0, 1.0) for bottom-right corner. + /// The `size` property of `CroppingImageProcessor` will be used along with + /// `anchor` to calculate a target rectangle in the size of image. + /// + /// The target size will be automatically calculated with a reasonable behavior. + /// For example, when you have an image size of `CGSize(width: 100, height: 100)`, + /// and a target size of `CGSize(width: 20, height: 20)`: + /// - with a (0.0, 0.0) anchor (top-left), the crop rect will be `{0, 0, 20, 20}`; + /// - with a (0.5, 0.5) anchor (center), it will be `{40, 40, 20, 20}` + /// - while with a (1.0, 1.0) anchor (bottom-right), it will be `{80, 80, 20, 20}` + public init(size: CGSize, anchor: CGPoint = CGPoint(x: 0.5, y: 0.5)) { + self.size = size + self.anchor = anchor + self.identifier = "com.onevcat.Kingfisher.CroppingImageProcessor(\(size)_\(anchor))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.crop(to: size, anchorOn: anchor) + case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for downsampling an image. Compared to `ResizingImageProcessor`, this processor +/// does not render the images to resize. Instead, it downsamples the input data directly to an +/// image. It is a more efficient than `ResizingImageProcessor`. Prefer to use `DownsamplingImageProcessor` as possible +/// as you can than the `ResizingImageProcessor`. +/// +/// Only CG-based images are supported. Animated images (like GIF) is not supported. +public struct DownsamplingImageProcessor: ImageProcessor { + + /// Target size of output image should be. It should be smaller than the size of + /// input image. If it is larger, the result image will be the same size of input + /// data without downsampling. + public let size: CGSize + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Creates a `DownsamplingImageProcessor`. + /// + /// - Parameter size: The target size of the downsample operation. + public init(size: CGSize) { + self.size = size + self.identifier = "com.onevcat.Kingfisher.DownsamplingImageProcessor(\(size))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + guard let data = image.kf.data(format: .unknown) else { + return nil + } + return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor) + case .data(let data): + return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor) + } + } +} + +infix operator |>: AdditionPrecedence +public func |>(left: ImageProcessor, right: ImageProcessor) -> ImageProcessor { + return left.append(another: right) +} + +extension KFCrossPlatformColor { + + var rgba: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + + #if os(macOS) + (usingColorSpace(.extendedSRGB) ?? self).getRed(&r, green: &g, blue: &b, alpha: &a) + #else + getRed(&r, green: &g, blue: &b, alpha: &a) + #endif + + return (r, g, b, a) + } + + var rgbaDescription: String { + let components = self.rgba + return String(format: "(%.2f,%.2f,%.2f,%.2f)", components.r, components.g, components.b, components.a) + } + + @available(*, deprecated, message: "`hex` is not safe for colors in extended space. Do not use this.") + var hex: String { + + let (r, g, b, a) = rgba + + let rInt = Int(r * 255) << 24 + let gInt = Int(g * 255) << 16 + let bInt = Int(b * 255) << 8 + let aInt = Int(a * 255) + + let rgba = rInt | gInt | bInt | aInt + + return String(format:"#%08x", rgba) + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageProgressive.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageProgressive.swift new file mode 100644 index 0000000..0e4f3cb --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageProgressive.swift @@ -0,0 +1,328 @@ +// +// ImageProgressive.swift +// Kingfisher +// +// Created by lixiang on 2019/5/10. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CoreGraphics + +private let sharedProcessingQueue: CallbackQueue = + .dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process")) + +public struct ImageProgressive { + + /// A default `ImageProgressive` could be used across. It blurs the progressive loading with the fastest + /// scan enabled and scan interval as 0. + public static let `default` = ImageProgressive( + isBlur: true, + isFastestScan: true, + scanInterval: 0 + ) + + /// Whether to enable blur effect processing + let isBlur: Bool + /// Whether to enable the fastest scan + let isFastestScan: Bool + /// Minimum time interval for each scan + let scanInterval: TimeInterval + + public init(isBlur: Bool, + isFastestScan: Bool, + scanInterval: TimeInterval + ) + { + self.isBlur = isBlur + self.isFastestScan = isFastestScan + self.scanInterval = scanInterval + } +} + +protocol ImageSettable: AnyObject { + var image: KFCrossPlatformImage? { get set } +} + +final class ImageProgressiveProvider: DataReceivingSideEffect { + + var onShouldApply: () -> Bool = { return true } + + func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) { + + DispatchQueue.main.async { + guard self.onShouldApply() else { return } + self.update(data: task.mutableData, with: task.callbacks) + } + } + + private let option: ImageProgressive + private let refresh: (KFCrossPlatformImage) -> Void + + private let decoder: ImageProgressiveDecoder + private let queue = ImageProgressiveSerialQueue() + + init?(_ options: KingfisherParsedOptionsInfo, + refresh: @escaping (KFCrossPlatformImage) -> Void) { + guard let option = options.progressiveJPEG else { return nil } + + self.option = option + self.refresh = refresh + self.decoder = ImageProgressiveDecoder( + option, + processingQueue: options.processingQueue ?? sharedProcessingQueue, + creatingOptions: options.imageCreatingOptions + ) + } + + func update(data: Data, with callbacks: [SessionDataTask.TaskCallback]) { + guard !data.isEmpty else { return } + + queue.add(minimum: option.scanInterval) { completion in + + func decode(_ data: Data) { + self.decoder.decode(data, with: callbacks) { image in + defer { completion() } + guard self.onShouldApply() else { return } + guard let image = image else { return } + self.refresh(image) + } + } + + let semaphore = DispatchSemaphore(value: 0) + var onShouldApply: Bool = false + + CallbackQueue.mainAsync.execute { + onShouldApply = self.onShouldApply() + semaphore.signal() + } + semaphore.wait() + guard onShouldApply else { + self.queue.clean() + completion() + return + } + + if self.option.isFastestScan { + decode(self.decoder.scanning(data) ?? Data()) + } else { + self.decoder.scanning(data).forEach { decode($0) } + } + } + } +} + +private final class ImageProgressiveDecoder { + + private let option: ImageProgressive + private let processingQueue: CallbackQueue + private let creatingOptions: ImageCreatingOptions + private(set) var scannedCount = 0 + private(set) var scannedIndex = -1 + + init(_ option: ImageProgressive, + processingQueue: CallbackQueue, + creatingOptions: ImageCreatingOptions) { + self.option = option + self.processingQueue = processingQueue + self.creatingOptions = creatingOptions + } + + func scanning(_ data: Data) -> [Data] { + guard data.kf.contains(jpeg: .SOF2) else { + return [] + } + guard scannedIndex + 1 < data.count else { + return [] + } + + var datas: [Data] = [] + var index = scannedIndex + 1 + var count = scannedCount + + while index < data.count - 1 { + scannedIndex = index + // 0xFF, 0xDA - Start Of Scan + let SOS = ImageFormat.JPEGMarker.SOS.bytes + if data[index] == SOS[0], data[index + 1] == SOS[1] { + if count > 0 { + datas.append(data[0 ..< index]) + } + count += 1 + } + index += 1 + } + + // Found more scans this the previous time + guard count > scannedCount else { return [] } + scannedCount = count + + // `> 1` checks that we've received a first scan (SOS) and then received + // and also received a second scan (SOS). This way we know that we have + // at least one full scan available. + guard count > 1 else { return [] } + return datas + } + + func scanning(_ data: Data) -> Data? { + guard data.kf.contains(jpeg: .SOF2) else { + return nil + } + guard scannedIndex + 1 < data.count else { + return nil + } + + var index = scannedIndex + 1 + var count = scannedCount + var lastSOSIndex = 0 + + while index < data.count - 1 { + scannedIndex = index + // 0xFF, 0xDA - Start Of Scan + let SOS = ImageFormat.JPEGMarker.SOS.bytes + if data[index] == SOS[0], data[index + 1] == SOS[1] { + lastSOSIndex = index + count += 1 + } + index += 1 + } + + // Found more scans this the previous time + guard count > scannedCount else { return nil } + scannedCount = count + + // `> 1` checks that we've received a first scan (SOS) and then received + // and also received a second scan (SOS). This way we know that we have + // at least one full scan available. + guard count > 1 && lastSOSIndex > 0 else { return nil } + return data[0 ..< lastSOSIndex] + } + + func decode(_ data: Data, + with callbacks: [SessionDataTask.TaskCallback], + completion: @escaping (KFCrossPlatformImage?) -> Void) { + guard data.kf.contains(jpeg: .SOF2) else { + CallbackQueue.mainCurrentOrAsync.execute { completion(nil) } + return + } + + func processing(_ data: Data) { + let processor = ImageDataProcessor( + data: data, + callbacks: callbacks, + processingQueue: processingQueue + ) + processor.onImageProcessed.delegate(on: self) { (self, result) in + guard let image = try? result.0.get() else { + CallbackQueue.mainCurrentOrAsync.execute { completion(nil) } + return + } + + CallbackQueue.mainCurrentOrAsync.execute { completion(image) } + } + processor.process() + } + + // Blur partial images. + let count = scannedCount + + if option.isBlur, count < 6 { + processingQueue.execute { + // Progressively reduce blur as we load more scans. + let image = KingfisherWrapper.image( + data: data, + options: self.creatingOptions + ) + let radius = max(2, 14 - count * 4) + let temp = image?.kf.blurred(withRadius: CGFloat(radius)) + processing(temp?.kf.data(format: .JPEG) ?? data) + } + + } else { + processing(data) + } + } +} + +private final class ImageProgressiveSerialQueue { + typealias ClosureCallback = ((@escaping () -> Void)) -> Void + + private let queue: DispatchQueue + private var items: [DispatchWorkItem] = [] + private var notify: (() -> Void)? + private var lastTime: TimeInterval? + var count: Int { return items.count } + + init() { + self.queue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageProgressive.SerialQueue") + } + + func add(minimum interval: TimeInterval, closure: @escaping ClosureCallback) { + let completion = { [weak self] in + guard let self = self else { return } + + self.queue.async { [weak self] in + guard let self = self else { return } + guard !self.items.isEmpty else { return } + + self.items.removeFirst() + + if let next = self.items.first { + self.queue.asyncAfter( + deadline: .now() + interval, + execute: next + ) + + } else { + self.lastTime = Date().timeIntervalSince1970 + self.notify?() + self.notify = nil + } + } + } + + queue.async { [weak self] in + guard let self = self else { return } + + let item = DispatchWorkItem { + closure(completion) + } + if self.items.isEmpty { + let difference = Date().timeIntervalSince1970 - (self.lastTime ?? 0) + let delay = difference < interval ? interval - difference : 0 + self.queue.asyncAfter(deadline: .now() + delay, execute: item) + } + self.items.append(item) + } + } + + func notify(_ closure: @escaping () -> Void) { + self.notify = closure + } + + func clean() { + queue.async { [weak self] in + guard let self = self else { return } + self.items.forEach { $0.cancel() } + self.items.removeAll() + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageTransition.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageTransition.swift new file mode 100644 index 0000000..4d042df --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageTransition.swift @@ -0,0 +1,118 @@ +// +// ImageTransition.swift +// Kingfisher +// +// Created by Wei Wang on 15/9/18. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +#if os(iOS) || os(tvOS) +import UIKit + +/// Transition effect which will be used when an image downloaded and set by `UIImageView` +/// extension API in Kingfisher. You can assign an enum value with transition duration as +/// an item in `KingfisherOptionsInfo` to enable the animation transition. +/// +/// Apple's UIViewAnimationOptions is used under the hood. +/// For custom transition, you should specified your own transition options, animations and +/// completion handler as well. +/// +/// - none: No animation transition. +/// - fade: Fade in the loaded image in a given duration. +/// - flipFromLeft: Flip from left transition. +/// - flipFromRight: Flip from right transition. +/// - flipFromTop: Flip from top transition. +/// - flipFromBottom: Flip from bottom transition. +/// - custom: Custom transition. +public enum ImageTransition { + /// No animation transition. + case none + /// Fade in the loaded image in a given duration. + case fade(TimeInterval) + /// Flip from left transition. + case flipFromLeft(TimeInterval) + /// Flip from right transition. + case flipFromRight(TimeInterval) + /// Flip from top transition. + case flipFromTop(TimeInterval) + /// Flip from bottom transition. + case flipFromBottom(TimeInterval) + /// Custom transition defined by a general animation block. + /// - duration: The time duration of this custom transition. + /// - options: `UIView.AnimationOptions` should be used in the transition. + /// - animations: The animation block will be applied when setting image. + /// - completion: A block called when the transition animation finishes. + case custom(duration: TimeInterval, + options: UIView.AnimationOptions, + animations: ((UIImageView, UIImage) -> Void)?, + completion: ((Bool) -> Void)?) + + var duration: TimeInterval { + switch self { + case .none: return 0 + case .fade(let duration): return duration + + case .flipFromLeft(let duration): return duration + case .flipFromRight(let duration): return duration + case .flipFromTop(let duration): return duration + case .flipFromBottom(let duration): return duration + + case .custom(let duration, _, _, _): return duration + } + } + + var animationOptions: UIView.AnimationOptions { + switch self { + case .none: return [] + case .fade: return .transitionCrossDissolve + + case .flipFromLeft: return .transitionFlipFromLeft + case .flipFromRight: return .transitionFlipFromRight + case .flipFromTop: return .transitionFlipFromTop + case .flipFromBottom: return .transitionFlipFromBottom + + case .custom(_, let options, _, _): return options + } + } + + var animations: ((UIImageView, UIImage) -> Void)? { + switch self { + case .custom(_, _, let animations, _): return animations + default: return { $0.image = $1 } + } + } + + var completion: ((Bool) -> Void)? { + switch self { + case .custom(_, _, _, let completion): return completion + default: return nil + } + } +} +#else +// Just a placeholder for compiling on macOS. +public enum ImageTransition { + case none + /// This is a placeholder on macOS now. It is for SwiftUI (KFImage) to identify the fade option only. + case fade(TimeInterval) +} +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/Placeholder.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/Placeholder.swift new file mode 100644 index 0000000..94d9e3a --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/Placeholder.swift @@ -0,0 +1,82 @@ +// +// Placeholder.swift +// Kingfisher +// +// Created by Tieme van Veen on 28/08/2017. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if !os(watchOS) + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif + +#if canImport(UIKit) +import UIKit +#endif + +/// Represents a placeholder type which could be set while loading as well as +/// loading finished without getting an image. +public protocol Placeholder { + + /// How the placeholder should be added to a given image view. + func add(to imageView: KFCrossPlatformImageView) + + /// How the placeholder should be removed from a given image view. + func remove(from imageView: KFCrossPlatformImageView) +} + +/// Default implementation of an image placeholder. The image will be set or +/// reset directly for `image` property of the image view. +extension KFCrossPlatformImage: Placeholder { + /// How the placeholder should be added to a given image view. + public func add(to imageView: KFCrossPlatformImageView) { imageView.image = self } + + /// How the placeholder should be removed from a given image view. + public func remove(from imageView: KFCrossPlatformImageView) { imageView.image = nil } +} + +/// Default implementation of an arbitrary view as placeholder. The view will be +/// added as a subview when adding and be removed from its super view when removing. +/// +/// To use your customize View type as placeholder, simply let it conforming to +/// `Placeholder` by `extension MyView: Placeholder {}`. +extension Placeholder where Self: KFCrossPlatformView { + + /// How the placeholder should be added to a given image view. + public func add(to imageView: KFCrossPlatformImageView) { + imageView.addSubview(self) + translatesAutoresizingMaskIntoConstraints = false + + centerXAnchor.constraint(equalTo: imageView.centerXAnchor).isActive = true + centerYAnchor.constraint(equalTo: imageView.centerYAnchor).isActive = true + heightAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true + widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true + } + + /// How the placeholder should be removed from a given image view. + public func remove(from imageView: KFCrossPlatformImageView) { + removeFromSuperview() + } +} + +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift new file mode 100644 index 0000000..d3b3ea1 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift @@ -0,0 +1,94 @@ +// +// AuthenticationChallengeResponsable.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +@available(*, deprecated, message: "Typo. Use `AuthenticationChallengeResponsible` instead", renamed: "AuthenticationChallengeResponsible") +public typealias AuthenticationChallengeResponsable = AuthenticationChallengeResponsible + +/// Protocol indicates that an authentication challenge could be handled. +public protocol AuthenticationChallengeResponsible: AnyObject { + + /// Called when a session level authentication challenge is received. + /// This method provide a chance to handle and response to the authentication + /// challenge before downloading could start. + /// + /// - Parameters: + /// - downloader: The downloader which receives this challenge. + /// - challenge: An object that contains the request for authentication. + /// - completionHandler: A handler that your delegate method must call. + /// + /// - Note: This method is a forward from `URLSessionDelegate.urlSession(:didReceiveChallenge:completionHandler:)`. + /// Please refer to the document of it in `URLSessionDelegate`. + func downloader( + _ downloader: ImageDownloader, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + + /// Called when a task level authentication challenge is received. + /// This method provide a chance to handle and response to the authentication + /// challenge before downloading could start. + /// + /// - Parameters: + /// - downloader: The downloader which receives this challenge. + /// - task: The task whose request requires authentication. + /// - challenge: An object that contains the request for authentication. + /// - completionHandler: A handler that your delegate method must call. + func downloader( + _ downloader: ImageDownloader, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) +} + +extension AuthenticationChallengeResponsible { + + public func downloader( + _ downloader: ImageDownloader, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) { + let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!) + completionHandler(.useCredential, credential) + return + } + } + + completionHandler(.performDefaultHandling, nil) + } + + public func downloader( + _ downloader: ImageDownloader, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + completionHandler(.performDefaultHandling, nil) + } + +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift new file mode 100644 index 0000000..b368972 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift @@ -0,0 +1,74 @@ +// +// ImageDataProcessor.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +private let sharedProcessingQueue: CallbackQueue = + .dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process")) + +// Handles image processing work on an own process queue. +class ImageDataProcessor { + let data: Data + let callbacks: [SessionDataTask.TaskCallback] + let queue: CallbackQueue + + // Note: We have an optimization choice there, to reduce queue dispatch by checking callback + // queue settings in each option... + let onImageProcessed = Delegate<(Result, SessionDataTask.TaskCallback), Void>() + + init(data: Data, callbacks: [SessionDataTask.TaskCallback], processingQueue: CallbackQueue?) { + self.data = data + self.callbacks = callbacks + self.queue = processingQueue ?? sharedProcessingQueue + } + + func process() { + queue.execute(doProcess) + } + + private func doProcess() { + var processedImages = [String: KFCrossPlatformImage]() + for callback in callbacks { + let processor = callback.options.processor + var image = processedImages[processor.identifier] + if image == nil { + image = processor.process(item: .data(data), options: callback.options) + processedImages[processor.identifier] = image + } + + let result: Result + if let image = image { + let finalImage = callback.options.backgroundDecode ? image.kf.decoded : image + result = .success(finalImage) + } else { + let error = KingfisherError.processorError( + reason: .processingFailed(processor: processor, item: .data(data))) + result = .failure(error) + } + onImageProcessed.call((result, callback)) + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift new file mode 100644 index 0000000..d50ed9a --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift @@ -0,0 +1,488 @@ +// +// ImageDownloader.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +typealias DownloadResult = Result + +/// Represents a success result of an image downloading progress. +public struct ImageLoadingResult { + + /// The downloaded image. + public let image: KFCrossPlatformImage + + /// Original URL of the image request. + public let url: URL? + + /// The raw data received from downloader. + public let originalData: Data +} + +/// Represents a task of an image downloading process. +public struct DownloadTask { + + /// The `SessionDataTask` object bounded to this download task. Multiple `DownloadTask`s could refer + /// to a same `sessionTask`. This is an optimization in Kingfisher to prevent multiple downloading task + /// for the same URL resource at the same time. + /// + /// When you `cancel` a `DownloadTask`, this `SessionDataTask` and its cancel token will be pass through. + /// You can use them to identify the cancelled task. + public let sessionTask: SessionDataTask + + /// The cancel token which is used to cancel the task. This is only for identify the task when it is cancelled. + /// To cancel a `DownloadTask`, use `cancel` instead. + public let cancelToken: SessionDataTask.CancelToken + + /// Cancel this task if it is running. It will do nothing if this task is not running. + /// + /// - Note: + /// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is being + /// downloading. However, even when internally no new session task created, a `DownloadTask` will be still created + /// and returned when you call related methods, but it will share the session downloading task with a previous task. + /// In this case, if multiple `DownloadTask`s share a single session download task, cancelling a `DownloadTask` + /// does not affect other `DownloadTask`s. + /// + /// If you need to cancel all `DownloadTask`s of a url, use `ImageDownloader.cancel(url:)`. If you need to cancel + /// all downloading tasks of an `ImageDownloader`, use `ImageDownloader.cancelAll()`. + public func cancel() { + sessionTask.cancel(token: cancelToken) + } +} + +extension DownloadTask { + enum WrappedTask { + case download(DownloadTask) + case dataProviding + + func cancel() { + switch self { + case .download(let task): task.cancel() + case .dataProviding: break + } + } + + var value: DownloadTask? { + switch self { + case .download(let task): return task + case .dataProviding: return nil + } + } + } +} + +/// Represents a downloading manager for requesting the image with a URL from server. +open class ImageDownloader { + + // MARK: Singleton + /// The default downloader. + public static let `default` = ImageDownloader(name: "default") + + // MARK: Public Properties + /// The duration before the downloading is timeout. Default is 15 seconds. + open var downloadTimeout: TimeInterval = 15.0 + + /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this + /// set will be ignored. You can use this set to specify the self-signed site. It only will be used if you don't + /// specify the `authenticationChallengeResponder`. + /// + /// If `authenticationChallengeResponder` is set, this property will be ignored and the implementation of + /// `authenticationChallengeResponder` will be used instead. + open var trustedHosts: Set? + + /// Use this to set supply a configuration for the downloader. By default, + /// NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used. + /// + /// You could change the configuration before a downloading task starts. + /// A configuration without persistent storage for caches is requested for downloader working correctly. + open var sessionConfiguration = URLSessionConfiguration.ephemeral { + didSet { + session.invalidateAndCancel() + session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) + } + } + open var sessionDelegate: SessionDelegate { + didSet { + session.invalidateAndCancel() + session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) + setupSessionHandler() + } + } + + /// Whether the download requests should use pipeline or not. Default is false. + open var requestsUsePipelining = false + + /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more. + open weak var delegate: ImageDownloaderDelegate? + + /// A responder for authentication challenge. + /// Downloader will forward the received authentication challenge for the downloading session to this responder. + open weak var authenticationChallengeResponder: AuthenticationChallengeResponsible? + + private let name: String + private var session: URLSession + + // MARK: Initializers + + /// Creates a downloader with name. + /// + /// - Parameter name: The name for the downloader. It should not be empty. + public init(name: String) { + if name.isEmpty { + fatalError("[Kingfisher] You should specify a name for the downloader. " + + "A downloader with empty name is not permitted.") + } + + self.name = name + + sessionDelegate = SessionDelegate() + session = URLSession( + configuration: sessionConfiguration, + delegate: sessionDelegate, + delegateQueue: nil) + + authenticationChallengeResponder = self + setupSessionHandler() + } + + deinit { session.invalidateAndCancel() } + + private func setupSessionHandler() { + sessionDelegate.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in + self.authenticationChallengeResponder?.downloader(self, didReceive: invoke.1, completionHandler: invoke.2) + } + sessionDelegate.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in + self.authenticationChallengeResponder?.downloader( + self, task: invoke.1, didReceive: invoke.2, completionHandler: invoke.3) + } + sessionDelegate.onValidStatusCode.delegate(on: self) { (self, code) in + return (self.delegate ?? self).isValidStatusCode(code, for: self) + } + sessionDelegate.onDownloadingFinished.delegate(on: self) { (self, value) in + let (url, result) = value + do { + let value = try result.get() + self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: value, error: nil) + } catch { + self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: nil, error: error) + } + } + sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in + return (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, with: task) + } + } + + // Wraps `completionHandler` to `onCompleted` respectively. + private func createCompletionCallBack(_ completionHandler: ((DownloadResult) -> Void)?) -> Delegate? { + return completionHandler.map { block -> Delegate in + + let delegate = Delegate, Void>() + delegate.delegate(on: self) { (self, callback) in + block(callback) + } + return delegate + } + } + + private func createTaskCallback( + _ completionHandler: ((DownloadResult) -> Void)?, + options: KingfisherParsedOptionsInfo + ) -> SessionDataTask.TaskCallback + { + return SessionDataTask.TaskCallback( + onCompleted: createCompletionCallBack(completionHandler), + options: options + ) + } + + private func createDownloadContext( + with url: URL, + options: KingfisherParsedOptionsInfo, + done: @escaping ((Result) -> Void) + ) + { + func checkRequestAndDone(r: URLRequest) { + + // There is a possibility that request modifier changed the url to `nil` or empty. + // In this case, throw an error. + guard let url = r.url, !url.absoluteString.isEmpty else { + done(.failure(KingfisherError.requestError(reason: .invalidURL(request: r)))) + return + } + + done(.success(DownloadingContext(url: url, request: r, options: options))) + } + + // Creates default request. + var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout) + request.httpShouldUsePipelining = requestsUsePipelining + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) , options.lowDataModeSource != nil { + request.allowsConstrainedNetworkAccess = false + } + + if let requestModifier = options.requestModifier { + // Modifies request before sending. + requestModifier.modified(for: request) { result in + guard let finalRequest = result else { + done(.failure(KingfisherError.requestError(reason: .emptyRequest))) + return + } + checkRequestAndDone(r: finalRequest) + } + } else { + checkRequestAndDone(r: request) + } + } + + private func addDownloadTask( + context: DownloadingContext, + callback: SessionDataTask.TaskCallback + ) -> DownloadTask + { + // Ready to start download. Add it to session task manager (`sessionHandler`) + let downloadTask: DownloadTask + if let existingTask = sessionDelegate.task(for: context.url) { + downloadTask = sessionDelegate.append(existingTask, url: context.url, callback: callback) + } else { + let sessionDataTask = session.dataTask(with: context.request) + sessionDataTask.priority = context.options.downloadPriority + downloadTask = sessionDelegate.add(sessionDataTask, url: context.url, callback: callback) + } + return downloadTask + } + + + private func reportWillDownloadImage(url: URL, request: URLRequest) { + delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request) + } + + private func reportDidDownloadImageData(result: Result<(Data, URLResponse?), KingfisherError>, url: URL) { + var response: URLResponse? + var err: Error? + do { + response = try result.get().1 + } catch { + err = error + } + self.delegate?.imageDownloader( + self, + didFinishDownloadingImageForURL: url, + with: response, + error: err + ) + } + + private func reportDidProcessImage( + result: Result, url: URL, response: URLResponse? + ) + { + if let image = try? result.get() { + self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response) + } + + } + + private func startDownloadTask( + context: DownloadingContext, + callback: SessionDataTask.TaskCallback + ) -> DownloadTask + { + + let downloadTask = addDownloadTask(context: context, callback: callback) + + let sessionTask = downloadTask.sessionTask + guard !sessionTask.started else { + return downloadTask + } + + sessionTask.onTaskDone.delegate(on: self) { (self, done) in + // Underlying downloading finishes. + // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback] + let (result, callbacks) = done + + // Before processing the downloaded data. + self.reportDidDownloadImageData(result: result, url: context.url) + + switch result { + // Download finished. Now process the data to an image. + case .success(let (data, response)): + let processor = ImageDataProcessor( + data: data, callbacks: callbacks, processingQueue: context.options.processingQueue + ) + processor.onImageProcessed.delegate(on: self) { (self, done) in + // `onImageProcessed` will be called for `callbacks.count` times, with each + // `SessionDataTask.TaskCallback` as the input parameter. + // result: Result, callback: SessionDataTask.TaskCallback + let (result, callback) = done + + self.reportDidProcessImage(result: result, url: context.url, response: response) + + let imageResult = result.map { ImageLoadingResult(image: $0, url: context.url, originalData: data) } + let queue = callback.options.callbackQueue + queue.execute { callback.onCompleted?.call(imageResult) } + } + processor.process() + + case .failure(let error): + callbacks.forEach { callback in + let queue = callback.options.callbackQueue + queue.execute { callback.onCompleted?.call(.failure(error)) } + } + } + } + + reportWillDownloadImage(url: context.url, request: context.request) + sessionTask.resume() + return downloadTask + } + + // MARK: Downloading Task + /// Downloads an image with a URL and option. Invoked internally by Kingfisher. Subclasses must invoke super. + /// + /// - Parameters: + /// - url: Target URL. + /// - options: The options could control download behavior. See `KingfisherOptionsInfo`. + /// - completionHandler: Called when the download progress finishes. This block will be called in the queue + /// defined in `.callbackQueue` in `options` parameter. + /// - Returns: A downloading task. You could call `cancel` on it to stop the download task. + @discardableResult + open func downloadImage( + with url: URL, + options: KingfisherParsedOptionsInfo, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var downloadTask: DownloadTask? + createDownloadContext(with: url, options: options) { result in + switch result { + case .success(let context): + // `downloadTask` will be set if the downloading started immediately. This is the case when no request + // modifier or a sync modifier (`ImageDownloadRequestModifier`) is used. Otherwise, when an + // `AsyncImageDownloadRequestModifier` is used the returned `downloadTask` of this method will be `nil` + // and the actual "delayed" task is given in `AsyncImageDownloadRequestModifier.onDownloadTaskStarted` + // callback. + downloadTask = self.startDownloadTask( + context: context, + callback: self.createTaskCallback(completionHandler, options: options) + ) + if let modifier = options.requestModifier { + modifier.onDownloadTaskStarted?(downloadTask) + } + case .failure(let error): + options.callbackQueue.execute { + completionHandler?(.failure(error)) + } + } + } + + return downloadTask + } + + /// Downloads an image with a URL and option. + /// + /// - Parameters: + /// - url: Target URL. + /// - options: The options could control download behavior. See `KingfisherOptionsInfo`. + /// - progressBlock: Called when the download progress updated. This block will be always be called in main queue. + /// - completionHandler: Called when the download progress finishes. This block will be called in the queue + /// defined in `.callbackQueue` in `options` parameter. + /// - Returns: A downloading task. You could call `cancel` on it to stop the download task. + @discardableResult + open func downloadImage( + with url: URL, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var info = KingfisherParsedOptionsInfo(options) + if let block = progressBlock { + info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + return downloadImage( + with: url, + options: info, + completionHandler: completionHandler) + } + + /// Downloads an image with a URL and option. + /// + /// - Parameters: + /// - url: Target URL. + /// - options: The options could control download behavior. See `KingfisherOptionsInfo`. + /// - completionHandler: Called when the download progress finishes. This block will be called in the queue + /// defined in `.callbackQueue` in `options` parameter. + /// - Returns: A downloading task. You could call `cancel` on it to stop the download task. + @discardableResult + open func downloadImage( + with url: URL, + options: KingfisherOptionsInfo? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + downloadImage( + with: url, + options: KingfisherParsedOptionsInfo(options), + completionHandler: completionHandler + ) + } +} + +// MARK: Cancelling Task +extension ImageDownloader { + + /// Cancel all downloading tasks for this `ImageDownloader`. It will trigger the completion handlers + /// for all not-yet-finished downloading tasks. + /// + /// If you need to only cancel a certain task, call `cancel()` on the `DownloadTask` + /// returned by the downloading methods. If you need to cancel all `DownloadTask`s of a certain url, + /// use `ImageDownloader.cancel(url:)`. + public func cancelAll() { + sessionDelegate.cancelAll() + } + + /// Cancel all downloading tasks for a given URL. It will trigger the completion handlers for + /// all not-yet-finished downloading tasks for the URL. + /// + /// - Parameter url: The URL which you want to cancel downloading. + public func cancel(url: URL) { + sessionDelegate.cancel(url: url) + } +} + +// Use the default implementation from extension of `AuthenticationChallengeResponsible`. +extension ImageDownloader: AuthenticationChallengeResponsible {} + +// Use the default implementation from extension of `ImageDownloaderDelegate`. +extension ImageDownloader: ImageDownloaderDelegate {} + +extension ImageDownloader { + struct DownloadingContext { + let url: URL + let request: URLRequest + let options: KingfisherParsedOptionsInfo + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift new file mode 100644 index 0000000..7f98640 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift @@ -0,0 +1,154 @@ +// +// ImageDownloaderDelegate.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Protocol of `ImageDownloader`. This protocol provides a set of methods which are related to image downloader +/// working stages and rules. +public protocol ImageDownloaderDelegate: AnyObject { + + /// Called when the `ImageDownloader` object will start downloading an image from a specified URL. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object which is used for the downloading operation. + /// - url: URL of the starting request. + /// - request: The request object for the download process. + /// + func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?) + + /// Called when the `ImageDownloader` completes a downloading request with success or failure. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object which is used for the downloading operation. + /// - url: URL of the original request URL. + /// - response: The response object of the downloading process. + /// - error: The error in case of failure. + /// + func imageDownloader( + _ downloader: ImageDownloader, + didFinishDownloadingImageForURL url: URL, + with response: URLResponse?, + error: Error?) + + /// Called when the `ImageDownloader` object successfully downloaded image data from specified URL. This is + /// your last chance to verify or modify the downloaded data before Kingfisher tries to perform addition + /// processing on the image data. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object which is used for the downloading operation. + /// - data: The original downloaded data. + /// - dataTask: The data task contains request and response information of the download. + /// - Note: + /// This can be used to pre-process raw image data before creation of `Image` instance (i.e. + /// decrypting or verification). If `nil` returned, the processing is interrupted and a `KingfisherError` with + /// `ResponseErrorReason.dataModifyingFailed` will be raised. You could use this fact to stop the image + /// processing flow if you find the data is corrupted or malformed. + /// + /// If this method is implemented, `imageDownloader(_:didDownload:for:)` will not be called anymore. + func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with dataTask: SessionDataTask) -> Data? + + /// Called when the `ImageDownloader` object successfully downloaded image data from specified URL. This is + /// your last chance to verify or modify the downloaded data before Kingfisher tries to perform addition + /// processing on the image data. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object which is used for the downloading operation. + /// - data: The original downloaded data. + /// - url: The URL of the original request URL. + /// - Returns: The data from which Kingfisher should use to create an image. You need to provide valid data + /// which content is one of the supported image file format. Kingfisher will perform process on this + /// data and try to convert it to an image object. + /// - Note: + /// This can be used to pre-process raw image data before creation of `Image` instance (i.e. + /// decrypting or verification). If `nil` returned, the processing is interrupted and a `KingfisherError` with + /// `ResponseErrorReason.dataModifyingFailed` will be raised. You could use this fact to stop the image + /// processing flow if you find the data is corrupted or malformed. + /// + /// If `imageDownloader(_:didDownload:with:)` is implemented, this method will not be called anymore. + func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? + + /// Called when the `ImageDownloader` object successfully downloads and processes an image from specified URL. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object which is used for the downloading operation. + /// - image: The downloaded and processed image. + /// - url: URL of the original request URL. + /// - response: The original response object of the downloading process. + /// + func imageDownloader( + _ downloader: ImageDownloader, + didDownload image: KFCrossPlatformImage, + for url: URL, + with response: URLResponse?) + + /// Checks if a received HTTP status code is valid or not. + /// By default, a status code in range 200..<400 is considered as valid. + /// If an invalid code is received, the downloader will raise an `KingfisherError` with + /// `ResponseErrorReason.invalidHTTPStatusCode` as its reason. + /// + /// - Parameters: + /// - code: The received HTTP status code. + /// - downloader: The `ImageDownloader` object asks for validate status code. + /// - Returns: Returns a value to indicate whether this HTTP status code is valid or not. + /// - Note: If the default 200 to 400 valid code does not suit your need, + /// you can implement this method to change that behavior. + func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool +} + +// Default implementation for `ImageDownloaderDelegate`. +extension ImageDownloaderDelegate { + public func imageDownloader( + _ downloader: ImageDownloader, + willDownloadImageForURL url: URL, + with request: URLRequest?) {} + + public func imageDownloader( + _ downloader: ImageDownloader, + didFinishDownloadingImageForURL url: URL, + with response: URLResponse?, + error: Error?) {} + + public func imageDownloader( + _ downloader: ImageDownloader, + didDownload image: KFCrossPlatformImage, + for url: URL, + with response: URLResponse?) {} + + public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool { + return (200..<400).contains(code) + } + + public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with task: SessionDataTask) -> Data? { + guard let url = task.originalURL else { + return data + } + return imageDownloader(downloader, didDownload: data, for: url) + } + + public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? { + return data + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageModifier.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageModifier.swift new file mode 100644 index 0000000..0acd0e8 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageModifier.swift @@ -0,0 +1,116 @@ +// +// ImageModifier.swift +// Kingfisher +// +// Created by Ethan Gill on 2017/11/28. +// +// Copyright (c) 2019 Ethan Gill +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// An `ImageModifier` can be used to change properties on an image between cache serialization and the actual use of +/// the image. The `modify(_:)` method will be called after the image retrieved from its source and before it returned +/// to the caller. This modified image is expected to be only used for rendering purpose, any changes applied by the +/// `ImageModifier` will not be serialized or cached. +public protocol ImageModifier { + /// Modify an input `Image`. + /// + /// - parameter image: Image which will be modified by `self` + /// + /// - returns: The modified image. + /// + /// - Note: The return value will be unmodified if modifying is not possible on + /// the current platform. + /// - Note: Most modifiers support UIImage or NSImage, but not CGImage. + func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage +} + +/// A wrapper for creating an `ImageModifier` easier. +/// This type conforms to `ImageModifier` and wraps an image modify block. +/// If the `block` throws an error, the original image will be used. +public struct AnyImageModifier: ImageModifier { + + /// A block which modifies images, or returns the original image + /// if modification cannot be performed with an error. + let block: (KFCrossPlatformImage) throws -> KFCrossPlatformImage + + /// Creates an `AnyImageModifier` with a given `modify` block. + public init(modify: @escaping (KFCrossPlatformImage) throws -> KFCrossPlatformImage) { + block = modify + } + + /// Modify an input `Image`. See `ImageModifier` protocol for more. + public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { + return (try? block(image)) ?? image + } +} + +#if os(iOS) || os(tvOS) || os(watchOS) +import UIKit + +/// Modifier for setting the rendering mode of images. +public struct RenderingModeImageModifier: ImageModifier { + + /// The rendering mode to apply to the image. + public let renderingMode: UIImage.RenderingMode + + /// Creates a `RenderingModeImageModifier`. + /// + /// - Parameter renderingMode: The rendering mode to apply to the image. Default is `.automatic`. + public init(renderingMode: UIImage.RenderingMode = .automatic) { + self.renderingMode = renderingMode + } + + /// Modify an input `Image`. See `ImageModifier` protocol for more. + public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { + return image.withRenderingMode(renderingMode) + } +} + +/// Modifier for setting the `flipsForRightToLeftLayoutDirection` property of images. +public struct FlipsForRightToLeftLayoutDirectionImageModifier: ImageModifier { + + /// Creates a `FlipsForRightToLeftLayoutDirectionImageModifier`. + public init() {} + + /// Modify an input `Image`. See `ImageModifier` protocol for more. + public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { + return image.imageFlippedForRightToLeftLayoutDirection() + } +} + +/// Modifier for setting the `alignmentRectInsets` property of images. +public struct AlignmentRectInsetsImageModifier: ImageModifier { + + /// The alignment insets to apply to the image + public let alignmentInsets: UIEdgeInsets + + /// Creates an `AlignmentRectInsetsImageModifier`. + public init(alignmentInsets: UIEdgeInsets) { + self.alignmentInsets = alignmentInsets + } + + /// Modify an input `Image`. See `ImageModifier` protocol for more. + public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { + return image.withAlignmentRectInsets(alignmentInsets) + } +} +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift new file mode 100644 index 0000000..3fce14c --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift @@ -0,0 +1,442 @@ +// +// ImagePrefetcher.swift +// Kingfisher +// +// Created by Claire Knight on 24/02/2016 +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// Progress update block of prefetcher when initialized with a list of resources. +/// +/// - `skippedResources`: An array of resources that are already cached before the prefetching starting. +/// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while +/// downloading, encountered an error when downloading or the download not being started at all. +/// - `completedResources`: An array of resources that are downloaded and cached successfully. +public typealias PrefetcherProgressBlock = + ((_ skippedResources: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> Void) + +/// Progress update block of prefetcher when initialized with a list of resources. +/// +/// - `skippedSources`: An array of sources that are already cached before the prefetching starting. +/// - `failedSources`: An array of sources that fail to be fetched. +/// - `completedResources`: An array of sources that are fetched and cached successfully. +public typealias PrefetcherSourceProgressBlock = + ((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void) + +/// Completion block of prefetcher when initialized with a list of sources. +/// +/// - `skippedResources`: An array of resources that are already cached before the prefetching starting. +/// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while +/// downloading, encountered an error when downloading or the download not being started at all. +/// - `completedResources`: An array of resources that are downloaded and cached successfully. +public typealias PrefetcherCompletionHandler = + ((_ skippedResources: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> Void) + +/// Completion block of prefetcher when initialized with a list of sources. +/// +/// - `skippedSources`: An array of sources that are already cached before the prefetching starting. +/// - `failedSources`: An array of sources that fail to be fetched. +/// - `completedSources`: An array of sources that are fetched and cached successfully. +public typealias PrefetcherSourceCompletionHandler = + ((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void) + +/// `ImagePrefetcher` represents a downloading manager for requesting many images via URLs, then caching them. +/// This is useful when you know a list of image resources and want to download them before showing. It also works with +/// some Cocoa prefetching mechanism like table view or collection view `prefetchDataSource`, to start image downloading +/// and caching before they display on screen. +public class ImagePrefetcher: CustomStringConvertible { + + public var description: String { + return "\(Unmanaged.passUnretained(self).toOpaque())" + } + + /// The maximum concurrent downloads to use when prefetching images. Default is 5. + public var maxConcurrentDownloads = 5 + + private let prefetchSources: [Source] + private let optionsInfo: KingfisherParsedOptionsInfo + + private var progressBlock: PrefetcherProgressBlock? + private var completionHandler: PrefetcherCompletionHandler? + + private var progressSourceBlock: PrefetcherSourceProgressBlock? + private var completionSourceHandler: PrefetcherSourceCompletionHandler? + + private var tasks = [String: DownloadTask.WrappedTask]() + + private var pendingSources: ArraySlice + private var skippedSources = [Source]() + private var completedSources = [Source]() + private var failedSources = [Source]() + + private var stopped = false + + // A manager used for prefetching. We will use the helper methods in manager. + private let manager: KingfisherManager + + private let pretchQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.pretchQueue") + private static let requestingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.requestingQueue") + + private var finished: Bool { + let totalFinished: Int = failedSources.count + skippedSources.count + completedSources.count + return totalFinished == prefetchSources.count && tasks.isEmpty + } + + /// Creates an image prefetcher with an array of URLs. + /// + /// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable. + /// After you get a valid `ImagePrefetcher` object, you call `start()` on it to begin the prefetching process. + /// The images which are already cached will be skipped without downloading again. + /// + /// - Parameters: + /// - urls: The URLs which should be prefetched. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called every time an resource is downloaded, skipped or cancelled. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init( + urls: [URL], + options: KingfisherOptionsInfo? = nil, + progressBlock: PrefetcherProgressBlock? = nil, + completionHandler: PrefetcherCompletionHandler? = nil) + { + let resources: [Resource] = urls.map { $0 } + self.init( + resources: resources, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + /// Creates an image prefetcher with an array of URLs. + /// + /// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable. + /// After you get a valid `ImagePrefetcher` object, you call `start()` on it to begin the prefetching process. + /// The images which are already cached will be skipped without downloading again. + /// + /// - Parameters: + /// - urls: The URLs which should be prefetched. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init( + urls: [URL], + options: KingfisherOptionsInfo? = nil, + completionHandler: PrefetcherCompletionHandler? = nil) + { + let resources: [Resource] = urls.map { $0 } + self.init( + resources: resources, + options: options, + progressBlock: nil, + completionHandler: completionHandler) + } + + /// Creates an image prefetcher with an array of resources. + /// + /// - Parameters: + /// - resources: The resources which should be prefetched. See `Resource` type for more. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called every time an resource is downloaded, skipped or cancelled. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init( + resources: [Resource], + options: KingfisherOptionsInfo? = nil, + progressBlock: PrefetcherProgressBlock? = nil, + completionHandler: PrefetcherCompletionHandler? = nil) + { + self.init(sources: resources.map { $0.convertToSource() }, options: options) + self.progressBlock = progressBlock + self.completionHandler = completionHandler + } + + /// Creates an image prefetcher with an array of resources. + /// + /// - Parameters: + /// - resources: The resources which should be prefetched. See `Resource` type for more. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init( + resources: [Resource], + options: KingfisherOptionsInfo? = nil, + completionHandler: PrefetcherCompletionHandler? = nil) + { + self.init(sources: resources.map { $0.convertToSource() }, options: options) + self.completionHandler = completionHandler + } + + /// Creates an image prefetcher with an array of sources. + /// + /// - Parameters: + /// - sources: The sources which should be prefetched. See `Source` type for more. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called every time an source fetching successes, fails, is skipped. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init(sources: [Source], + options: KingfisherOptionsInfo? = nil, + progressBlock: PrefetcherSourceProgressBlock? = nil, + completionHandler: PrefetcherSourceCompletionHandler? = nil) + { + self.init(sources: sources, options: options) + self.progressSourceBlock = progressBlock + self.completionSourceHandler = completionHandler + } + + /// Creates an image prefetcher with an array of sources. + /// + /// - Parameters: + /// - sources: The sources which should be prefetched. See `Source` type for more. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init(sources: [Source], + options: KingfisherOptionsInfo? = nil, + completionHandler: PrefetcherSourceCompletionHandler? = nil) + { + self.init(sources: sources, options: options) + self.completionSourceHandler = completionHandler + } + + init(sources: [Source], options: KingfisherOptionsInfo?) { + var options = KingfisherParsedOptionsInfo(options) + prefetchSources = sources + pendingSources = ArraySlice(sources) + + // We want all callbacks from our prefetch queue, so we should ignore the callback queue in options. + // Add our own callback dispatch queue to make sure all internal callbacks are + // coming back in our expected queue. + options.callbackQueue = .dispatch(pretchQueue) + optionsInfo = options + + let cache = optionsInfo.targetCache ?? .default + let downloader = optionsInfo.downloader ?? .default + manager = KingfisherManager(downloader: downloader, cache: cache) + } + + /// Starts to download the resources and cache them. This can be useful for background downloading + /// of assets that are required for later use in an app. This code will not try and update any UI + /// with the results of the process. + public func start() { + pretchQueue.async { + guard !self.stopped else { + assertionFailure("You can not restart the same prefetcher. Try to create a new prefetcher.") + self.handleComplete() + return + } + + guard self.maxConcurrentDownloads > 0 else { + assertionFailure("There should be concurrent downloads value should be at least 1.") + self.handleComplete() + return + } + + // Empty case. + guard self.prefetchSources.count > 0 else { + self.handleComplete() + return + } + + let initialConcurrentDownloads = min(self.prefetchSources.count, self.maxConcurrentDownloads) + for _ in 0 ..< initialConcurrentDownloads { + if let resource = self.pendingSources.popFirst() { + self.startPrefetching(resource) + } + } + } + } + + /// Stops current downloading progress, and cancel any future prefetching activity that might be occuring. + public func stop() { + pretchQueue.async { + if self.finished { return } + self.stopped = true + self.tasks.values.forEach { $0.cancel() } + } + } + + private func downloadAndCache(_ source: Source) { + + let downloadTaskCompletionHandler: ((Result) -> Void) = { result in + self.tasks.removeValue(forKey: source.cacheKey) + do { + let _ = try result.get() + self.completedSources.append(source) + } catch { + self.failedSources.append(source) + } + + self.reportProgress() + if self.stopped { + if self.tasks.isEmpty { + self.failedSources.append(contentsOf: self.pendingSources) + self.handleComplete() + } + } else { + self.reportCompletionOrStartNext() + } + } + + var downloadTask: DownloadTask.WrappedTask? + ImagePrefetcher.requestingQueue.sync { + let context = RetrievingContext( + options: optionsInfo, originalSource: source + ) + downloadTask = manager.loadAndCacheImage( + source: source, + context: context, + completionHandler: downloadTaskCompletionHandler) + } + + if let downloadTask = downloadTask { + tasks[source.cacheKey] = downloadTask + } + } + + private func append(cached source: Source) { + skippedSources.append(source) + + reportProgress() + reportCompletionOrStartNext() + } + + private func startPrefetching(_ source: Source) + { + if optionsInfo.forceRefresh { + downloadAndCache(source) + return + } + + let cacheType = manager.cache.imageCachedType( + forKey: source.cacheKey, + processorIdentifier: optionsInfo.processor.identifier) + switch cacheType { + case .memory: + append(cached: source) + case .disk: + if optionsInfo.alsoPrefetchToMemory { + let context = RetrievingContext(options: optionsInfo, originalSource: source) + _ = manager.retrieveImageFromCache( + source: source, + context: context) + { + _ in + self.append(cached: source) + } + } else { + append(cached: source) + } + case .none: + downloadAndCache(source) + } + } + + private func reportProgress() { + + if progressBlock == nil && progressSourceBlock == nil { + return + } + + let skipped = self.skippedSources + let failed = self.failedSources + let completed = self.completedSources + CallbackQueue.mainCurrentOrAsync.execute { + self.progressSourceBlock?(skipped, failed, completed) + self.progressBlock?( + skipped.compactMap { $0.asResource }, + failed.compactMap { $0.asResource }, + completed.compactMap { $0.asResource } + ) + } + } + + private func reportCompletionOrStartNext() { + if let resource = self.pendingSources.popFirst() { + // Loose call stack for huge ammount of sources. + pretchQueue.async { self.startPrefetching(resource) } + } else { + guard allFinished else { return } + self.handleComplete() + } + } + + var allFinished: Bool { + return skippedSources.count + failedSources.count + completedSources.count == prefetchSources.count + } + + private func handleComplete() { + + if completionHandler == nil && completionSourceHandler == nil { + return + } + + // The completion handler should be called on the main thread + CallbackQueue.mainCurrentOrAsync.execute { + self.completionSourceHandler?(self.skippedSources, self.failedSources, self.completedSources) + self.completionHandler?( + self.skippedSources.compactMap { $0.asResource }, + self.failedSources.compactMap { $0.asResource }, + self.completedSources.compactMap { $0.asResource } + ) + self.completionHandler = nil + self.progressBlock = nil + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift new file mode 100644 index 0000000..0d13cbe --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift @@ -0,0 +1,76 @@ +// +// RedirectHandler.swift +// Kingfisher +// +// Created by Roman Maidanovych on 2018/12/10. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents and wraps a method for modifying request during an image download request redirection. +public protocol ImageDownloadRedirectHandler { + + /// The `ImageDownloadRedirectHandler` contained will be used to change the request before redirection. + /// This is the posibility you can modify the image download request during redirection. You can modify the + /// request for some customizing purpose, such as adding auth token to the header, do basic HTTP auth or + /// something like url mapping. + /// + /// Usually, you pass an `ImageDownloadRedirectHandler` as the associated value of + /// `KingfisherOptionsInfoItem.redirectHandler` and use it as the `options` parameter in related methods. + /// + /// If you do nothing with the input `request` and return it as is, a downloading process will redirect with it. + /// + /// - Parameters: + /// - task: The current `SessionDataTask` which triggers this redirect. + /// - response: The response received during redirection. + /// - newRequest: The request for redirection which can be modified. + /// - completionHandler: A closure for being called with modified request. + func handleHTTPRedirection( + for task: SessionDataTask, + response: HTTPURLResponse, + newRequest: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) +} + +/// A wrapper for creating an `ImageDownloadRedirectHandler` easier. +/// This type conforms to `ImageDownloadRedirectHandler` and wraps a redirect request modify block. +public struct AnyRedirectHandler: ImageDownloadRedirectHandler { + + let block: (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void + + public func handleHTTPRedirection( + for task: SessionDataTask, + response: HTTPURLResponse, + newRequest: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) + { + block(task, response, newRequest, completionHandler) + } + + /// Creates a value of `ImageDownloadRedirectHandler` which runs `modify` block. + /// + /// - Parameter modify: The request modifying block runs when a request modifying task comes. + /// + public init(handle: @escaping (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void) { + block = handle + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/RequestModifier.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/RequestModifier.swift new file mode 100644 index 0000000..a1d972d --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/RequestModifier.swift @@ -0,0 +1,108 @@ +// +// RequestModifier.swift +// Kingfisher +// +// Created by Wei Wang on 2016/09/05. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents and wraps a method for modifying request before an image download request starts in an asynchronous way. +public protocol AsyncImageDownloadRequestModifier { + + /// This method will be called just before the `request` being sent. + /// This is the last chance you can modify the image download request. You can modify the request for some + /// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping. + /// When you have done with the modification, call the `reportModified` block with the modified request and the data + /// download will happen with this request. + /// + /// Usually, you pass an `AsyncImageDownloadRequestModifier` as the associated value of + /// `KingfisherOptionsInfoItem.requestModifier` and use it as the `options` parameter in related methods. + /// + /// If you do nothing with the input `request` and return it as is, a downloading process will start with it. + /// + /// - Parameters: + /// - request: The input request contains necessary information like `url`. This request is generated + /// according to your resource url as a GET request. + /// - reportModified: The callback block you need to call after the asynchronous modifying done. + /// + func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void) + + /// A block will be called when the download task started. + /// + /// If an `AsyncImageDownloadRequestModifier` and the asynchronous modification happens before the download, the + /// related download method will not return a valid `DownloadTask` value. Instead, you can get one from this method. + var onDownloadTaskStarted: ((DownloadTask?) -> Void)? { get } +} + +/// Represents and wraps a method for modifying request before an image download request starts. +public protocol ImageDownloadRequestModifier: AsyncImageDownloadRequestModifier { + + /// This method will be called just before the `request` being sent. + /// This is the last chance you can modify the image download request. You can modify the request for some + /// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping. + /// + /// Usually, you pass an `ImageDownloadRequestModifier` as the associated value of + /// `KingfisherOptionsInfoItem.requestModifier` and use it as the `options` parameter in related methods. + /// + /// If you do nothing with the input `request` and return it as is, a downloading process will start with it. + /// + /// - Parameter request: The input request contains necessary information like `url`. This request is generated + /// according to your resource url as a GET request. + /// - Returns: A modified version of request, which you wish to use for downloading an image. If `nil` returned, + /// a `KingfisherError.requestError` with `.emptyRequest` as its reason will occur. + /// + func modified(for request: URLRequest) -> URLRequest? +} + +extension ImageDownloadRequestModifier { + public func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void) { + let request = modified(for: request) + reportModified(request) + } + + /// This is `nil` for a sync `ImageDownloadRequestModifier` by default. You can get the `DownloadTask` from the + /// return value of downloader method. + public var onDownloadTaskStarted: ((DownloadTask?) -> Void)? { return nil } +} + +/// A wrapper for creating an `ImageDownloadRequestModifier` easier. +/// This type conforms to `ImageDownloadRequestModifier` and wraps an image modify block. +public struct AnyModifier: ImageDownloadRequestModifier { + + let block: (URLRequest) -> URLRequest? + + /// For `ImageDownloadRequestModifier` conformation. + public func modified(for request: URLRequest) -> URLRequest? { + return block(request) + } + + /// Creates a value of `ImageDownloadRequestModifier` which runs `modify` block. + /// + /// - Parameter modify: The request modifying block runs when a request modifying task comes. + /// The return `URLRequest?` value of this block will be used as the image download request. + /// If `nil` returned, a `KingfisherError.requestError` with `.emptyRequest` as its + /// reason will occur. + public init(modify: @escaping (URLRequest) -> URLRequest?) { + block = modify + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift new file mode 100644 index 0000000..1ab5a2e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift @@ -0,0 +1,153 @@ +// +// RetryStrategy.swift +// Kingfisher +// +// Created by onevcat on 2020/05/04. +// +// Copyright (c) 2020 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a retry context which could be used to determine the current retry status. +public class RetryContext { + + /// The source from which the target image should be retrieved. + public let source: Source + + /// The last error which caused current retry behavior. + public let error: KingfisherError + + /// The retried count before current retry happens. This value is `0` if the current retry is for the first time. + public var retriedCount: Int + + /// A user set value for passing any other information during the retry. If you choose to use `RetryDecision.retry` + /// as the retry decision for `RetryStrategy.retry(context:retryHandler:)`, the associated value of + /// `RetryDecision.retry` will be delivered to you in the next retry. + public internal(set) var userInfo: Any? = nil + + init(source: Source, error: KingfisherError) { + self.source = source + self.error = error + self.retriedCount = 0 + } + + @discardableResult + func increaseRetryCount() -> RetryContext { + retriedCount += 1 + return self + } +} + +/// Represents decision of behavior on the current retry. +public enum RetryDecision { + /// A retry should happen. The associated `userInfo` will be pass to the next retry in the `RetryContext` parameter. + case retry(userInfo: Any?) + /// There should be no more retry attempt. The image retrieving process will fail with an error. + case stop +} + +/// Defines a retry strategy can be applied to a `.retryStrategy` option. +public protocol RetryStrategy { + + /// Kingfisher calls this method if an error happens during the image retrieving process from a `KingfisherManager`. + /// You implement this method to provide necessary logic based on the `context` parameter. Then you need to call + /// `retryHandler` to pass the retry decision back to Kingfisher. + /// + /// - Parameters: + /// - context: The retry context containing information of current retry attempt. + /// - retryHandler: A block you need to call with a decision of whether the retry should happen or not. + func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void) +} + +/// A retry strategy that guides Kingfisher to retry when a `.responseError` happens, with a specified max retry count +/// and a certain interval mechanism. +public struct DelayRetryStrategy: RetryStrategy { + + /// Represents the interval mechanism which used in a `DelayRetryStrategy`. + public enum Interval { + /// The next retry attempt should happen in fixed seconds. For example, if the associated value is 3, the + /// attempts happens after 3 seconds after the previous decision is made. + case seconds(TimeInterval) + /// The next retry attempt should happen in an accumulated duration. For example, if the associated value is 3, + /// the attempts happens with interval of 3, 6, 9, 12, ... seconds. + case accumulated(TimeInterval) + /// Uses a block to determine the next interval. The current retry count is given as a parameter. + case custom(block: (_ retriedCount: Int) -> TimeInterval) + + func timeInterval(for retriedCount: Int) -> TimeInterval { + let retryAfter: TimeInterval + switch self { + case .seconds(let interval): + retryAfter = interval + case .accumulated(let interval): + retryAfter = Double(retriedCount + 1) * interval + case .custom(let block): + retryAfter = block(retriedCount) + } + return retryAfter + } + } + + /// The max retry count defined for the retry strategy + public let maxRetryCount: Int + + /// The retry interval mechanism defined for the retry strategy. + public let retryInterval: Interval + + /// Creates a delay retry strategy. + /// - Parameters: + /// - maxRetryCount: The max retry count. + /// - retryInterval: The retry interval mechanism. By default, `.seconds(3)` is used to provide a constant retry + /// interval. + public init(maxRetryCount: Int, retryInterval: Interval = .seconds(3)) { + self.maxRetryCount = maxRetryCount + self.retryInterval = retryInterval + } + + public func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void) { + // Retry count exceeded. + guard context.retriedCount < maxRetryCount else { + retryHandler(.stop) + return + } + + // User cancel the task. No retry. + guard !context.error.isTaskCancelled else { + retryHandler(.stop) + return + } + + // Only retry for a response error. + guard case KingfisherError.responseError = context.error else { + retryHandler(.stop) + return + } + + let interval = retryInterval.timeInterval(for: context.retriedCount) + if interval == 0 { + retryHandler(.retry(userInfo: nil)) + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + interval) { + retryHandler(.retry(userInfo: nil)) + } + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift new file mode 100644 index 0000000..8932bd5 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift @@ -0,0 +1,127 @@ +// +// SessionDataTask.swift +// Kingfisher +// +// Created by Wei Wang on 2018/11/1. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a session data task in `ImageDownloader`. It consists of an underlying `URLSessionDataTask` and +/// an array of `TaskCallback`. Multiple `TaskCallback`s could be added for a single downloading data task. +public class SessionDataTask { + + /// Represents the type of token which used for cancelling a task. + public typealias CancelToken = Int + + struct TaskCallback { + let onCompleted: Delegate, Void>? + let options: KingfisherParsedOptionsInfo + } + + /// Downloaded raw data of current task. + public private(set) var mutableData: Data + + // This is a copy of `task.originalRequest?.url`. It is for getting a race-safe behavior for a pitfall on iOS 13. + // Ref: https://github.com/onevcat/Kingfisher/issues/1511 + public let originalURL: URL? + + /// The underlying download task. It is only for debugging purpose when you encountered an error. You should not + /// modify the content of this task or start it yourself. + public let task: URLSessionDataTask + private var callbacksStore = [CancelToken: TaskCallback]() + + var callbacks: [SessionDataTask.TaskCallback] { + lock.lock() + defer { lock.unlock() } + return Array(callbacksStore.values) + } + + private var currentToken = 0 + private let lock = NSLock() + + let onTaskDone = Delegate<(Result<(Data, URLResponse?), KingfisherError>, [TaskCallback]), Void>() + let onCallbackCancelled = Delegate<(CancelToken, TaskCallback), Void>() + + var started = false + var containsCallbacks: Bool { + // We should be able to use `task.state != .running` to check it. + // However, in some rare cases, cancelling the task does not change + // task state to `.cancelling` immediately, but still in `.running`. + // So we need to check callbacks count to for sure that it is safe to remove the + // task in delegate. + return !callbacks.isEmpty + } + + init(task: URLSessionDataTask) { + self.task = task + self.originalURL = task.originalRequest?.url + mutableData = Data() + } + + func addCallback(_ callback: TaskCallback) -> CancelToken { + lock.lock() + defer { lock.unlock() } + callbacksStore[currentToken] = callback + defer { currentToken += 1 } + return currentToken + } + + func removeCallback(_ token: CancelToken) -> TaskCallback? { + lock.lock() + defer { lock.unlock() } + if let callback = callbacksStore[token] { + callbacksStore[token] = nil + return callback + } + return nil + } + + func removeAllCallbacks() -> Void { + lock.lock() + defer { lock.unlock() } + callbacksStore.removeAll() + } + + func resume() { + guard !started else { return } + started = true + task.resume() + } + + func cancel(token: CancelToken) { + guard let callback = removeCallback(token) else { + return + } + onCallbackCancelled.call((token, callback)) + } + + func forceCancel() { + for token in callbacksStore.keys { + cancel(token: token) + } + } + + func didReceiveData(_ data: Data) { + mutableData.append(data) + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift new file mode 100644 index 0000000..bebe680 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift @@ -0,0 +1,263 @@ +// +// SessionDelegate.swift +// Kingfisher +// +// Created by Wei Wang on 2018/11/1. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +// Represents the delegate object of downloader session. It also behave like a task manager for downloading. +@objc(KFSessionDelegate) // Fix for ObjC header name conflicting. https://github.com/onevcat/Kingfisher/issues/1530 +open class SessionDelegate: NSObject { + + typealias SessionChallengeFunc = ( + URLSession, + URLAuthenticationChallenge, + (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) + + typealias SessionTaskChallengeFunc = ( + URLSession, + URLSessionTask, + URLAuthenticationChallenge, + (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) + + private var tasks: [URL: SessionDataTask] = [:] + private let lock = NSLock() + + let onValidStatusCode = Delegate() + let onDownloadingFinished = Delegate<(URL, Result), Void>() + let onDidDownloadData = Delegate() + + let onReceiveSessionChallenge = Delegate() + let onReceiveSessionTaskChallenge = Delegate() + + func add( + _ dataTask: URLSessionDataTask, + url: URL, + callback: SessionDataTask.TaskCallback) -> DownloadTask + { + lock.lock() + defer { lock.unlock() } + + // Create a new task if necessary. + let task = SessionDataTask(task: dataTask) + task.onCallbackCancelled.delegate(on: self) { [weak task] (self, value) in + guard let task = task else { return } + + let (token, callback) = value + + let error = KingfisherError.requestError(reason: .taskCancelled(task: task, token: token)) + task.onTaskDone.call((.failure(error), [callback])) + // No other callbacks waiting, we can clear the task now. + if !task.containsCallbacks { + let dataTask = task.task + + self.cancelTask(dataTask) + self.remove(task) + } + } + let token = task.addCallback(callback) + tasks[url] = task + return DownloadTask(sessionTask: task, cancelToken: token) + } + + private func cancelTask(_ dataTask: URLSessionDataTask) { + lock.lock() + defer { lock.unlock() } + dataTask.cancel() + } + + func append( + _ task: SessionDataTask, + url: URL, + callback: SessionDataTask.TaskCallback) -> DownloadTask + { + let token = task.addCallback(callback) + return DownloadTask(sessionTask: task, cancelToken: token) + } + + private func remove(_ task: SessionDataTask) { + lock.lock() + defer { lock.unlock() } + + guard let url = task.originalURL else { + return + } + task.removeAllCallbacks() + tasks[url] = nil + } + + private func task(for task: URLSessionTask) -> SessionDataTask? { + lock.lock() + defer { lock.unlock() } + + guard let url = task.originalRequest?.url else { + return nil + } + guard let sessionTask = tasks[url] else { + return nil + } + guard sessionTask.task.taskIdentifier == task.taskIdentifier else { + return nil + } + return sessionTask + } + + func task(for url: URL) -> SessionDataTask? { + lock.lock() + defer { lock.unlock() } + return tasks[url] + } + + func cancelAll() { + lock.lock() + let taskValues = tasks.values + lock.unlock() + for task in taskValues { + task.forceCancel() + } + } + + func cancel(url: URL) { + lock.lock() + let task = tasks[url] + lock.unlock() + task?.forceCancel() + } +} + +extension SessionDelegate: URLSessionDataDelegate { + + open func urlSession( + _ session: URLSession, + dataTask: URLSessionDataTask, + didReceive response: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) + { + guard let httpResponse = response as? HTTPURLResponse else { + let error = KingfisherError.responseError(reason: .invalidURLResponse(response: response)) + onCompleted(task: dataTask, result: .failure(error)) + completionHandler(.cancel) + return + } + + let httpStatusCode = httpResponse.statusCode + guard onValidStatusCode.call(httpStatusCode) == true else { + let error = KingfisherError.responseError(reason: .invalidHTTPStatusCode(response: httpResponse)) + onCompleted(task: dataTask, result: .failure(error)) + completionHandler(.cancel) + return + } + completionHandler(.allow) + } + + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + guard let task = self.task(for: dataTask) else { + return + } + + task.didReceiveData(data) + + task.callbacks.forEach { callback in + callback.options.onDataReceived?.forEach { sideEffect in + sideEffect.onDataReceived(session, task: task, data: data) + } + } + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + guard let sessionTask = self.task(for: task) else { return } + + if let url = sessionTask.originalURL { + let result: Result + if let error = error { + result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error))) + } else if let response = task.response { + result = .success(response) + } else { + result = .failure(KingfisherError.responseError(reason: .noURLResponse(task: sessionTask))) + } + onDownloadingFinished.call((url, result)) + } + + let result: Result<(Data, URLResponse?), KingfisherError> + if let error = error { + result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error))) + } else { + if let data = onDidDownloadData.call(sessionTask) { + result = .success((data, task.response)) + } else { + result = .failure(KingfisherError.responseError(reason: .dataModifyingFailed(task: sessionTask))) + } + } + onCompleted(task: task, result: result) + } + + open func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + onReceiveSessionChallenge.call((session, challenge, completionHandler)) + } + + open func urlSession( + _ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + onReceiveSessionTaskChallenge.call((session, task, challenge, completionHandler)) + } + + open func urlSession( + _ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) + { + guard let sessionDataTask = self.task(for: task), + let redirectHandler = Array(sessionDataTask.callbacks).last?.options.redirectHandler else + { + completionHandler(request) + return + } + + redirectHandler.handleHTTPRedirection( + for: sessionDataTask, + response: response, + newRequest: request, + completionHandler: completionHandler) + } + + private func onCompleted(task: URLSessionTask, result: Result<(Data, URLResponse?), KingfisherError>) { + guard let sessionTask = self.task(for: task) else { + return + } + sessionTask.onTaskDone.call((result, sessionTask.callbacks)) + remove(sessionTask) + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift new file mode 100644 index 0000000..b0eec2d --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift @@ -0,0 +1,130 @@ +// +// ImageBinder.swift +// Kingfisher +// +// Created by onevcat on 2019/06/27. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage { + + /// Represents a binder for `KFImage`. It takes responsibility as an `ObjectBinding` and performs + /// image downloading and progress reporting based on `KingfisherManager`. + class ImageBinder: ObservableObject { + + init() {} + + var downloadTask: DownloadTask? + private var loading = false + + var loadingOrSucceeded: Bool { + return loading || loadedImage != nil + } + + // Do not use @Published due to https://github.com/onevcat/Kingfisher/issues/1717. Revert to @Published once + // we can drop iOS 12. + var loaded = false { willSet { objectWillChange.send() } } + var loadedImage: KFCrossPlatformImage? = nil { willSet { objectWillChange.send() } } + var progress: Progress = .init() { willSet { objectWillChange.send() } } + + func start(context: Context) { + guard let source = context.source else { + CallbackQueue.mainCurrentOrAsync.execute { + context.onFailureDelegate.call(KingfisherError.imageSettingError(reason: .emptySource)) + if let image = context.options.onFailureImage { + self.loadedImage = image + } + self.loading = false + self.loaded = true + } + return + } + + loading = true + + progress = .init() + downloadTask = KingfisherManager.shared + .retrieveImage( + with: source, + options: context.options, + progressBlock: { size, total in + self.updateProgress(downloaded: size, total: total) + context.onProgressDelegate.call((size, total)) + }, + completionHandler: { [weak self] result in + + guard let self = self else { return } + + CallbackQueue.mainCurrentOrAsync.execute { + self.downloadTask = nil + self.loading = false + } + + switch result { + case .success(let value): + CallbackQueue.mainCurrentOrAsync.execute { + if let fadeDuration = context.fadeTransitionDuration(cacheType: value.cacheType) { + let animation = Animation.linear(duration: fadeDuration) + withAnimation(animation) { self.loaded = true } + } else { + self.loaded = true + } + self.loadedImage = value.image + } + + CallbackQueue.mainAsync.execute { + context.onSuccessDelegate.call(value) + } + case .failure(let error): + CallbackQueue.mainCurrentOrAsync.execute { + if let image = context.options.onFailureImage { + self.loadedImage = image + } + self.loaded = true + } + + CallbackQueue.mainAsync.execute { + context.onFailureDelegate.call(error) + } + } + }) + } + + private func updateProgress(downloaded: Int64, total: Int64) { + progress.totalUnitCount = total + progress.completedUnitCount = downloaded + objectWillChange.send() + } + + /// Cancels the download task if it is in progress. + func cancel() { + downloadTask?.cancel() + downloadTask = nil + loading = false + } + } +} +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift new file mode 100644 index 0000000..7275f0e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift @@ -0,0 +1,99 @@ +// +// ImageContext.swift +// Kingfisher +// +// Created by onevcat on 2021/05/08. +// +// Copyright (c) 2021 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage { + public class Context { + let source: Source? + var options = KingfisherParsedOptionsInfo( + KingfisherManager.shared.defaultOptions + [.loadDiskFileSynchronously] + ) + + var configurations: [(HoldingView) -> HoldingView] = [] + var renderConfigurations: [(HoldingView.RenderingView) -> Void] = [] + + var cancelOnDisappear: Bool = false + var placeholder: ((Progress) -> AnyView)? = nil + + let onFailureDelegate = Delegate() + let onSuccessDelegate = Delegate() + let onProgressDelegate = Delegate<(Int64, Int64), Void>() + + init(source: Source?) { + self.source = source + } + + func shouldApplyFade(cacheType: CacheType) -> Bool { + options.forceTransition || cacheType == .none + } + + func fadeTransitionDuration(cacheType: CacheType) -> TimeInterval? { + shouldApplyFade(cacheType: cacheType) + ? options.transition.fadeDuration + : nil + } + } +} + +extension ImageTransition { + // Only for fade effect in SwiftUI. + fileprivate var fadeDuration: TimeInterval? { + switch self { + case .fade(let duration): + return duration + default: + return nil + } + } +} + + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage.Context: Hashable { + public static func == (lhs: KFImage.Context, rhs: KFImage.Context) -> Bool { + lhs.source == rhs.source && + lhs.options.processor.identifier == rhs.options.processor.identifier + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(source) + hasher.combine(options.processor.identifier) + } +} + +#if canImport(UIKit) && !os(watchOS) +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFAnimatedImage { + public typealias Context = KFImage.Context + typealias ImageBinder = KFImage.ImageBinder +} +#endif + +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift new file mode 100644 index 0000000..ad25eb2 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift @@ -0,0 +1,96 @@ +// +// KFAnimatedImage.swift +// Kingfisher +// +// Created by wangxingbin on 2021/4/29. +// +// Copyright (c) 2021 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(SwiftUI) && canImport(Combine) && canImport(UIKit) && !os(watchOS) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public struct KFAnimatedImage: KFImageProtocol { + public typealias HoldingView = KFAnimatedImageViewRepresenter + public var context: Context + public init(context: KFImage.Context) { + self.context = context + } + + /// Configures current rendering view with a `block`. This block will be applied when the under-hood + /// `AnimatedImageView` is created in `UIViewRepresentable.makeUIView(context:)` + /// + /// - Parameter block: The block applies to the animated image view. + /// - Returns: A `KFAnimatedImage` view that being configured by the `block`. + public func configure(_ block: @escaping (HoldingView.RenderingView) -> Void) -> Self { + context.renderConfigurations.append(block) + return self + } +} + +/// A wrapped `UIViewRepresentable` of `AnimatedImageView` +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public struct KFAnimatedImageViewRepresenter: UIViewRepresentable, KFImageHoldingView { + public typealias RenderingView = AnimatedImageView + public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> KFAnimatedImageViewRepresenter { + KFAnimatedImageViewRepresenter(image: image, context: context) + } + + var image: KFCrossPlatformImage? + let context: KFImage.Context + + public func makeUIView(context: Context) -> AnimatedImageView { + let view = AnimatedImageView() + + self.context.renderConfigurations.forEach { $0(view) } + + view.image = image + + // Allow SwiftUI scale (fit/fill) working fine. + view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + view.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + return view + } + + public func updateUIView(_ uiView: AnimatedImageView, context: Context) { + uiView.image = image + } +} + +#if DEBUG +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +struct KFAnimatedImage_Previews: PreviewProvider { + static var previews: some View { + Group { + KFAnimatedImage(source: .network(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF/1.gif")!)) + .onSuccess { r in + print(r) + } + .placeholder { + ProgressView() + } + .padding() + } + } +} +#endif +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift new file mode 100644 index 0000000..67f1c19 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift @@ -0,0 +1,106 @@ +// +// KFImage.swift +// Kingfisher +// +// Created by onevcat on 2019/06/26. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public struct KFImage: KFImageProtocol { + public var context: Context + public init(context: Context) { + self.context = context + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension Image: KFImageHoldingView { + public typealias RenderingView = Image + public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> Image { + Image(crossPlatformImage: image) + } +} + +// MARK: - Image compatibility. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage { + + public func resizable( + capInsets: EdgeInsets = EdgeInsets(), + resizingMode: Image.ResizingMode = .stretch) -> KFImage + { + configure { $0.resizable(capInsets: capInsets, resizingMode: resizingMode) } + } + + public func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> KFImage { + configure { $0.renderingMode(renderingMode) } + } + + public func interpolation(_ interpolation: Image.Interpolation) -> KFImage { + configure { $0.interpolation(interpolation) } + } + + public func antialiased(_ isAntialiased: Bool) -> KFImage { + configure { $0.antialiased(isAntialiased) } + } + + /// Starts the loading process of `self` immediately. + /// + /// By default, a `KFImage` will not load its source until the `onAppear` is called. This is a lazily loading + /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a + /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once + /// could help avoiding the flickering, with some performance trade-off. + /// + /// - Deprecated: This is not necessary anymore since `@StateObject` is used for holding the image data. + /// It does nothing now and please just remove it. + /// + /// - Returns: The `Self` value with changes applied. + @available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.") + public func loadImmediately(_ start: Bool = true) -> KFImage { + return self + } +} + +#if DEBUG +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +struct KFImage_Previews: PreviewProvider { + static var previews: some View { + Group { + KFImage.url(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png")!) + .onSuccess { r in + print(r) + } + .placeholder { p in + ProgressView(p) + } + .resizable() + .aspectRatio(contentMode: .fit) + .padding() + } + } +} +#endif +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift new file mode 100644 index 0000000..e553b2a --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift @@ -0,0 +1,138 @@ +// +// KFImageOptions.swift +// Kingfisher +// +// Created by onevcat on 2020/12/20. +// +// Copyright (c) 2020 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +// MARK: - KFImage creating. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + + /// Creates a `KFImage` for a given `Source`. + /// - Parameters: + /// - source: The `Source` object defines data information from network or a data provider. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func source( + _ source: Source? + ) -> Self + { + Self.init(source: source) + } + + /// Creates a `KFImage` for a given `Resource`. + /// - Parameters: + /// - source: The `Resource` object defines data information like key or URL. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func resource( + _ resource: Resource? + ) -> Self + { + source(resource?.convertToSource()) + } + + /// Creates a `KFImage` for a given `URL`. + /// - Parameters: + /// - url: The URL where the image should be downloaded. + /// - cacheKey: The key used to store the downloaded image in cache. + /// If `nil`, the `absoluteString` of `url` is used as the cache key. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func url( + _ url: URL?, cacheKey: String? = nil + ) -> Self + { + source(url?.convertToSource(overrideCacheKey: cacheKey)) + } + + /// Creates a `KFImage` for a given `ImageDataProvider`. + /// - Parameters: + /// - provider: The `ImageDataProvider` object contains information about the data. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func dataProvider( + _ provider: ImageDataProvider? + ) -> Self + { + source(provider?.convertToSource()) + } + + /// Creates a builder for some given raw data and a cache key. + /// - Parameters: + /// - data: The data object from which the image should be created. + /// - cacheKey: The key used to store the downloaded image in cache. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func data( + _ data: Data?, cacheKey: String + ) -> Self + { + if let data = data { + return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey)) + } else { + return dataProvider(nil) + } + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + /// Sets a placeholder `View` which shows when loading the image, with a progress parameter as input. + /// - Parameter content: A view that describes the placeholder. + /// - Returns: A `KFImage` view that contains `content` as its placeholder. + public func placeholder(@ViewBuilder _ content: @escaping (Progress) -> P) -> Self { + context.placeholder = { progress in + return AnyView(content(progress)) + } + return self + } + + /// Sets a placeholder `View` which shows when loading the image. + /// - Parameter content: A view that describes the placeholder. + /// - Returns: A `KFImage` view that contains `content` as its placeholder. + public func placeholder(@ViewBuilder _ content: @escaping () -> P) -> Self { + placeholder { _ in content() } + } + + /// Sets cancelling the download task bound to `self` when the view disappearing. + /// - Parameter flag: Whether cancel the task or not. + /// - Returns: A `KFImage` view that cancels downloading task when disappears. + public func cancelOnDisappear(_ flag: Bool) -> Self { + context.cancelOnDisappear = flag + return self + } + + /// Sets a fade transition for the image task. + /// - Parameter duration: The duration of the fade transition. + /// - Returns: A `KFImage` with changes applied. + /// + /// Kingfisher will use the fade transition to animate the image in if it is downloaded from web. + /// The transition will not happen when the + /// image is retrieved from either memory or disk cache by default. If you need to do the transition even when + /// the image being retrieved from cache, also call `forceRefresh()` on the returned `KFImage`. + public func fade(duration: TimeInterval) -> Self { + context.options.transition = .fade(duration) + return self + } +} +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift new file mode 100644 index 0000000..4eca452 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift @@ -0,0 +1,93 @@ +// +// KFImageProtocol.swift +// Kingfisher +// +// Created by onevcat on 2021/05/08. +// +// Copyright (c) 2021 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public protocol KFImageProtocol: View, KFOptionSetter { + associatedtype HoldingView: KFImageHoldingView + var context: KFImage.Context { get set } + init(context: KFImage.Context) +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + public var body: some View { + ZStack { + KFImageRenderer( + context: context + ).id(context) + } + } + + /// Creates a Kingfisher compatible image view to load image from the given `Source`. + /// - Parameters: + /// - source: The image `Source` defining where to load the target image. + public init(source: Source?) { + let context = KFImage.Context(source: source) + self.init(context: context) + } + + /// Creates a Kingfisher compatible image view to load image from the given `URL`. + /// - Parameters: + /// - source: The image `Source` defining where to load the target image. + public init(_ url: URL?) { + self.init(source: url?.convertToSource()) + } + + /// Configures current image with a `block`. This block will be lazily applied when creating the final `Image`. + /// - Parameter block: The block applies to loaded image. + /// - Returns: A `KFImage` view that configures internal `Image` with `block`. + public func configure(_ block: @escaping (HoldingView) -> HoldingView) -> Self { + context.configurations.append(block) + return self + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public protocol KFImageHoldingView: View { + associatedtype RenderingView + static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> Self +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + public var options: KingfisherParsedOptionsInfo { + get { context.options } + nonmutating set { context.options = newValue } + } + + public var onFailureDelegate: Delegate { context.onFailureDelegate } + public var onSuccessDelegate: Delegate { context.onSuccessDelegate } + public var onProgressDelegate: Delegate<(Int64, Int64), Void> { context.onProgressDelegate } + + public var delegateObserver: AnyObject { context } +} + + +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift new file mode 100644 index 0000000..90e6846 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift @@ -0,0 +1,105 @@ +// +// KFImageRenderer.swift +// Kingfisher +// +// Created by onevcat on 2021/05/08. +// +// Copyright (c) 2021 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +/// A Kingfisher compatible SwiftUI `View` to load an image from a `Source`. +/// Declaring a `KFImage` in a `View`'s body to trigger loading from the given `Source`. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +struct KFImageRenderer : View where HoldingView: KFImageHoldingView { + + @StateObject var binder: KFImage.ImageBinder = .init() + let context: KFImage.Context + + var body: some View { + ZStack { + context.configurations + .reduce(HoldingView.created(from: binder.loadedImage, context: context)) { + current, config in config(current) + } + .opacity(binder.loaded ? 1.0 : 0.0) + if binder.loadedImage == nil { + Group { + if let placeholder = context.placeholder, let view = placeholder(binder.progress) { + view + } else { + Color.clear + } + } + .onAppear { [weak binder = self.binder] in + guard let binder = binder else { + return + } + if !binder.loadingOrSucceeded { + binder.start(context: context) + } + } + .onDisappear { [weak binder = self.binder] in + guard let binder = binder else { + return + } + if context.cancelOnDisappear { + binder.cancel() + } + } + } + } + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension Image { + // Creates an Image with either UIImage or NSImage. + init(crossPlatformImage: KFCrossPlatformImage?) { + #if canImport(UIKit) + self.init(uiImage: crossPlatformImage ?? KFCrossPlatformImage()) + #elseif canImport(AppKit) + self.init(nsImage: crossPlatformImage ?? KFCrossPlatformImage()) + #endif + } +} + +#if canImport(UIKit) +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension UIImage.Orientation { + func toSwiftUI() -> Image.Orientation { + switch self { + case .down: return .down + case .up: return .up + case .left: return .left + case .right: return .right + case .upMirrored: return .upMirrored + case .downMirrored: return .downMirrored + case .leftMirrored: return .leftMirrored + case .rightMirrored: return .rightMirrored + @unknown default: return .up + } + } +} +#endif +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Box.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Box.swift new file mode 100644 index 0000000..0303a6e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Box.swift @@ -0,0 +1,34 @@ +// +// Box.swift +// Kingfisher +// +// Created by Wei Wang on 2018/3/17. +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +class Box { + var value: T + + init(_ value: T) { + self.value = value + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift new file mode 100644 index 0000000..822af28 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift @@ -0,0 +1,83 @@ +// +// CallbackQueue.swift +// Kingfisher +// +// Created by onevcat on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +public typealias ExecutionQueue = CallbackQueue + +/// Represents callback queue behaviors when an calling of closure be dispatched. +/// +/// - asyncMain: Dispatch the calling to `DispatchQueue.main` with an `async` behavior. +/// - currentMainOrAsync: Dispatch the calling to `DispatchQueue.main` with an `async` behavior if current queue is not +/// `.main`. Otherwise, call the closure immediately in current main queue. +/// - untouch: Do not change the calling queue for closure. +/// - dispatch: Dispatches to a specified `DispatchQueue`. +public enum CallbackQueue { + /// Dispatch the calling to `DispatchQueue.main` with an `async` behavior. + case mainAsync + /// Dispatch the calling to `DispatchQueue.main` with an `async` behavior if current queue is not + /// `.main`. Otherwise, call the closure immediately in current main queue. + case mainCurrentOrAsync + /// Do not change the calling queue for closure. + case untouch + /// Dispatches to a specified `DispatchQueue`. + case dispatch(DispatchQueue) + + public func execute(_ block: @escaping () -> Void) { + switch self { + case .mainAsync: + DispatchQueue.main.async { block() } + case .mainCurrentOrAsync: + DispatchQueue.main.safeAsync { block() } + case .untouch: + block() + case .dispatch(let queue): + queue.async { block() } + } + } + + var queue: DispatchQueue { + switch self { + case .mainAsync: return .main + case .mainCurrentOrAsync: return .main + case .untouch: return OperationQueue.current?.underlyingQueue ?? .main + case .dispatch(let queue): return queue + } + } +} + +extension DispatchQueue { + // This method will dispatch the `block` to self. + // If `self` is the main queue, and current thread is main thread, the block + // will be invoked immediately instead of being dispatched. + func safeAsync(_ block: @escaping () -> Void) { + if self === DispatchQueue.main && Thread.isMainThread { + block() + } else { + async { block() } + } + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Delegate.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Delegate.swift new file mode 100644 index 0000000..9caa1a6 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Delegate.swift @@ -0,0 +1,132 @@ +// +// Delegate.swift +// Kingfisher +// +// Created by onevcat on 2018/10/10. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +/// A class that keeps a weakly reference for `self` when implementing `onXXX` behaviors. +/// Instead of remembering to keep `self` as weak in a stored closure: +/// +/// ```swift +/// // MyClass.swift +/// var onDone: (() -> Void)? +/// func done() { +/// onDone?() +/// } +/// +/// // ViewController.swift +/// var obj: MyClass? +/// +/// func doSomething() { +/// obj = MyClass() +/// obj!.onDone = { [weak self] in +/// self?.reportDone() +/// } +/// } +/// ``` +/// +/// You can create a `Delegate` and observe on `self`. Now, there is no retain cycle inside: +/// +/// ```swift +/// // MyClass.swift +/// let onDone = Delegate<(), Void>() +/// func done() { +/// onDone.call() +/// } +/// +/// // ViewController.swift +/// var obj: MyClass? +/// +/// func doSomething() { +/// obj = MyClass() +/// obj!.onDone.delegate(on: self) { (self, _) +/// // `self` here is shadowed and does not keep a strong ref. +/// // So you can release both `MyClass` instance and `ViewController` instance. +/// self.reportDone() +/// } +/// } +/// ``` +/// +public class Delegate { + public init() {} + + private var block: ((Input) -> Output?)? + public func delegate(on target: T, block: ((T, Input) -> Output)?) { + self.block = { [weak target] input in + guard let target = target else { return nil } + return block?(target, input) + } + } + + public func call(_ input: Input) -> Output? { + return block?(input) + } + + public func callAsFunction(_ input: Input) -> Output? { + return call(input) + } +} + +extension Delegate where Input == Void { + public func call() -> Output? { + return call(()) + } + + public func callAsFunction() -> Output? { + return call() + } +} + +extension Delegate where Input == Void, Output: OptionalProtocol { + public func call() -> Output { + return call(()) + } + + public func callAsFunction() -> Output { + return call() + } +} + +extension Delegate where Output: OptionalProtocol { + public func call(_ input: Input) -> Output { + if let result = block?(input) { + return result + } else { + return Output._createNil + } + } + + public func callAsFunction(_ input: Input) -> Output { + return call(input) + } +} + +public protocol OptionalProtocol { + static var _createNil: Self { get } +} +extension Optional : OptionalProtocol { + public static var _createNil: Optional { + return nil + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift new file mode 100644 index 0000000..f1e1e8b --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift @@ -0,0 +1,125 @@ +// +// ExtensionHelpers.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +extension CGFloat { + var isEven: Bool { + return truncatingRemainder(dividingBy: 2.0) == 0 + } +} + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +extension NSBezierPath { + convenience init(roundedRect rect: NSRect, topLeftRadius: CGFloat, topRightRadius: CGFloat, + bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat) + { + self.init() + + let maxCorner = min(rect.width, rect.height) / 2 + + let radiusTopLeft = min(maxCorner, max(0, topLeftRadius)) + let radiusTopRight = min(maxCorner, max(0, topRightRadius)) + let radiusBottomLeft = min(maxCorner, max(0, bottomLeftRadius)) + let radiusBottomRight = min(maxCorner, max(0, bottomRightRadius)) + + guard !rect.isEmpty else { + return + } + + let topLeft = NSPoint(x: rect.minX, y: rect.maxY) + let topRight = NSPoint(x: rect.maxX, y: rect.maxY) + let bottomRight = NSPoint(x: rect.maxX, y: rect.minY) + + move(to: NSPoint(x: rect.midX, y: rect.maxY)) + appendArc(from: topLeft, to: rect.origin, radius: radiusTopLeft) + appendArc(from: rect.origin, to: bottomRight, radius: radiusBottomLeft) + appendArc(from: bottomRight, to: topRight, radius: radiusBottomRight) + appendArc(from: topRight, to: topLeft, radius: radiusTopRight) + close() + } + + convenience init(roundedRect rect: NSRect, byRoundingCorners corners: RectCorner, radius: CGFloat) { + let radiusTopLeft = corners.contains(.topLeft) ? radius : 0 + let radiusTopRight = corners.contains(.topRight) ? radius : 0 + let radiusBottomLeft = corners.contains(.bottomLeft) ? radius : 0 + let radiusBottomRight = corners.contains(.bottomRight) ? radius : 0 + + self.init(roundedRect: rect, topLeftRadius: radiusTopLeft, topRightRadius: radiusTopRight, + bottomLeftRadius: radiusBottomLeft, bottomRightRadius: radiusBottomRight) + } +} + +extension KFCrossPlatformImage { + // macOS does not support scale. This is just for code compatibility across platforms. + convenience init?(data: Data, scale: CGFloat) { + self.init(data: data) + } +} +#endif + +#if canImport(UIKit) +import UIKit +extension RectCorner { + var uiRectCorner: UIRectCorner { + + var result: UIRectCorner = [] + + if contains(.topLeft) { result.insert(.topLeft) } + if contains(.topRight) { result.insert(.topRight) } + if contains(.bottomLeft) { result.insert(.bottomLeft) } + if contains(.bottomRight) { result.insert(.bottomRight) } + + return result + } +} +#endif + +extension Date { + var isPast: Bool { + return isPast(referenceDate: Date()) + } + + var isFuture: Bool { + return !isPast + } + + func isPast(referenceDate: Date) -> Bool { + return timeIntervalSince(referenceDate) <= 0 + } + + func isFuture(referenceDate: Date) -> Bool { + return !isPast(referenceDate: referenceDate) + } + + // `Date` in memory is a wrap for `TimeInterval`. But in file attribute it can only accept `Int` number. + // By default the system will `round` it. But it is not friendly for testing purpose. + // So we always `ceil` the value when used for file attributes. + var fileAttributeDate: Date { + return Date(timeIntervalSince1970: ceil(timeIntervalSince1970)) + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Result.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Result.swift new file mode 100644 index 0000000..b06500d --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Result.swift @@ -0,0 +1,71 @@ +// +// Result.swift +// Kingfisher +// +// Created by onevcat on 2018/09/22. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +// These helper methods are not public since we do not want them to be exposed or cause any conflicting. +// However, they are just wrapper of `ResultUtil` static methods. +extension Result where Failure: Error { + + /// Evaluates the given transform closures to create a single output value. + /// + /// - Parameters: + /// - onSuccess: A closure that transforms the success value. + /// - onFailure: A closure that transforms the error value. + /// - Returns: A single `Output` value. + func match( + onSuccess: (Success) -> Output, + onFailure: (Failure) -> Output) -> Output + { + switch self { + case let .success(value): + return onSuccess(value) + case let .failure(error): + return onFailure(error) + } + } + + func matchSuccess(with folder: (Success?) -> Output) -> Output { + return match( + onSuccess: { value in return folder(value) }, + onFailure: { _ in return folder(nil) } + ) + } + + func matchFailure(with folder: (Error?) -> Output) -> Output { + return match( + onSuccess: { _ in return folder(nil) }, + onFailure: { error in return folder(error) } + ) + } + + func match(with folder: (Success?, Error?) -> Output) -> Output { + return match( + onSuccess: { return folder($0, nil) }, + onFailure: { return folder(nil, $0) } + ) + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Runtime.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Runtime.swift new file mode 100644 index 0000000..d5818e2 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Runtime.swift @@ -0,0 +1,35 @@ +// +// Runtime.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/12. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +func getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> T? { + return objc_getAssociatedObject(object, key) as? T +} + +func setRetainedAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: T) { + objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift new file mode 100644 index 0000000..19d05d6 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift @@ -0,0 +1,110 @@ +// +// SizeExtensions.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import CoreGraphics + +extension CGSize: KingfisherCompatibleValue {} +extension KingfisherWrapper where Base == CGSize { + + /// Returns a size by resizing the `base` size to a target size under a given content mode. + /// + /// - Parameters: + /// - size: The target size to resize to. + /// - contentMode: Content mode of the target size should be when resizing. + /// - Returns: The resized size under the given `ContentMode`. + public func resize(to size: CGSize, for contentMode: ContentMode) -> CGSize { + switch contentMode { + case .aspectFit: + return constrained(size) + case .aspectFill: + return filling(size) + case .none: + return size + } + } + + /// Returns a size by resizing the `base` size by making it aspect fitting the given `size`. + /// + /// - Parameter size: The size in which the `base` should fit in. + /// - Returns: The size fitted in by the input `size`, while keeps `base` aspect. + public func constrained(_ size: CGSize) -> CGSize { + let aspectWidth = round(aspectRatio * size.height) + let aspectHeight = round(size.width / aspectRatio) + + return aspectWidth > size.width ? + CGSize(width: size.width, height: aspectHeight) : + CGSize(width: aspectWidth, height: size.height) + } + + /// Returns a size by resizing the `base` size by making it aspect filling the given `size`. + /// + /// - Parameter size: The size in which the `base` should fill. + /// - Returns: The size be filled by the input `size`, while keeps `base` aspect. + public func filling(_ size: CGSize) -> CGSize { + let aspectWidth = round(aspectRatio * size.height) + let aspectHeight = round(size.width / aspectRatio) + + return aspectWidth < size.width ? + CGSize(width: size.width, height: aspectHeight) : + CGSize(width: aspectWidth, height: size.height) + } + + /// Returns a `CGRect` for which the `base` size is constrained to an input `size` at a given `anchor` point. + /// + /// - Parameters: + /// - size: The size in which the `base` should be constrained to. + /// - anchor: An anchor point in which the size constraint should happen. + /// - Returns: The result `CGRect` for the constraint operation. + public func constrainedRect(for size: CGSize, anchor: CGPoint) -> CGRect { + + let unifiedAnchor = CGPoint(x: anchor.x.clamped(to: 0.0...1.0), + y: anchor.y.clamped(to: 0.0...1.0)) + + let x = unifiedAnchor.x * base.width - unifiedAnchor.x * size.width + let y = unifiedAnchor.y * base.height - unifiedAnchor.y * size.height + let r = CGRect(x: x, y: y, width: size.width, height: size.height) + + let ori = CGRect(origin: .zero, size: base) + return ori.intersection(r) + } + + private var aspectRatio: CGFloat { + return base.height == 0.0 ? 1.0 : base.width / base.height + } +} + +extension CGRect { + func scaled(_ scale: CGFloat) -> CGRect { + return CGRect(x: origin.x * scale, y: origin.y * scale, + width: size.width * scale, height: size.height * scale) + } +} + +extension Comparable { + func clamped(to limits: ClosedRange) -> Self { + return min(max(self, limits.lowerBound), limits.upperBound) + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/String+MD5.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/String+MD5.swift new file mode 100644 index 0000000..091fb62 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/String+MD5.swift @@ -0,0 +1,288 @@ +// +// String+MD5.swift +// Kingfisher +// +// Created by Wei Wang on 18/09/25. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CommonCrypto + +extension String: KingfisherCompatibleValue { } +extension KingfisherWrapper where Base == String { + var md5: String { + guard let data = base.data(using: .utf8) else { + return base + } + + let message = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in + return [UInt8](bytes) + } + + let MD5Calculator = MD5(message) + let MD5Data = MD5Calculator.calculate() + + var MD5String = String() + for c in MD5Data { + MD5String += String(format: "%02x", c) + } + return MD5String + } + + var ext: String? { + var ext = "" + if let index = base.lastIndex(of: ".") { + let extRange = base.index(index, offsetBy: 1).. 0 ? String(firstSeg) : nil + } +} + +// array of bytes, little-endian representation +func arrayOfBytes(_ value: T, length: Int? = nil) -> [UInt8] { + let totalBytes = length ?? (MemoryLayout.size * 8) + + let valuePointer = UnsafeMutablePointer.allocate(capacity: 1) + valuePointer.pointee = value + + let bytes = valuePointer.withMemoryRebound(to: UInt8.self, capacity: totalBytes) { (bytesPointer) -> [UInt8] in + var bytes = [UInt8](repeating: 0, count: totalBytes) + for j in 0...size, totalBytes) { + bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee + } + return bytes + } + + valuePointer.deinitialize(count: 1) + valuePointer.deallocate() + + return bytes +} + +extension Int { + // Array of bytes with optional padding (little-endian) + func bytes(_ totalBytes: Int = MemoryLayout.size) -> [UInt8] { + return arrayOfBytes(self, length: totalBytes) + } + +} + +extension NSMutableData { + + // Convenient way to append bytes + func appendBytes(_ arrayOfBytes: [UInt8]) { + append(arrayOfBytes, length: arrayOfBytes.count) + } + +} + +protocol HashProtocol { + var message: [UInt8] { get } + // Common part for hash calculation. Prepare header data. + func prepare(_ len: Int) -> [UInt8] +} + +extension HashProtocol { + + func prepare(_ len: Int) -> [UInt8] { + var tmpMessage = message + + // Step 1. Append Padding Bits + tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message + + // append "0" bit until message length in bits ≡ 448 (mod 512) + var msgLength = tmpMessage.count + var counter = 0 + + while msgLength % len != (len - 8) { + counter += 1 + msgLength += 1 + } + + tmpMessage += [UInt8](repeating: 0, count: counter) + return tmpMessage + } +} + +func toUInt32Array(_ slice: ArraySlice) -> [UInt32] { + var result = [UInt32]() + result.reserveCapacity(16) + + for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout.size) { + let d0 = UInt32(slice[idx.advanced(by: 3)]) << 24 + let d1 = UInt32(slice[idx.advanced(by: 2)]) << 16 + let d2 = UInt32(slice[idx.advanced(by: 1)]) << 8 + let d3 = UInt32(slice[idx]) + let val: UInt32 = d0 | d1 | d2 | d3 + + result.append(val) + } + return result +} + +struct BytesIterator: IteratorProtocol { + + let chunkSize: Int + let data: [UInt8] + + init(chunkSize: Int, data: [UInt8]) { + self.chunkSize = chunkSize + self.data = data + } + + var offset = 0 + + mutating func next() -> ArraySlice? { + let end = min(chunkSize, data.count - offset) + let result = data[offset.. 0 ? result : nil + } +} + +struct BytesSequence: Sequence { + let chunkSize: Int + let data: [UInt8] + + func makeIterator() -> BytesIterator { + return BytesIterator(chunkSize: chunkSize, data: data) + } +} + +func rotateLeft(_ value: UInt32, bits: UInt32) -> UInt32 { + return ((value << bits) & 0xFFFFFFFF) | (value >> (32 - bits)) +} + +class MD5: HashProtocol { + + static let size = 16 // 128 / 8 + let message: [UInt8] + + init (_ message: [UInt8]) { + self.message = message + } + + // specifies the per-round shift amounts + private let shifts: [UInt32] = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21] + + // binary integer part of the sines of integers (Radians) + private let sines: [UInt32] = [0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, + 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, + 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, + 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05, + 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, + 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391] + + private let hashes: [UInt32] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] + + func calculate() -> [UInt8] { + var tmpMessage = prepare(64) + tmpMessage.reserveCapacity(tmpMessage.count + 4) + + // hash values + var hh = hashes + + // Step 2. Append Length a 64-bit representation of lengthInBits + let lengthInBits = (message.count * 8) + let lengthBytes = lengthInBits.bytes(64 / 8) + tmpMessage += lengthBytes.reversed() + + // Process the message in successive 512-bit chunks: + let chunkSizeBytes = 512 / 8 // 64 + + for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) { + // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15 + let M = toUInt32Array(chunk) + assert(M.count == 16, "Invalid array") + + // Initialize hash value for this chunk: + var A: UInt32 = hh[0] + var B: UInt32 = hh[1] + var C: UInt32 = hh[2] + var D: UInt32 = hh[3] + + var dTemp: UInt32 = 0 + + // Main loop + for j in 0 ..< sines.count { + var g = 0 + var F: UInt32 = 0 + + switch j { + case 0...15: + F = (B & C) | ((~B) & D) + g = j + case 16...31: + F = (D & B) | (~D & C) + g = (5 * j + 1) % 16 + case 32...47: + F = B ^ C ^ D + g = (3 * j + 5) % 16 + case 48...63: + F = C ^ (B | (~D)) + g = (7 * j) % 16 + default: + break + } + dTemp = D + D = C + C = B + B = B &+ rotateLeft((A &+ F &+ sines[j] &+ M[g]), bits: shifts[j]) + A = dTemp + } + + hh[0] = hh[0] &+ A + hh[1] = hh[1] &+ B + hh[2] = hh[2] &+ C + hh[3] = hh[3] &+ D + } + var result = [UInt8]() + result.reserveCapacity(hh.count / 4) + + hh.forEach { + let itemLE = $0.littleEndian + let r1 = UInt8(itemLE & 0xff) + let r2 = UInt8((itemLE >> 8) & 0xff) + let r3 = UInt8((itemLE >> 16) & 0xff) + let r4 = UInt8((itemLE >> 24) & 0xff) + result += [r1, r2, r3, r4] + } + return result + } +} diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift new file mode 100644 index 0000000..94c2b7b --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift @@ -0,0 +1,660 @@ +// +// AnimatableImageView.swift +// Kingfisher +// +// Created by bl4ckra1sond3tre on 4/22/16. +// +// The AnimatableImageView, AnimatedFrame and Animator is a modified version of +// some classes from kaishin's Gifu project (https://github.com/kaishin/Gifu) +// +// The MIT License (MIT) +// +// Copyright (c) 2019 Reda Lemeden. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// The name and characters used in the demo of this software are property of their +// respective owners. + +#if !os(watchOS) +#if canImport(UIKit) +import UIKit +import ImageIO + +/// Protocol of `AnimatedImageView`. +public protocol AnimatedImageViewDelegate: AnyObject { + + /// Called after the animatedImageView has finished each animation loop. + /// + /// - Parameters: + /// - imageView: The `AnimatedImageView` that is being animated. + /// - count: The looped count. + func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) + + /// Called after the `AnimatedImageView` has reached the max repeat count. + /// + /// - Parameter imageView: The `AnimatedImageView` that is being animated. + func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) +} + +extension AnimatedImageViewDelegate { + public func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) {} + public func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) {} +} + +let KFRunLoopModeCommon = RunLoop.Mode.common + +/// Represents a subclass of `UIImageView` for displaying animated image. +/// Different from showing animated image in a normal `UIImageView` (which load all frames at one time), +/// `AnimatedImageView` only tries to load several frames (defined by `framePreloadCount`) to reduce memory usage. +/// It provides a tradeoff between memory usage and CPU time. If you have a memory issue when using a normal image +/// view to load GIF data, you could give this class a try. +/// +/// Kingfisher supports setting GIF animated data to either `UIImageView` and `AnimatedImageView` out of box. So +/// it would be fairly easy to switch between them. +open class AnimatedImageView: UIImageView { + /// Proxy object for preventing a reference cycle between the `CADDisplayLink` and `AnimatedImageView`. + class TargetProxy { + private weak var target: AnimatedImageView? + + init(target: AnimatedImageView) { + self.target = target + } + + @objc func onScreenUpdate() { + target?.updateFrameIfNeeded() + } + } + + /// Enumeration that specifies repeat count of GIF + public enum RepeatCount: Equatable { + case once + case finite(count: UInt) + case infinite + + public static func ==(lhs: RepeatCount, rhs: RepeatCount) -> Bool { + switch (lhs, rhs) { + case let (.finite(l), .finite(r)): + return l == r + case (.once, .once), + (.infinite, .infinite): + return true + case (.once, .finite(let count)), + (.finite(let count), .once): + return count == 1 + case (.once, _), + (.infinite, _), + (.finite, _): + return false + } + } + } + + // MARK: - Public property + /// Whether automatically play the animation when the view become visible. Default is `true`. + public var autoPlayAnimatedImage = true + + /// The count of the frames should be preloaded before shown. + public var framePreloadCount = 10 + + /// Specifies whether the GIF frames should be pre-scaled to the image view's size or not. + /// If the downloaded image is larger than the image view's size, it will help to reduce some memory use. + /// Default is `true`. + public var needsPrescaling = true + + /// Decode the GIF frames in background thread before using. It will decode frames data and do a off-screen + /// rendering to extract pixel information in background. This can reduce the main thread CPU usage. + public var backgroundDecode = true + + /// The animation timer's run loop mode. Default is `RunLoop.Mode.common`. + /// Set this property to `RunLoop.Mode.default` will make the animation pause during UIScrollView scrolling. + public var runLoopMode = KFRunLoopModeCommon { + willSet { + guard runLoopMode != newValue else { return } + stopAnimating() + displayLink.remove(from: .main, forMode: runLoopMode) + displayLink.add(to: .main, forMode: newValue) + startAnimating() + } + } + + /// The repeat count. The animated image will keep animate until it the loop count reaches this value. + /// Setting this value to another one will reset current animation. + /// + /// Default is `.infinite`, which means the animation will last forever. + public var repeatCount = RepeatCount.infinite { + didSet { + if oldValue != repeatCount { + reset() + setNeedsDisplay() + layer.setNeedsDisplay() + } + } + } + + /// Delegate of this `AnimatedImageView` object. See `AnimatedImageViewDelegate` protocol for more. + public weak var delegate: AnimatedImageViewDelegate? + + /// The `Animator` instance that holds the frames of a specific image in memory. + public private(set) var animator: Animator? + + // MARK: - Private property + // Dispatch queue used for preloading images. + private lazy var preloadQueue: DispatchQueue = { + return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue") + }() + + // A flag to avoid invalidating the displayLink on deinit if it was never created, because displayLink is so lazy. + private var isDisplayLinkInitialized: Bool = false + + // A display link that keeps calling the `updateFrame` method on every screen refresh. + private lazy var displayLink: CADisplayLink = { + isDisplayLinkInitialized = true + let displayLink = CADisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate)) + displayLink.add(to: .main, forMode: runLoopMode) + displayLink.isPaused = true + return displayLink + }() + + // MARK: - Override + override open var image: KFCrossPlatformImage? { + didSet { + if image != oldValue { + reset() + } + setNeedsDisplay() + layer.setNeedsDisplay() + } + } + + open override var isHighlighted: Bool { + get { + super.isHighlighted + } + set { + // Highlighted image is unsupported for animated images. + // See https://github.com/onevcat/Kingfisher/issues/1679 + if displayLink.isPaused { + super.isHighlighted = newValue + } + } + } + + deinit { + if isDisplayLinkInitialized { + displayLink.invalidate() + } + } + + override open var isAnimating: Bool { + if isDisplayLinkInitialized { + return !displayLink.isPaused + } else { + return super.isAnimating + } + } + + /// Starts the animation. + override open func startAnimating() { + guard !isAnimating else { return } + guard let animator = animator else { return } + guard !animator.isReachMaxRepeatCount else { return } + + displayLink.isPaused = false + } + + /// Stops the animation. + override open func stopAnimating() { + super.stopAnimating() + if isDisplayLinkInitialized { + displayLink.isPaused = true + } + } + + override open func display(_ layer: CALayer) { + layer.contents = animator?.currentFrameImage?.cgImage ?? image?.cgImage + } + + override open func didMoveToWindow() { + super.didMoveToWindow() + didMove() + } + + override open func didMoveToSuperview() { + super.didMoveToSuperview() + didMove() + } + + // This is for back compatibility that using regular `UIImageView` to show animated image. + override func shouldPreloadAllAnimation() -> Bool { + return false + } + + // Reset the animator. + private func reset() { + animator = nil + if let image = image, let imageSource = image.kf.imageSource { + let targetSize = bounds.scaled(UIScreen.main.scale).size + let animator = Animator( + imageSource: imageSource, + contentMode: contentMode, + size: targetSize, + imageSize: image.kf.size, + imageScale: image.kf.scale, + framePreloadCount: framePreloadCount, + repeatCount: repeatCount, + preloadQueue: preloadQueue) + animator.delegate = self + animator.needsPrescaling = needsPrescaling + animator.backgroundDecode = backgroundDecode + animator.prepareFramesAsynchronously() + self.animator = animator + } + didMove() + } + + private func didMove() { + if autoPlayAnimatedImage && animator != nil { + if let _ = superview, let _ = window { + startAnimating() + } else { + stopAnimating() + } + } + } + + /// Update the current frame with the displayLink duration. + private func updateFrameIfNeeded() { + guard let animator = animator else { + return + } + + guard !animator.isFinished else { + stopAnimating() + delegate?.animatedImageViewDidFinishAnimating(self) + return + } + + let duration: CFTimeInterval + + // CA based display link is opt-out from ProMotion by default. + // So the duration and its FPS might not match. + // See [#718](https://github.com/onevcat/Kingfisher/issues/718) + // By setting CADisableMinimumFrameDuration to YES in Info.plist may + // cause the preferredFramesPerSecond being 0 + let preferredFramesPerSecond = displayLink.preferredFramesPerSecond + if preferredFramesPerSecond == 0 { + duration = displayLink.duration + } else { + // Some devices (like iPad Pro 10.5) will have a different FPS. + duration = 1.0 / TimeInterval(preferredFramesPerSecond) + } + + animator.shouldChangeFrame(with: duration) { [weak self] hasNewFrame in + if hasNewFrame { + self?.layer.setNeedsDisplay() + } + } + } +} + +protocol AnimatorDelegate: AnyObject { + func animator(_ animator: AnimatedImageView.Animator, didPlayAnimationLoops count: UInt) +} + +extension AnimatedImageView: AnimatorDelegate { + func animator(_ animator: Animator, didPlayAnimationLoops count: UInt) { + delegate?.animatedImageView(self, didPlayAnimationLoops: count) + } +} + +extension AnimatedImageView { + + // Represents a single frame in a GIF. + struct AnimatedFrame { + + // The image to display for this frame. Its value is nil when the frame is removed from the buffer. + let image: UIImage? + + // The duration that this frame should remain active. + let duration: TimeInterval + + // A placeholder frame with no image assigned. + // Used to replace frames that are no longer needed in the animation. + var placeholderFrame: AnimatedFrame { + return AnimatedFrame(image: nil, duration: duration) + } + + // Whether this frame instance contains an image or not. + var isPlaceholder: Bool { + return image == nil + } + + // Returns a new instance from an optional image. + // + // - parameter image: An optional `UIImage` instance to be assigned to the new frame. + // - returns: An `AnimatedFrame` instance. + func makeAnimatedFrame(image: UIImage?) -> AnimatedFrame { + return AnimatedFrame(image: image, duration: duration) + } + } +} + +extension AnimatedImageView { + + // MARK: - Animator + + /// An animator which used to drive the data behind `AnimatedImageView`. + public class Animator { + private let size: CGSize + + private let imageSize: CGSize + private let imageScale: CGFloat + + /// The maximum count of image frames that needs preload. + public let maxFrameCount: Int + + private let imageSource: CGImageSource + private let maxRepeatCount: RepeatCount + + private let maxTimeStep: TimeInterval = 1.0 + private let animatedFrames = SafeArray() + private var frameCount = 0 + private var timeSinceLastFrameChange: TimeInterval = 0.0 + private var currentRepeatCount: UInt = 0 + + var isFinished: Bool = false + + var needsPrescaling = true + + var backgroundDecode = true + + weak var delegate: AnimatorDelegate? + + // Total duration of one animation loop + var loopDuration: TimeInterval = 0 + + /// The image of the current frame. + public var currentFrameImage: UIImage? { + return frame(at: currentFrameIndex) + } + + /// The duration of the current active frame duration. + public var currentFrameDuration: TimeInterval { + return duration(at: currentFrameIndex) + } + + /// The index of the current animation frame. + public internal(set) var currentFrameIndex = 0 { + didSet { + previousFrameIndex = oldValue + } + } + + var previousFrameIndex = 0 { + didSet { + preloadQueue.async { + self.updatePreloadedFrames() + } + } + } + + var isReachMaxRepeatCount: Bool { + switch maxRepeatCount { + case .once: + return currentRepeatCount >= 1 + case .finite(let maxCount): + return currentRepeatCount >= maxCount + case .infinite: + return false + } + } + + /// Whether the current frame is the last frame or not in the animation sequence. + public var isLastFrame: Bool { + return currentFrameIndex == frameCount - 1 + } + + var preloadingIsNeeded: Bool { + return maxFrameCount < frameCount - 1 + } + + var contentMode = UIView.ContentMode.scaleToFill + + private lazy var preloadQueue: DispatchQueue = { + return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue") + }() + + /// Creates an animator with image source reference. + /// + /// - Parameters: + /// - source: The reference of animated image. + /// - mode: Content mode of the `AnimatedImageView`. + /// - size: Size of the `AnimatedImageView`. + /// - imageSize: Size of the `KingfisherWrapper`. + /// - imageScale: Scale of the `KingfisherWrapper`. + /// - count: Count of frames needed to be preloaded. + /// - repeatCount: The repeat count should this animator uses. + /// - preloadQueue: Dispatch queue used for preloading images. + init(imageSource source: CGImageSource, + contentMode mode: UIView.ContentMode, + size: CGSize, + imageSize: CGSize, + imageScale: CGFloat, + framePreloadCount count: Int, + repeatCount: RepeatCount, + preloadQueue: DispatchQueue) { + self.imageSource = source + self.contentMode = mode + self.size = size + self.imageSize = imageSize + self.imageScale = imageScale + self.maxFrameCount = count + self.maxRepeatCount = repeatCount + self.preloadQueue = preloadQueue + + GraphicsContext.begin(size: imageSize, scale: imageScale) + } + + deinit { + resetAnimatedFrames() + GraphicsContext.end() + } + + /// Gets the image frame of a given index. + /// - Parameter index: The index of desired image. + /// - Returns: The decoded image at the frame. `nil` if the index is out of bound or the image is not yet loaded. + public func frame(at index: Int) -> KFCrossPlatformImage? { + return animatedFrames[index]?.image + } + + public func duration(at index: Int) -> TimeInterval { + return animatedFrames[index]?.duration ?? .infinity + } + + func prepareFramesAsynchronously() { + frameCount = Int(CGImageSourceGetCount(imageSource)) + animatedFrames.reserveCapacity(frameCount) + preloadQueue.async { [weak self] in + self?.setupAnimatedFrames() + } + } + + func shouldChangeFrame(with duration: CFTimeInterval, handler: (Bool) -> Void) { + incrementTimeSinceLastFrameChange(with: duration) + + if currentFrameDuration > timeSinceLastFrameChange { + handler(false) + } else { + resetTimeSinceLastFrameChange() + incrementCurrentFrameIndex() + handler(true) + } + } + + private func setupAnimatedFrames() { + resetAnimatedFrames() + + var duration: TimeInterval = 0 + + (0.. maxFrameCount { return } + animatedFrames[index] = animatedFrames[index]?.makeAnimatedFrame(image: loadFrame(at: index)) + } + + self.loopDuration = duration + } + + private func resetAnimatedFrames() { + animatedFrames.removeAll() + } + + private func loadFrame(at index: Int) -> UIImage? { + let resize = needsPrescaling && size != .zero + let options: [CFString: Any]? + if resize { + options = [ + kCGImageSourceCreateThumbnailFromImageIfAbsent: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height) + ] + } else { + options = nil + } + + guard let cgImage = CGImageSourceCreateImageAtIndex(imageSource, index, options as CFDictionary?) else { + return nil + } + + let image = KFCrossPlatformImage(cgImage: cgImage) + + guard let context = GraphicsContext.current(size: imageSize, scale: imageScale, inverting: true, cgImage: cgImage) else { + return image + } + + return backgroundDecode ? image.kf.decoded(on: context) : image + } + + private func updatePreloadedFrames() { + guard preloadingIsNeeded else { + return + } + + animatedFrames[previousFrameIndex] = animatedFrames[previousFrameIndex]?.placeholderFrame + + preloadIndexes(start: currentFrameIndex).forEach { index in + guard let currentAnimatedFrame = animatedFrames[index] else { return } + if !currentAnimatedFrame.isPlaceholder { return } + animatedFrames[index] = currentAnimatedFrame.makeAnimatedFrame(image: loadFrame(at: index)) + } + } + + private func incrementCurrentFrameIndex() { + let wasLastFrame = isLastFrame + currentFrameIndex = increment(frameIndex: currentFrameIndex) + if isLastFrame { + currentRepeatCount += 1 + if isReachMaxRepeatCount { + isFinished = true + + // Notify the delegate here because the animation is stopping. + delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount) + } + } else if wasLastFrame { + + // Notify the delegate that the loop completed + delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount) + } + } + + private func incrementTimeSinceLastFrameChange(with duration: TimeInterval) { + timeSinceLastFrameChange += min(maxTimeStep, duration) + } + + private func resetTimeSinceLastFrameChange() { + timeSinceLastFrameChange -= currentFrameDuration + } + + private func increment(frameIndex: Int, by value: Int = 1) -> Int { + return (frameIndex + value) % frameCount + } + + private func preloadIndexes(start index: Int) -> [Int] { + let nextIndex = increment(frameIndex: index) + let lastIndex = increment(frameIndex: index, by: maxFrameCount) + + if lastIndex >= nextIndex { + return [Int](nextIndex...lastIndex) + } else { + return [Int](nextIndex.. { + private var array: Array = [] + private let lock = NSLock() + + subscript(index: Int) -> Element? { + get { + lock.lock() + defer { lock.unlock() } + return array.indices ~= index ? array[index] : nil + } + + set { + lock.lock() + defer { lock.unlock() } + if let newValue = newValue, array.indices ~= index { + array[index] = newValue + } + } + } + + var count : Int { + lock.lock() + defer { lock.unlock() } + return array.count + } + + func reserveCapacity(_ count: Int) { + lock.lock() + defer { lock.unlock() } + array.reserveCapacity(count) + } + + func append(_ element: Element) { + lock.lock() + defer { lock.unlock() } + array += [element] + } + + func removeAll() { + lock.lock() + defer { lock.unlock() } + array = [] + } +} +#endif +#endif diff --git a/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Views/Indicator.swift b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Views/Indicator.swift new file mode 100644 index 0000000..a3fa5a4 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Kingfisher/Sources/Views/Indicator.swift @@ -0,0 +1,231 @@ +// +// Indicator.swift +// Kingfisher +// +// Created by João D. Moreira on 30/08/16. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if !os(watchOS) + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +public typealias IndicatorView = NSView +#else +import UIKit +public typealias IndicatorView = UIView +#endif + +/// Represents the activity indicator type which should be added to +/// an image view when an image is being downloaded. +/// +/// - none: No indicator. +/// - activity: Uses the system activity indicator. +/// - image: Uses an image as indicator. GIF is supported. +/// - custom: Uses a custom indicator. The type of associated value should conform to the `Indicator` protocol. +public enum IndicatorType { + /// No indicator. + case none + /// Uses the system activity indicator. + case activity + /// Uses an image as indicator. GIF is supported. + case image(imageData: Data) + /// Uses a custom indicator. The type of associated value should conform to the `Indicator` protocol. + case custom(indicator: Indicator) +} + +/// An indicator type which can be used to show the download task is in progress. +public protocol Indicator { + + /// Called when the indicator should start animating. + func startAnimatingView() + + /// Called when the indicator should stop animating. + func stopAnimatingView() + + /// Center offset of the indicator. Kingfisher will use this value to determine the position of + /// indicator in the super view. + var centerOffset: CGPoint { get } + + /// The indicator view which would be added to the super view. + var view: IndicatorView { get } + + /// The size strategy used when adding the indicator to image view. + /// - Parameter imageView: The super view of indicator. + func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy +} + +public enum IndicatorSizeStrategy { + case intrinsicSize + case full + case size(CGSize) +} + +extension Indicator { + + /// Default implementation of `centerOffset` of `Indicator`. The default value is `.zero`, means that there is + /// no offset for the indicator view. + public var centerOffset: CGPoint { return .zero } + + /// Default implementation of `centerOffset` of `Indicator`. The default value is `.full`, means that the indicator + /// will pin to the same height and width as the image view. + public func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy { + return .full + } +} + +// Displays a NSProgressIndicator / UIActivityIndicatorView +final class ActivityIndicator: Indicator { + + #if os(macOS) + private let activityIndicatorView: NSProgressIndicator + #else + private let activityIndicatorView: UIActivityIndicatorView + #endif + private var animatingCount = 0 + + var view: IndicatorView { + return activityIndicatorView + } + + func startAnimatingView() { + if animatingCount == 0 { + #if os(macOS) + activityIndicatorView.startAnimation(nil) + #else + activityIndicatorView.startAnimating() + #endif + activityIndicatorView.isHidden = false + } + animatingCount += 1 + } + + func stopAnimatingView() { + animatingCount = max(animatingCount - 1, 0) + if animatingCount == 0 { + #if os(macOS) + activityIndicatorView.stopAnimation(nil) + #else + activityIndicatorView.stopAnimating() + #endif + activityIndicatorView.isHidden = true + } + } + + func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy { + return .intrinsicSize + } + + init() { + #if os(macOS) + activityIndicatorView = NSProgressIndicator(frame: CGRect(x: 0, y: 0, width: 16, height: 16)) + activityIndicatorView.controlSize = .small + activityIndicatorView.style = .spinning + #else + let indicatorStyle: UIActivityIndicatorView.Style + + #if os(tvOS) + if #available(tvOS 13.0, *) { + indicatorStyle = UIActivityIndicatorView.Style.large + } else { + indicatorStyle = UIActivityIndicatorView.Style.white + } + #else + if #available(iOS 13.0, * ) { + indicatorStyle = UIActivityIndicatorView.Style.medium + } else { + indicatorStyle = UIActivityIndicatorView.Style.gray + } + #endif + + activityIndicatorView = UIActivityIndicatorView(style: indicatorStyle) + #endif + } +} + +#if canImport(UIKit) +extension UIActivityIndicatorView.Style { + #if compiler(>=5.1) + #else + static let large = UIActivityIndicatorView.Style.white + #if !os(tvOS) + static let medium = UIActivityIndicatorView.Style.gray + #endif + #endif +} +#endif + +// MARK: - ImageIndicator +// Displays an ImageView. Supports gif +final class ImageIndicator: Indicator { + private let animatedImageIndicatorView: KFCrossPlatformImageView + + var view: IndicatorView { + return animatedImageIndicatorView + } + + init?( + imageData data: Data, + processor: ImageProcessor = DefaultImageProcessor.default, + options: KingfisherParsedOptionsInfo? = nil) + { + var options = options ?? KingfisherParsedOptionsInfo(nil) + // Use normal image view to show animations, so we need to preload all animation data. + if !options.preloadAllAnimationData { + options.preloadAllAnimationData = true + } + + guard let image = processor.process(item: .data(data), options: options) else { + return nil + } + + animatedImageIndicatorView = KFCrossPlatformImageView() + animatedImageIndicatorView.image = image + + #if os(macOS) + // Need for gif to animate on macOS + animatedImageIndicatorView.imageScaling = .scaleNone + animatedImageIndicatorView.canDrawSubviewsIntoLayer = true + #else + animatedImageIndicatorView.contentMode = .center + #endif + } + + func startAnimatingView() { + #if os(macOS) + animatedImageIndicatorView.animates = true + #else + animatedImageIndicatorView.startAnimating() + #endif + animatedImageIndicatorView.isHidden = false + } + + func stopAnimatingView() { + #if os(macOS) + animatedImageIndicatorView.animates = false + #else + animatedImageIndicatorView.stopAnimating() + #endif + animatedImageIndicatorView.isHidden = true + } +} + +#endif diff --git a/jaem/week6/NewsApp/Pods/Manifest.lock b/jaem/week6/NewsApp/Pods/Manifest.lock new file mode 100644 index 0000000..ee7e733 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Manifest.lock @@ -0,0 +1,20 @@ +PODS: + - Alamofire (5.6.1) + - Kingfisher (7.2.2) + +DEPENDENCIES: + - Alamofire + - Kingfisher (~> 7.0) + +SPEC REPOS: + trunk: + - Alamofire + - Kingfisher + +SPEC CHECKSUMS: + Alamofire: 87bd8c952f9a4454320fce00d9cc3de57bcadaf5 + Kingfisher: 184d4d1a8c36666e663caf8e08abe87898595c53 + +PODFILE CHECKSUM: eb5eeb66a7ef05a29db918fad840f9d4c14b9b25 + +COCOAPODS: 1.11.3 diff --git a/jaem/week6/NewsApp/Pods/Pods.xcodeproj/project.pbxproj b/jaem/week6/NewsApp/Pods/Pods.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0665929 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Pods.xcodeproj/project.pbxproj @@ -0,0 +1,1555 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 005B319B494ED2DAA239B9939A504DFC /* Alamofire-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D167A016457E1A5BD5B9A73C4FF9434 /* Alamofire-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 00BEA6029C428FEE644AC3D42AD83282 /* ImagePrefetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 240168000C008585397987F341FC9AFA /* ImagePrefetcher.swift */; }; + 0285857A24F66E925987A5876F0BE63B /* ImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 597967B6CDA2E72751463CF56198E927 /* ImageDataProvider.swift */; }; + 045DE6EBF9B2F63F60F5BE60C1198E06 /* RedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 499955C35F6D4A8F96E940FF129FF498 /* RedirectHandler.swift */; }; + 04A896288CE3A59B530250337A5F8362 /* Result+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3F986AF94568CEEB8AF631C72F03B2 /* Result+Alamofire.swift */; }; + 0510E8EA51914CB2176AD0F173937FAB /* KFImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26990E9D251C57CAB34BC5C9EC37033 /* KFImageRenderer.swift */; }; + 05228565AAA7FCED4BAFB2B7EF71D53D /* KingfisherOptionsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8B2E44706F2E23444FC30F3BAEE3DC /* KingfisherOptionsInfo.swift */; }; + 059639E700DEFAEF08F56484E5F67BE7 /* NSButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6E93449DD3CC778DD5D443C7780D895 /* NSButton+Kingfisher.swift */; }; + 082EDC820D76DF95C71A5018112DE512 /* UIButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9B50C2ED49A8E73A4D64C205173820 /* UIButton+Kingfisher.swift */; }; + 0ED8FBFD9A86D21BF69137EC9350E575 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9ECAD406B234DD522B04457BCADA54 /* SessionDelegate.swift */; }; + 0F4037DBF307AC8058BD0A3D35C7E7E9 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */; }; + 1185A2B40E14F2FCBC761FC99777CAD8 /* ExtensionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365099CC49560F0DAFBF90D1A7FBFD24 /* ExtensionHelpers.swift */; }; + 122980E44B15C64CF0B14DC94D7EB5C9 /* Kingfisher-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 6C321B1DFC61DFB5E1EB0BF63C4E7B05 /* Kingfisher-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1952485AFF7A1BCCA4D4B142E82FE627 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06442349E67A8F1944EBFB63B46B406E /* AnimatedImageView.swift */; }; + 1976BB7D7E26A12E29283E71154B63B3 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B42FDB98681E0F2B54A53C6263FC0FB7 /* SessionDelegate.swift */; }; + 1AA89F327105C026976BF6E831B193A2 /* ImageBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE0A9D42161F3A6512AEE4A7F63B4A0 /* ImageBinder.swift */; }; + 1B28908D035557EB037085CBD6131F37 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */; }; + 1C6C582579ADF5257B3AF3184D1CD98D /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */; }; + 1EE44196E7BCE57AD96A2C751651EF40 /* AlamofireExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72447455A0B451F260AE94E750E590D7 /* AlamofireExtended.swift */; }; + 1FD2928BC156D990D68B105F518C60B6 /* MemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2601AB15F49167B95C5DDC2FBC7B78 /* MemoryStorage.swift */; }; + 1FE693B5ACC6AD7320CEFC20B64546E4 /* KingfisherManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7AD2807A19FC11299041D8A8777267 /* KingfisherManager.swift */; }; + 1FEE89BF952BE7ACA46E642DA2E48CA2 /* DiskStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D362E01704CD82D45E9C7857FB20AC1E /* DiskStorage.swift */; }; + 22216C300C763044344B9DBF97317E63 /* RetryStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F40A9A287AE4680FD4A041AAD0C5ED /* RetryStrategy.swift */; }; + 22BD1346F66BFCB129AAA44EEF322AC9 /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BFD9705B9A22E25BBBD0A608C90CCC /* Resource.swift */; }; + 243D7CFE1D56ED80ACB2B3E71B4CB603 /* AuthenticationChallengeResponsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DBC7F03013C95C65EE295C4DAC2A64 /* AuthenticationChallengeResponsable.swift */; }; + 25FC036BEA33CAB5D80F5A41644535D3 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCDFE9C10374511CCC375D2E99C812E /* Storage.swift */; }; + 2969A24709C57F23CCDB8DB5000384EB /* Pods-NewsAppTests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = D03523C8957B09097CA8D933BF03E5EE /* Pods-NewsAppTests-dummy.m */; }; + 29FF13E23FD52E46D30530549410AD7C /* ImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1FA3BADCA4576957512EC8D60D2B76 /* ImageTransition.swift */; }; + 2BE89C24BFD3FB663E37C607C289B3B6 /* RedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C1BAC7555E3D0AC24A397BB028A3CEC /* RedirectHandler.swift */; }; + 2CBE3651CA006E19F5D64A2DE9B9A028 /* CachedResponseHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A117735F022F30BB9DC641136E51C9 /* CachedResponseHandler.swift */; }; + 2CCD13099063CD560E3067BD132914FA /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC710AFB6066D8535108054FD1115E94 /* Notifications.swift */; }; + 33A7D0F2D03004CE256A75E03DF33C70 /* RetryPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 715993D57B4975C3CF94C6BC997595C6 /* RetryPolicy.swift */; }; + 3A0FEB7407D69B9499D00B6E664D6131 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */; }; + 3AD5DBB915C2623991F7DBACD173BBB4 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */; }; + 3AF7DB9AEFF47F1F7F91AF28440E4AC6 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A723238162323CE3B380FA8E894C865 /* Filter.swift */; }; + 3C4059621E23842C19D4EB5D35B41989 /* Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67030433EFF4B0333EC8811BC7EC47D6 /* Validation.swift */; }; + 420C200A05BB29E1D299D1BADE9139D2 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 872D7EFA572ECEE8EF993C27196E16DD /* CFNetwork.framework */; }; + 457BE444ED617FA7D6851D6DAA9D7234 /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B347AF8E0B52CE83F1B9A8F51F1AE31 /* Delegate.swift */; }; + 46A64A43AFA057B6B63C8F0C12F509B4 /* Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = B114DBD20B001B2A9D93F8F8C47903B6 /* Combine.swift */; }; + 4DCA9775E5CCF599460BDB46E77F6FA4 /* Kingfisher-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 6835FBF931D5C31EFFF83ED559436CDE /* Kingfisher-dummy.m */; }; + 4F37E521D341C47CE73DDCF21BA95A52 /* KingfisherError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3CD3E23CCF124B01CA690E27E72D1B9 /* KingfisherError.swift */; }; + 506128E1CC424E40E2691546D9547549 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C6C8F94FCB9064F02B48B607AB69962 /* Placeholder.swift */; }; + 509490FB1D30FEC59AE4BC21AEEBB7BB /* RequestModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C264879E6B16EA9E07F073E24220A232 /* RequestModifier.swift */; }; + 55AABB1FB38F61A3369ACC555FF3046D /* Alamofire-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D13E09327BDC573DE5AAFE079172AB9 /* Alamofire-dummy.m */; }; + 57FC31B14C753B5C63CEF00560F8A6EF /* SizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CBA959C728F5B6931C11DE61853D88 /* SizeExtensions.swift */; }; + 582D59E0D2EF62E0575933C99B393704 /* GraphicsContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D370E0D48C1925AC14D155BEDF3DC7D4 /* GraphicsContext.swift */; }; + 59BC9047F4BEBBC06235608D974E230D /* NSTextAttachment+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EBE61AE143E2DB816492EB19AA1651F /* NSTextAttachment+Kingfisher.swift */; }; + 5ADB30DD9A03859018550A999ACB0652 /* KFImageOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BAF59EA7A7DD980421989D2A205E6B0 /* KFImageOptions.swift */; }; + 5E27DD292D3A55657712DD7AFA7B8FCA /* KFImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9566B136FA420BDAAE8B272AE10CFE3E /* KFImage.swift */; }; + 5F852F38CBC282496CCBE37C51324B2F /* ImageProgressive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7366FB0DEF319ED32BAF9DE5654C8DB7 /* ImageProgressive.swift */; }; + 68FB2DCB4C77DBCAF9A6037E470F2BDE /* ParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 249A902C9828CB270329EFFAFAC1843A /* ParameterEncoding.swift */; }; + 70FEC06F54286257E1BA1ECA0C99198D /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16E37EF24D9E916BB47E43D8E954D3AF /* Image.swift */; }; + 7483E5327027263F7E4B94A2997191C4 /* AuthenticationInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B7F2B2CCF604EF10790364BAB06391 /* AuthenticationInterceptor.swift */; }; + 75966A9262648D4647D764E3E76BC6AC /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB099A6B514875F22EF98798B56B4BA5 /* Response.swift */; }; + 7930C94414B4C661867AC4FBE82E996C /* URLEncodedFormEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072684B1154141462994E3AD2ACD3606 /* URLEncodedFormEncoder.swift */; }; + 7B068137A8925891446203B5D3D6A4ED /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 872D7EFA572ECEE8EF993C27196E16DD /* CFNetwork.framework */; }; + 7C7418FF01DD7BB909719682B634A8A5 /* SessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C323A988742445270C1D6A4DD40ED07 /* SessionDataTask.swift */; }; + 7E02F5B62BE00E97847DF549FFED2490 /* HTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FAB3233CC80F28F7E66DF4DC9F2FA /* HTTPHeaders.swift */; }; + 7F1BB526AAE3ECDCE90127D9D0E10261 /* StringEncoding+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = D942B0884573FD857CFB17A94E6A5807 /* StringEncoding+Alamofire.swift */; }; + 7FE695DA8EE7FF1286556E06B692009B /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EE41735C363063D401BC89633A455BE /* MultipartFormData.swift */; }; + 7FFE4021A4F14124342AD41CE1117B3E /* KFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86DDDE6D887ED6DFE35529D771930C65 /* KFAnimatedImage.swift */; }; + 80738D8956C9987CCCEDF551961E5069 /* ImageDataProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D911DA9899C96EA4408220F4754219 /* ImageDataProcessor.swift */; }; + 808C960C82D708FC1A42C581D6CB4940 /* URLSessionConfiguration+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7407E80368EF1CE2A091B4F0F8F81FF /* URLSessionConfiguration+Alamofire.swift */; }; + 81B8D2B7CEB25C2448B0BC9B33591A65 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1939B0AEE888F2EB258D6832523F67C /* Session.swift */; }; + 824D816B1EE404F2DD400EE678695CBE /* ResponseSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C32A86B99F0C8B9E42B3D9C7F9730889 /* ResponseSerialization.swift */; }; + 881A35B28D93C56E46E305F6138B1A76 /* ImageDownloaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077E33C20C9BD4FAF66AE7A6EC1657F4 /* ImageDownloaderDelegate.swift */; }; + 8D75FC8D7476C9674234F39F1A820D8C /* URLConvertible+URLRequestConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404743B9B61FE5357078AFD049FC6B45 /* URLConvertible+URLRequestConvertible.swift */; }; + 8EAF77C22278D0A4A8D696BCBB64E7B8 /* Pods-NewsApp-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 8753ADE1B95DA6998495A947D1C6CAB8 /* Pods-NewsApp-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 99D058E53EFEE3AC4857CDE3DBA5C004 /* ParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A1EDFD24518B3F930192EDAE086842 /* ParameterEncoder.swift */; }; + 9B0A78AC22E7EDA755F51D86527E2D9C /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496EC73E3893F724BA57D993B154BF49 /* Source.swift */; }; + 9C9030DEDB0DF955B16FE08C50892D57 /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1529C2E8B334710FA50E4C9A7153006A /* Concurrency.swift */; }; + 9F5FE22DA95B66B8DC21CB13BE25EC9B /* WKInterfaceImage+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C444AA66DFF06EB1D1D714FE5939A17 /* WKInterfaceImage+Kingfisher.swift */; }; + A29100AA1876DDEFF6F54694A51FDB0E /* NetworkReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A043F9EF1DF150712ECDD2FB7CD2E46B /* NetworkReachabilityManager.swift */; }; + A316388A35648CB2987E761771456087 /* KFOptionsSetter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011604690A5B598427758F666A2F13C8 /* KFOptionsSetter.swift */; }; + A39D3555EC8B45B7D6B9505DDAF0F117 /* Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7A8F9A880D8CF92488BF36640F2DBBE /* Kingfisher.swift */; }; + A53BDE589BDD6483F3EEDCE5EA1DCCD3 /* Protected.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFFE766A6E0F3A621DFD9487DA3CAF7E /* Protected.swift */; }; + A88A844D5356E1690E445024CB796E09 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7006AE0546B679604D685DCA785C542C /* Result.swift */; }; + AA44E3F1A54787E853CBCDA8F296061C /* Pods-NewsApp-NewsAppUITests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = AC42B34F4BEA8B3606A4D9B8CA9E63FB /* Pods-NewsApp-NewsAppUITests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B25E07EA645911443A38DA1E68166156 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B7F4B3A6C95F8600D69F851451D63F6 /* Box.swift */; }; + B3658C29BBDE1033F6269A92E612CB30 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0341B04B64485596391C33DD9360D74E /* Request.swift */; }; + B704B198B9B520D449260877E300D821 /* ServerTrustEvaluation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04303942CB47C4C340CF398F8653BB98 /* ServerTrustEvaluation.swift */; }; + BC0ECA8F22DEDE8886E189CD0EAA1197 /* URLRequest+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB31688140758856323E0B3187769487 /* URLRequest+Alamofire.swift */; }; + BD382E78580D295D10100678D4F66A76 /* String+MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23963C2629645E475AB2931F62E451C7 /* String+MD5.swift */; }; + CA4162FB68C4A59A55D9B8733D2225DC /* Pods-NewsApp-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A66915525E199A205AB96CEAA6F6F8B /* Pods-NewsApp-dummy.m */; }; + CD7AC3E1C98EA54F7C05C36C52805220 /* CacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC3A37B1FA2F4841F5E8CC3FC102A19 /* CacheSerializer.swift */; }; + CEBFFEED65D877702B2F36102528CF6D /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932C67B305CF8C8942FEBD2375BE2E1F /* EventMonitor.swift */; }; + D0EA90FBF83350C49E6EF6C8A98D6F00 /* AFError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80270ADB4ECE74E17A7DB57236035920 /* AFError.swift */; }; + D5E8B596B6BA50EBE333C08018168137 /* Pods-NewsApp-NewsAppUITests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = E640F25A4959F11A2B05BD5612BF5BC9 /* Pods-NewsApp-NewsAppUITests-dummy.m */; }; + D603AA58EF97D461A57B2B1BCB883868 /* AVAssetImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74729EFA464A2B107050ED713601AB8 /* AVAssetImageDataProvider.swift */; }; + D6B4751CED01D53E4A1B6A571AAA2F83 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B56F1736D1127A32614C8CBBEF3E5F6 /* HTTPMethod.swift */; }; + DA34899BEF0668D76CBCE8C4CE47B97B /* RequestTaskMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0DD802295003561B6C5741521D716BC /* RequestTaskMap.swift */; }; + DAFC6CE6321395CF4523DD66DADBB9BA /* ImageDrawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7933A5F1F8BD20A48039AA759ADB465E /* ImageDrawing.swift */; }; + DBB8088E14A2ADEDB1CD840BAC835267 /* CPListItem+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142C2CAF3091DF534D3E7B4AAA4DFC0B /* CPListItem+Kingfisher.swift */; }; + DD72DC30CF19FFC81AB19CD0B074000D /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE76C5003FE080CF8820CA18C434EA3 /* ImageDownloader.swift */; }; + DD902FE8D6824681C929D028655AE121 /* RequestInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6599B0D2C1F4B263D4D859853F0F348A /* RequestInterceptor.swift */; }; + DE532EF7D50A9CF68587DAD4C1A02BD7 /* FormatIndicatedCacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8676B059D0C43C5A2FC81E0F55AD63D7 /* FormatIndicatedCacheSerializer.swift */; }; + DF4563832C19B8582C810BF502A5CA29 /* KF.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660CF05D9FA7E4371D6E6A45ED8EE01F /* KF.swift */; }; + DFCDE4638265B4CCD494ECA5D560DBEE /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5723DEFD772E79C36C19B268F7643CE0 /* Indicator.swift */; }; + E54654D504A42C24F284A68F87F7671D /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20EDDB802D0AEBA579A2FAC5B1B6BDE /* OperationQueue+Alamofire.swift */; }; + E5B664771063F1A9A372519A8466860B /* ImageView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 985C3C905A92749F5FB0325692FBF789 /* ImageView+Kingfisher.swift */; }; + E6D6C7D5E458A05CC736C340F853E9F6 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C65DBD1C381FE1E4AA74F824E85F325 /* ImageFormat.swift */; }; + E719A3B025B9DACE693130120BD9B927 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52554E1C9731D5352FDE9E63F8C5466B /* Accelerate.framework */; }; + E9B4C89E7EB3B27D46AFCA452C3D426F /* MultipartUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BA6A6C6AEAC43638A2B248BBEA90EBD /* MultipartUpload.swift */; }; + EBB32304E8DD4BA115454E0050D47DED /* Runtime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CB85CAAF3329112CD2FFF9889E98FF7 /* Runtime.swift */; }; + ED0C8BA7560D7324587B353E0960479F /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE9B6094B04E75ACDBB73C3A194D690 /* ImageCache.swift */; }; + EEC150B66BCCD6C80FDA7E4D1975166B /* DispatchQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48BAA15044E6AD772F77A856FA5107F2 /* DispatchQueue+Alamofire.swift */; }; + EF9C4588CDA85AED8BBCF77451B2A35B /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95F29F7BBB1AA54AD679386E26C7740 /* ImageProcessor.swift */; }; + F17A4CA4664CABB331D39FE902E06843 /* Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76CC7FAE70D848424E201CB9C76CABD3 /* Alamofire.swift */; }; + F17B1F8F2B6580343025237455A29D61 /* TVMonogramView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4193FEA28CDEB3710C7C41537C8D5E2E /* TVMonogramView+Kingfisher.swift */; }; + F1F7715C2158962AF8E13DA25F6BA69C /* Pods-NewsAppTests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C9A11397034BC6E9818A86ABE37D261 /* Pods-NewsAppTests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F24021BDE9B42D604E3341CAD8E34759 /* GIFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF4F904B2EF1F1E836FADDE8C6B05DF /* GIFAnimatedImage.swift */; }; + F5414F8A5B40521D0E4AEEB28378CB49 /* ImageContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB10145B8616970B7B29BFF058CF910 /* ImageContext.swift */; }; + F54DE563418B1783D6EC491A0C3A05DB /* ImageModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07524CF920EFBC25C9A0C9B3E57AA118 /* ImageModifier.swift */; }; + F9537B023E24AC4A724E301F7E372491 /* KFImageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292F77B8C48397E58C30BC7A313423E4 /* KFImageProtocol.swift */; }; + FF09824309346665E2F1F7F5A45FB10F /* CallbackQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CCE8AF433C89864A9FE3C1F9C34A96 /* CallbackQueue.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 2EC17AF2A6A8BD0B29B98700AAE63556 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E8022D22FAA6690B5E1C379C1BCE1491; + remoteInfo = Kingfisher; + }; + 9E094CE8AC863886AD47DDAF6238E99B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EAAA1AD3A8A1B59AB91319EE40752C6D; + remoteInfo = Alamofire; + }; + AF0E1994F63FC3036B04577256FDD251 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EAAA1AD3A8A1B59AB91319EE40752C6D; + remoteInfo = Alamofire; + }; + C5EB1635709E055474F1A5375A8B24FC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6B14A80A672BEFB65A571F51A375D8D1; + remoteInfo = "Pods-NewsApp"; + }; + F4BABA04C3C7E4011126649748B6BC65 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E8022D22FAA6690B5E1C379C1BCE1491; + remoteInfo = Kingfisher; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 011604690A5B598427758F666A2F13C8 /* KFOptionsSetter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFOptionsSetter.swift; path = Sources/General/KFOptionsSetter.swift; sourceTree = ""; }; + 0232C9EF1ACF5315E3EAF12996CCBF4E /* Pods-NewsAppTests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-NewsAppTests.modulemap"; sourceTree = ""; }; + 031F7B599607B10186672B7EB854655B /* Pods-NewsApp-NewsAppUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-NewsApp-NewsAppUITests.debug.xcconfig"; sourceTree = ""; }; + 0341B04B64485596391C33DD9360D74E /* Request.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Request.swift; path = Source/Request.swift; sourceTree = ""; }; + 04303942CB47C4C340CF398F8653BB98 /* ServerTrustEvaluation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServerTrustEvaluation.swift; path = Source/ServerTrustEvaluation.swift; sourceTree = ""; }; + 06442349E67A8F1944EBFB63B46B406E /* AnimatedImageView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AnimatedImageView.swift; path = Sources/Views/AnimatedImageView.swift; sourceTree = ""; }; + 072684B1154141462994E3AD2ACD3606 /* URLEncodedFormEncoder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = URLEncodedFormEncoder.swift; path = Source/URLEncodedFormEncoder.swift; sourceTree = ""; }; + 07524CF920EFBC25C9A0C9B3E57AA118 /* ImageModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageModifier.swift; path = Sources/Networking/ImageModifier.swift; sourceTree = ""; }; + 077E33C20C9BD4FAF66AE7A6EC1657F4 /* ImageDownloaderDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDownloaderDelegate.swift; path = Sources/Networking/ImageDownloaderDelegate.swift; sourceTree = ""; }; + 09496892A4F123263B81DEDFA7F99078 /* Pods-NewsApp-NewsAppUITests-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-NewsApp-NewsAppUITests-frameworks.sh"; sourceTree = ""; }; + 0B347AF8E0B52CE83F1B9A8F51F1AE31 /* Delegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Delegate.swift; path = Sources/Utility/Delegate.swift; sourceTree = ""; }; + 0B7F4B3A6C95F8600D69F851451D63F6 /* Box.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Box.swift; path = Sources/Utility/Box.swift; sourceTree = ""; }; + 0BC3A37B1FA2F4841F5E8CC3FC102A19 /* CacheSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CacheSerializer.swift; path = Sources/Cache/CacheSerializer.swift; sourceTree = ""; }; + 0C323A988742445270C1D6A4DD40ED07 /* SessionDataTask.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDataTask.swift; path = Sources/Networking/SessionDataTask.swift; sourceTree = ""; }; + 0D0038510FD52FD645AC54E46EA78C31 /* Pods-NewsApp-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-NewsApp-Info.plist"; sourceTree = ""; }; + 0D167A016457E1A5BD5B9A73C4FF9434 /* Alamofire-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Alamofire-umbrella.h"; sourceTree = ""; }; + 142C2CAF3091DF534D3E7B4AAA4DFC0B /* CPListItem+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "CPListItem+Kingfisher.swift"; path = "Sources/Extensions/CPListItem+Kingfisher.swift"; sourceTree = ""; }; + 148FA9BD1508B6E7E220C9D9BF1928F8 /* Kingfisher-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Kingfisher-prefix.pch"; sourceTree = ""; }; + 1529C2E8B334710FA50E4C9A7153006A /* Concurrency.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Concurrency.swift; path = Source/Concurrency.swift; sourceTree = ""; }; + 16E37EF24D9E916BB47E43D8E954D3AF /* Image.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Image.swift; path = Sources/Image/Image.swift; sourceTree = ""; }; + 1986AFB6D3CAD62304F3FA85BE29D575 /* Kingfisher-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Kingfisher-Info.plist"; sourceTree = ""; }; + 1B8B2E44706F2E23444FC30F3BAEE3DC /* KingfisherOptionsInfo.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherOptionsInfo.swift; path = Sources/General/KingfisherOptionsInfo.swift; sourceTree = ""; }; + 1C1BAC7555E3D0AC24A397BB028A3CEC /* RedirectHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedirectHandler.swift; path = Sources/Networking/RedirectHandler.swift; sourceTree = ""; }; + 1C444AA66DFF06EB1D1D714FE5939A17 /* WKInterfaceImage+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "WKInterfaceImage+Kingfisher.swift"; path = "Sources/Extensions/WKInterfaceImage+Kingfisher.swift"; sourceTree = ""; }; + 1CB85CAAF3329112CD2FFF9889E98FF7 /* Runtime.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Runtime.swift; path = Sources/Utility/Runtime.swift; sourceTree = ""; }; + 1D1A7C603C2A16508B0260C5D7CEEBC4 /* Pods-NewsApp-NewsAppUITests-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-NewsApp-NewsAppUITests-Info.plist"; sourceTree = ""; }; + 1EBE61AE143E2DB816492EB19AA1651F /* NSTextAttachment+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSTextAttachment+Kingfisher.swift"; path = "Sources/Extensions/NSTextAttachment+Kingfisher.swift"; sourceTree = ""; }; + 21DBC7F03013C95C65EE295C4DAC2A64 /* AuthenticationChallengeResponsable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthenticationChallengeResponsable.swift; path = Sources/Networking/AuthenticationChallengeResponsable.swift; sourceTree = ""; }; + 23963C2629645E475AB2931F62E451C7 /* String+MD5.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "String+MD5.swift"; path = "Sources/Utility/String+MD5.swift"; sourceTree = ""; }; + 240168000C008585397987F341FC9AFA /* ImagePrefetcher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImagePrefetcher.swift; path = Sources/Networking/ImagePrefetcher.swift; sourceTree = ""; }; + 249A902C9828CB270329EFFAFAC1843A /* ParameterEncoding.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ParameterEncoding.swift; path = Source/ParameterEncoding.swift; sourceTree = ""; }; + 24A6C9EC04946183C998CEC9FFF72EFD /* Alamofire-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Alamofire-prefix.pch"; sourceTree = ""; }; + 27F40A9A287AE4680FD4A041AAD0C5ED /* RetryStrategy.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RetryStrategy.swift; path = Sources/Networking/RetryStrategy.swift; sourceTree = ""; }; + 292F77B8C48397E58C30BC7A313423E4 /* KFImageProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageProtocol.swift; path = Sources/SwiftUI/KFImageProtocol.swift; sourceTree = ""; }; + 2BAF59EA7A7DD980421989D2A205E6B0 /* KFImageOptions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageOptions.swift; path = Sources/SwiftUI/KFImageOptions.swift; sourceTree = ""; }; + 2F3F986AF94568CEEB8AF631C72F03B2 /* Result+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Result+Alamofire.swift"; path = "Source/Result+Alamofire.swift"; sourceTree = ""; }; + 365099CC49560F0DAFBF90D1A7FBFD24 /* ExtensionHelpers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExtensionHelpers.swift; path = Sources/Utility/ExtensionHelpers.swift; sourceTree = ""; }; + 37D911DA9899C96EA4408220F4754219 /* ImageDataProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDataProcessor.swift; path = Sources/Networking/ImageDataProcessor.swift; sourceTree = ""; }; + 39E3E5F872763E10C58E7DEF2B8ED837 /* Pods-NewsApp */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-NewsApp"; path = Pods_NewsApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3C65DBD1C381FE1E4AA74F824E85F325 /* ImageFormat.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageFormat.swift; path = Sources/Image/ImageFormat.swift; sourceTree = ""; }; + 3D13E09327BDC573DE5AAFE079172AB9 /* Alamofire-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Alamofire-dummy.m"; sourceTree = ""; }; + 404743B9B61FE5357078AFD049FC6B45 /* URLConvertible+URLRequestConvertible.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URLConvertible+URLRequestConvertible.swift"; path = "Source/URLConvertible+URLRequestConvertible.swift"; sourceTree = ""; }; + 40CCE8AF433C89864A9FE3C1F9C34A96 /* CallbackQueue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CallbackQueue.swift; path = Sources/Utility/CallbackQueue.swift; sourceTree = ""; }; + 4193FEA28CDEB3710C7C41537C8D5E2E /* TVMonogramView+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "TVMonogramView+Kingfisher.swift"; path = "Sources/Extensions/TVMonogramView+Kingfisher.swift"; sourceTree = ""; }; + 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + 44A117735F022F30BB9DC641136E51C9 /* CachedResponseHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CachedResponseHandler.swift; path = Source/CachedResponseHandler.swift; sourceTree = ""; }; + 44A856DC5499608A2401CFC01FE42E7C /* Pods-NewsApp-NewsAppUITests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-NewsApp-NewsAppUITests-acknowledgements.markdown"; sourceTree = ""; }; + 46BED43B2B37C76713033EC2A2529FD1 /* Pods-NewsAppTests-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-NewsAppTests-Info.plist"; sourceTree = ""; }; + 470E14DB7DAE8D7C7BE98FA70AFD5AB6 /* Pods-NewsApp-NewsAppUITests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-NewsApp-NewsAppUITests-acknowledgements.plist"; sourceTree = ""; }; + 48BAA15044E6AD772F77A856FA5107F2 /* DispatchQueue+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "DispatchQueue+Alamofire.swift"; path = "Source/DispatchQueue+Alamofire.swift"; sourceTree = ""; }; + 496EC73E3893F724BA57D993B154BF49 /* Source.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Source.swift; path = Sources/General/ImageSource/Source.swift; sourceTree = ""; }; + 499955C35F6D4A8F96E940FF129FF498 /* RedirectHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedirectHandler.swift; path = Source/RedirectHandler.swift; sourceTree = ""; }; + 49E7E2847E30735BA63D9F1E6149D704 /* Pods-NewsAppTests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-NewsAppTests-acknowledgements.plist"; sourceTree = ""; }; + 4A66915525E199A205AB96CEAA6F6F8B /* Pods-NewsApp-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-NewsApp-dummy.m"; sourceTree = ""; }; + 4C1FA3BADCA4576957512EC8D60D2B76 /* ImageTransition.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageTransition.swift; path = Sources/Image/ImageTransition.swift; sourceTree = ""; }; + 4C9A11397034BC6E9818A86ABE37D261 /* Pods-NewsAppTests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-NewsAppTests-umbrella.h"; sourceTree = ""; }; + 4EF4F904B2EF1F1E836FADDE8C6B05DF /* GIFAnimatedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GIFAnimatedImage.swift; path = Sources/Image/GIFAnimatedImage.swift; sourceTree = ""; }; + 52554E1C9731D5352FDE9E63F8C5466B /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Accelerate.framework; sourceTree = DEVELOPER_DIR; }; + 56B4F426A74516AD9DF8A17EA00CE61E /* Alamofire.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Alamofire.release.xcconfig; sourceTree = ""; }; + 5723DEFD772E79C36C19B268F7643CE0 /* Indicator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Indicator.swift; path = Sources/Views/Indicator.swift; sourceTree = ""; }; + 597967B6CDA2E72751463CF56198E927 /* ImageDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDataProvider.swift; path = Sources/General/ImageSource/ImageDataProvider.swift; sourceTree = ""; }; + 5B56F1736D1127A32614C8CBBEF3E5F6 /* HTTPMethod.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HTTPMethod.swift; path = Source/HTTPMethod.swift; sourceTree = ""; }; + 5D797E9A5C5782CE845840781FA1CC81 /* Alamofire */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Alamofire; path = Alamofire.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 630CD17355E726BB7AF8B6C6A581C2FE /* Alamofire.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Alamofire.debug.xcconfig; sourceTree = ""; }; + 6566364BE13E2E93E56CAB5BA17B3F15 /* Pods-NewsApp-NewsAppUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-NewsApp-NewsAppUITests.release.xcconfig"; sourceTree = ""; }; + 6599B0D2C1F4B263D4D859853F0F348A /* RequestInterceptor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestInterceptor.swift; path = Source/RequestInterceptor.swift; sourceTree = ""; }; + 660CF05D9FA7E4371D6E6A45ED8EE01F /* KF.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KF.swift; path = Sources/General/KF.swift; sourceTree = ""; }; + 67030433EFF4B0333EC8811BC7EC47D6 /* Validation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Validation.swift; path = Source/Validation.swift; sourceTree = ""; }; + 6835FBF931D5C31EFFF83ED559436CDE /* Kingfisher-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Kingfisher-dummy.m"; sourceTree = ""; }; + 6A723238162323CE3B380FA8E894C865 /* Filter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Filter.swift; path = Sources/Image/Filter.swift; sourceTree = ""; }; + 6BCDFE9C10374511CCC375D2E99C812E /* Storage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Storage.swift; path = Sources/Cache/Storage.swift; sourceTree = ""; }; + 6C321B1DFC61DFB5E1EB0BF63C4E7B05 /* Kingfisher-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Kingfisher-umbrella.h"; sourceTree = ""; }; + 6E5C70357A0F35A05639673633F51624 /* Kingfisher.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Kingfisher.debug.xcconfig; sourceTree = ""; }; + 7006AE0546B679604D685DCA785C542C /* Result.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Result.swift; path = Sources/Utility/Result.swift; sourceTree = ""; }; + 715993D57B4975C3CF94C6BC997595C6 /* RetryPolicy.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RetryPolicy.swift; path = Source/RetryPolicy.swift; sourceTree = ""; }; + 72447455A0B451F260AE94E750E590D7 /* AlamofireExtended.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AlamofireExtended.swift; path = Source/AlamofireExtended.swift; sourceTree = ""; }; + 7366FB0DEF319ED32BAF9DE5654C8DB7 /* ImageProgressive.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProgressive.swift; path = Sources/Image/ImageProgressive.swift; sourceTree = ""; }; + 73DC4358317E61D22885E415D01F8DFF /* Kingfisher.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Kingfisher.modulemap; sourceTree = ""; }; + 76CC7FAE70D848424E201CB9C76CABD3 /* Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Alamofire.swift; path = Source/Alamofire.swift; sourceTree = ""; }; + 7933A5F1F8BD20A48039AA759ADB465E /* ImageDrawing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDrawing.swift; path = Sources/Image/ImageDrawing.swift; sourceTree = ""; }; + 7B7AD2807A19FC11299041D8A8777267 /* KingfisherManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherManager.swift; path = Sources/General/KingfisherManager.swift; sourceTree = ""; }; + 7C6C8F94FCB9064F02B48B607AB69962 /* Placeholder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Placeholder.swift; path = Sources/Image/Placeholder.swift; sourceTree = ""; }; + 7EE41735C363063D401BC89633A455BE /* MultipartFormData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultipartFormData.swift; path = Source/MultipartFormData.swift; sourceTree = ""; }; + 80270ADB4ECE74E17A7DB57236035920 /* AFError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AFError.swift; path = Source/AFError.swift; sourceTree = ""; }; + 84F3F764D390DCA958D22088CB0BFD4C /* Pods-NewsApp.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-NewsApp.modulemap"; sourceTree = ""; }; + 84FCBF4EB7A598BBD98B0461C5566C7C /* Pods-NewsApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-NewsApp.release.xcconfig"; sourceTree = ""; }; + 8676B059D0C43C5A2FC81E0F55AD63D7 /* FormatIndicatedCacheSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FormatIndicatedCacheSerializer.swift; path = Sources/Cache/FormatIndicatedCacheSerializer.swift; sourceTree = ""; }; + 86DDDE6D887ED6DFE35529D771930C65 /* KFAnimatedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFAnimatedImage.swift; path = Sources/SwiftUI/KFAnimatedImage.swift; sourceTree = ""; }; + 872D7EFA572ECEE8EF993C27196E16DD /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; }; + 8753ADE1B95DA6998495A947D1C6CAB8 /* Pods-NewsApp-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-NewsApp-umbrella.h"; sourceTree = ""; }; + 8AE9B6094B04E75ACDBB73C3A194D690 /* ImageCache.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = Sources/Cache/ImageCache.swift; sourceTree = ""; }; + 8BA6A6C6AEAC43638A2B248BBEA90EBD /* MultipartUpload.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultipartUpload.swift; path = Source/MultipartUpload.swift; sourceTree = ""; }; + 8DCBD10175E2FB0BB51786C294395B93 /* Pods-NewsApp-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-NewsApp-acknowledgements.markdown"; sourceTree = ""; }; + 91B992A3950C83A53E3762E8C537D5E1 /* Pods-NewsAppTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-NewsAppTests.debug.xcconfig"; sourceTree = ""; }; + 932C67B305CF8C8942FEBD2375BE2E1F /* EventMonitor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = EventMonitor.swift; path = Source/EventMonitor.swift; sourceTree = ""; }; + 9566B136FA420BDAAE8B272AE10CFE3E /* KFImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImage.swift; path = Sources/SwiftUI/KFImage.swift; sourceTree = ""; }; + 95BFD9705B9A22E25BBBD0A608C90CCC /* Resource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Resource.swift; path = Sources/General/ImageSource/Resource.swift; sourceTree = ""; }; + 985C3C905A92749F5FB0325692FBF789 /* ImageView+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "ImageView+Kingfisher.swift"; path = "Sources/Extensions/ImageView+Kingfisher.swift"; sourceTree = ""; }; + 9958850B94FD8741B805BA1B4D017F67 /* Pods-NewsAppTests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-NewsAppTests-acknowledgements.markdown"; sourceTree = ""; }; + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 9DE61855BE4753EF235A677A7890CE6E /* Pods-NewsApp-NewsAppUITests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-NewsApp-NewsAppUITests.modulemap"; sourceTree = ""; }; + A043F9EF1DF150712ECDD2FB7CD2E46B /* NetworkReachabilityManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkReachabilityManager.swift; path = Source/NetworkReachabilityManager.swift; sourceTree = ""; }; + A0DD802295003561B6C5741521D716BC /* RequestTaskMap.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestTaskMap.swift; path = Source/RequestTaskMap.swift; sourceTree = ""; }; + A1939B0AEE888F2EB258D6832523F67C /* Session.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Session.swift; path = Source/Session.swift; sourceTree = ""; }; + A20EDDB802D0AEBA579A2FAC5B1B6BDE /* OperationQueue+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "OperationQueue+Alamofire.swift"; path = "Source/OperationQueue+Alamofire.swift"; sourceTree = ""; }; + A5FA97D693388252B940C1E1D23434D6 /* Pods-NewsApp-NewsAppUITests */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-NewsApp-NewsAppUITests"; path = Pods_NewsApp_NewsAppUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AC2601AB15F49167B95C5DDC2FBC7B78 /* MemoryStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MemoryStorage.swift; path = Sources/Cache/MemoryStorage.swift; sourceTree = ""; }; + AC42B34F4BEA8B3606A4D9B8CA9E63FB /* Pods-NewsApp-NewsAppUITests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-NewsApp-NewsAppUITests-umbrella.h"; sourceTree = ""; }; + AFFE766A6E0F3A621DFD9487DA3CAF7E /* Protected.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Protected.swift; path = Source/Protected.swift; sourceTree = ""; }; + B0BAB418BAD6B4B421F289A06BD5909C /* Alamofire-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Alamofire-Info.plist"; sourceTree = ""; }; + B114DBD20B001B2A9D93F8F8C47903B6 /* Combine.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Combine.swift; path = Source/Combine.swift; sourceTree = ""; }; + B42FDB98681E0F2B54A53C6263FC0FB7 /* SessionDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDelegate.swift; path = Source/SessionDelegate.swift; sourceTree = ""; }; + B7407E80368EF1CE2A091B4F0F8F81FF /* URLSessionConfiguration+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URLSessionConfiguration+Alamofire.swift"; path = "Source/URLSessionConfiguration+Alamofire.swift"; sourceTree = ""; }; + B74729EFA464A2B107050ED713601AB8 /* AVAssetImageDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AVAssetImageDataProvider.swift; path = Sources/General/ImageSource/AVAssetImageDataProvider.swift; sourceTree = ""; }; + BA9B50C2ED49A8E73A4D64C205173820 /* UIButton+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIButton+Kingfisher.swift"; path = "Sources/Extensions/UIButton+Kingfisher.swift"; sourceTree = ""; }; + BB099A6B514875F22EF98798B56B4BA5 /* Response.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Response.swift; path = Source/Response.swift; sourceTree = ""; }; + BB31688140758856323E0B3187769487 /* URLRequest+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URLRequest+Alamofire.swift"; path = "Source/URLRequest+Alamofire.swift"; sourceTree = ""; }; + BBD00D878FCCB38F1C26FDC274149C65 /* Pods-NewsApp-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-NewsApp-acknowledgements.plist"; sourceTree = ""; }; + C00B4E3033B80A2128A5B9128FF1B260 /* Pods-NewsAppTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-NewsAppTests.release.xcconfig"; sourceTree = ""; }; + C264879E6B16EA9E07F073E24220A232 /* RequestModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestModifier.swift; path = Sources/Networking/RequestModifier.swift; sourceTree = ""; }; + C26990E9D251C57CAB34BC5C9EC37033 /* KFImageRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageRenderer.swift; path = Sources/SwiftUI/KFImageRenderer.swift; sourceTree = ""; }; + C32A86B99F0C8B9E42B3D9C7F9730889 /* ResponseSerialization.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ResponseSerialization.swift; path = Source/ResponseSerialization.swift; sourceTree = ""; }; + C3CD3E23CCF124B01CA690E27E72D1B9 /* KingfisherError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherError.swift; path = Sources/General/KingfisherError.swift; sourceTree = ""; }; + C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Kingfisher; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C6E93449DD3CC778DD5D443C7780D895 /* NSButton+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSButton+Kingfisher.swift"; path = "Sources/Extensions/NSButton+Kingfisher.swift"; sourceTree = ""; }; + C8F034BD260F6DA78A6C46D3298BDF43 /* Pods-NewsAppTests */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-NewsAppTests"; path = Pods_NewsAppTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C95F29F7BBB1AA54AD679386E26C7740 /* ImageProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProcessor.swift; path = Sources/Image/ImageProcessor.swift; sourceTree = ""; }; + CE9ECAD406B234DD522B04457BCADA54 /* SessionDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDelegate.swift; path = Sources/Networking/SessionDelegate.swift; sourceTree = ""; }; + D03523C8957B09097CA8D933BF03E5EE /* Pods-NewsAppTests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-NewsAppTests-dummy.m"; sourceTree = ""; }; + D362E01704CD82D45E9C7857FB20AC1E /* DiskStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DiskStorage.swift; path = Sources/Cache/DiskStorage.swift; sourceTree = ""; }; + D370E0D48C1925AC14D155BEDF3DC7D4 /* GraphicsContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GraphicsContext.swift; path = Sources/Image/GraphicsContext.swift; sourceTree = ""; }; + D8CBA959C728F5B6931C11DE61853D88 /* SizeExtensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SizeExtensions.swift; path = Sources/Utility/SizeExtensions.swift; sourceTree = ""; }; + D942B0884573FD857CFB17A94E6A5807 /* StringEncoding+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "StringEncoding+Alamofire.swift"; path = "Source/StringEncoding+Alamofire.swift"; sourceTree = ""; }; + DAE0A9D42161F3A6512AEE4A7F63B4A0 /* ImageBinder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageBinder.swift; path = Sources/SwiftUI/ImageBinder.swift; sourceTree = ""; }; + DB1FAB3233CC80F28F7E66DF4DC9F2FA /* HTTPHeaders.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HTTPHeaders.swift; path = Source/HTTPHeaders.swift; sourceTree = ""; }; + E5C029CD6D6115372FB51461CE517110 /* Pods-NewsApp-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-NewsApp-frameworks.sh"; sourceTree = ""; }; + E640F25A4959F11A2B05BD5612BF5BC9 /* Pods-NewsApp-NewsAppUITests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-NewsApp-NewsAppUITests-dummy.m"; sourceTree = ""; }; + E79C064D85E6621CBFB4B667ADF4915C /* Pods-NewsApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-NewsApp.debug.xcconfig"; sourceTree = ""; }; + E7A8F9A880D8CF92488BF36640F2DBBE /* Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Kingfisher.swift; path = Sources/General/Kingfisher.swift; sourceTree = ""; }; + EDEF84AAA5BEDA67174B11EECD29C8BD /* Kingfisher.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Kingfisher.release.xcconfig; sourceTree = ""; }; + EEE76C5003FE080CF8820CA18C434EA3 /* ImageDownloader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDownloader.swift; path = Sources/Networking/ImageDownloader.swift; sourceTree = ""; }; + F1B7F2B2CCF604EF10790364BAB06391 /* AuthenticationInterceptor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthenticationInterceptor.swift; path = Source/AuthenticationInterceptor.swift; sourceTree = ""; }; + F6A1EDFD24518B3F930192EDAE086842 /* ParameterEncoder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ParameterEncoder.swift; path = Source/ParameterEncoder.swift; sourceTree = ""; }; + FAB10145B8616970B7B29BFF058CF910 /* ImageContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageContext.swift; path = Sources/SwiftUI/ImageContext.swift; sourceTree = ""; }; + FB0BD0DA3BA3BF98EFC7BF4305A340BA /* Alamofire.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Alamofire.modulemap; sourceTree = ""; }; + FC710AFB6066D8535108054FD1115E94 /* Notifications.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Notifications.swift; path = Source/Notifications.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 03906A121F0D694286913186D2F8880E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B28908D035557EB037085CBD6131F37 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 15DC142A7EE833251AA37FC8E2B8E01F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B068137A8925891446203B5D3D6A4ED /* CFNetwork.framework in Frameworks */, + 0F4037DBF307AC8058BD0A3D35C7E7E9 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4A5425AC96D6CDFB26665FCA97A95A49 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1C6C582579ADF5257B3AF3184D1CD98D /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61E022E1087493AE9960ED2848202518 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A0FEB7407D69B9499D00B6E664D6131 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 81DB1665E1495609510BA493822E5A85 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E719A3B025B9DACE693130120BD9B927 /* Accelerate.framework in Frameworks */, + 420C200A05BB29E1D299D1BADE9139D2 /* CFNetwork.framework in Frameworks */, + 3AD5DBB915C2623991F7DBACD173BBB4 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 03C5C200A0787E300053CFA8F53CA094 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5EE21CBB8EFB615AB450D209E054FAE6 /* iOS */, + ); + name = Frameworks; + sourceTree = ""; + }; + 0F4AF1A3447E9E41BB475E0A5B868464 /* Kingfisher */ = { + isa = PBXGroup; + children = ( + 06442349E67A8F1944EBFB63B46B406E /* AnimatedImageView.swift */, + 21DBC7F03013C95C65EE295C4DAC2A64 /* AuthenticationChallengeResponsable.swift */, + B74729EFA464A2B107050ED713601AB8 /* AVAssetImageDataProvider.swift */, + 0B7F4B3A6C95F8600D69F851451D63F6 /* Box.swift */, + 0BC3A37B1FA2F4841F5E8CC3FC102A19 /* CacheSerializer.swift */, + 40CCE8AF433C89864A9FE3C1F9C34A96 /* CallbackQueue.swift */, + 142C2CAF3091DF534D3E7B4AAA4DFC0B /* CPListItem+Kingfisher.swift */, + 0B347AF8E0B52CE83F1B9A8F51F1AE31 /* Delegate.swift */, + D362E01704CD82D45E9C7857FB20AC1E /* DiskStorage.swift */, + 365099CC49560F0DAFBF90D1A7FBFD24 /* ExtensionHelpers.swift */, + 6A723238162323CE3B380FA8E894C865 /* Filter.swift */, + 8676B059D0C43C5A2FC81E0F55AD63D7 /* FormatIndicatedCacheSerializer.swift */, + 4EF4F904B2EF1F1E836FADDE8C6B05DF /* GIFAnimatedImage.swift */, + D370E0D48C1925AC14D155BEDF3DC7D4 /* GraphicsContext.swift */, + 16E37EF24D9E916BB47E43D8E954D3AF /* Image.swift */, + DAE0A9D42161F3A6512AEE4A7F63B4A0 /* ImageBinder.swift */, + 8AE9B6094B04E75ACDBB73C3A194D690 /* ImageCache.swift */, + FAB10145B8616970B7B29BFF058CF910 /* ImageContext.swift */, + 37D911DA9899C96EA4408220F4754219 /* ImageDataProcessor.swift */, + 597967B6CDA2E72751463CF56198E927 /* ImageDataProvider.swift */, + EEE76C5003FE080CF8820CA18C434EA3 /* ImageDownloader.swift */, + 077E33C20C9BD4FAF66AE7A6EC1657F4 /* ImageDownloaderDelegate.swift */, + 7933A5F1F8BD20A48039AA759ADB465E /* ImageDrawing.swift */, + 3C65DBD1C381FE1E4AA74F824E85F325 /* ImageFormat.swift */, + 07524CF920EFBC25C9A0C9B3E57AA118 /* ImageModifier.swift */, + 240168000C008585397987F341FC9AFA /* ImagePrefetcher.swift */, + C95F29F7BBB1AA54AD679386E26C7740 /* ImageProcessor.swift */, + 7366FB0DEF319ED32BAF9DE5654C8DB7 /* ImageProgressive.swift */, + 4C1FA3BADCA4576957512EC8D60D2B76 /* ImageTransition.swift */, + 985C3C905A92749F5FB0325692FBF789 /* ImageView+Kingfisher.swift */, + 5723DEFD772E79C36C19B268F7643CE0 /* Indicator.swift */, + 660CF05D9FA7E4371D6E6A45ED8EE01F /* KF.swift */, + 86DDDE6D887ED6DFE35529D771930C65 /* KFAnimatedImage.swift */, + 9566B136FA420BDAAE8B272AE10CFE3E /* KFImage.swift */, + 2BAF59EA7A7DD980421989D2A205E6B0 /* KFImageOptions.swift */, + 292F77B8C48397E58C30BC7A313423E4 /* KFImageProtocol.swift */, + C26990E9D251C57CAB34BC5C9EC37033 /* KFImageRenderer.swift */, + 011604690A5B598427758F666A2F13C8 /* KFOptionsSetter.swift */, + E7A8F9A880D8CF92488BF36640F2DBBE /* Kingfisher.swift */, + C3CD3E23CCF124B01CA690E27E72D1B9 /* KingfisherError.swift */, + 7B7AD2807A19FC11299041D8A8777267 /* KingfisherManager.swift */, + 1B8B2E44706F2E23444FC30F3BAEE3DC /* KingfisherOptionsInfo.swift */, + AC2601AB15F49167B95C5DDC2FBC7B78 /* MemoryStorage.swift */, + C6E93449DD3CC778DD5D443C7780D895 /* NSButton+Kingfisher.swift */, + 1EBE61AE143E2DB816492EB19AA1651F /* NSTextAttachment+Kingfisher.swift */, + 7C6C8F94FCB9064F02B48B607AB69962 /* Placeholder.swift */, + 1C1BAC7555E3D0AC24A397BB028A3CEC /* RedirectHandler.swift */, + C264879E6B16EA9E07F073E24220A232 /* RequestModifier.swift */, + 95BFD9705B9A22E25BBBD0A608C90CCC /* Resource.swift */, + 7006AE0546B679604D685DCA785C542C /* Result.swift */, + 27F40A9A287AE4680FD4A041AAD0C5ED /* RetryStrategy.swift */, + 1CB85CAAF3329112CD2FFF9889E98FF7 /* Runtime.swift */, + 0C323A988742445270C1D6A4DD40ED07 /* SessionDataTask.swift */, + CE9ECAD406B234DD522B04457BCADA54 /* SessionDelegate.swift */, + D8CBA959C728F5B6931C11DE61853D88 /* SizeExtensions.swift */, + 496EC73E3893F724BA57D993B154BF49 /* Source.swift */, + 6BCDFE9C10374511CCC375D2E99C812E /* Storage.swift */, + 23963C2629645E475AB2931F62E451C7 /* String+MD5.swift */, + 4193FEA28CDEB3710C7C41537C8D5E2E /* TVMonogramView+Kingfisher.swift */, + BA9B50C2ED49A8E73A4D64C205173820 /* UIButton+Kingfisher.swift */, + 1C444AA66DFF06EB1D1D714FE5939A17 /* WKInterfaceImage+Kingfisher.swift */, + 729C05219705FA5F82BEFFE650B8F5E9 /* Support Files */, + ); + name = Kingfisher; + path = Kingfisher; + sourceTree = ""; + }; + 1E75F81CC79CF69CB4EA4C194E904C70 /* Pods */ = { + isa = PBXGroup; + children = ( + 78C3F1E5DD9988656EC031F46E619FBD /* Alamofire */, + 0F4AF1A3447E9E41BB475E0A5B868464 /* Kingfisher */, + ); + name = Pods; + sourceTree = ""; + }; + 5EE21CBB8EFB615AB450D209E054FAE6 /* iOS */ = { + isa = PBXGroup; + children = ( + 52554E1C9731D5352FDE9E63F8C5466B /* Accelerate.framework */, + 872D7EFA572ECEE8EF993C27196E16DD /* CFNetwork.framework */, + 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; + 6F1D967767CECA40AC890F7DC03EA2B8 /* Support Files */ = { + isa = PBXGroup; + children = ( + FB0BD0DA3BA3BF98EFC7BF4305A340BA /* Alamofire.modulemap */, + 3D13E09327BDC573DE5AAFE079172AB9 /* Alamofire-dummy.m */, + B0BAB418BAD6B4B421F289A06BD5909C /* Alamofire-Info.plist */, + 24A6C9EC04946183C998CEC9FFF72EFD /* Alamofire-prefix.pch */, + 0D167A016457E1A5BD5B9A73C4FF9434 /* Alamofire-umbrella.h */, + 630CD17355E726BB7AF8B6C6A581C2FE /* Alamofire.debug.xcconfig */, + 56B4F426A74516AD9DF8A17EA00CE61E /* Alamofire.release.xcconfig */, + ); + name = "Support Files"; + path = "../Target Support Files/Alamofire"; + sourceTree = ""; + }; + 729C05219705FA5F82BEFFE650B8F5E9 /* Support Files */ = { + isa = PBXGroup; + children = ( + 73DC4358317E61D22885E415D01F8DFF /* Kingfisher.modulemap */, + 6835FBF931D5C31EFFF83ED559436CDE /* Kingfisher-dummy.m */, + 1986AFB6D3CAD62304F3FA85BE29D575 /* Kingfisher-Info.plist */, + 148FA9BD1508B6E7E220C9D9BF1928F8 /* Kingfisher-prefix.pch */, + 6C321B1DFC61DFB5E1EB0BF63C4E7B05 /* Kingfisher-umbrella.h */, + 6E5C70357A0F35A05639673633F51624 /* Kingfisher.debug.xcconfig */, + EDEF84AAA5BEDA67174B11EECD29C8BD /* Kingfisher.release.xcconfig */, + ); + name = "Support Files"; + path = "../Target Support Files/Kingfisher"; + sourceTree = ""; + }; + 78C3F1E5DD9988656EC031F46E619FBD /* Alamofire */ = { + isa = PBXGroup; + children = ( + 80270ADB4ECE74E17A7DB57236035920 /* AFError.swift */, + 76CC7FAE70D848424E201CB9C76CABD3 /* Alamofire.swift */, + 72447455A0B451F260AE94E750E590D7 /* AlamofireExtended.swift */, + F1B7F2B2CCF604EF10790364BAB06391 /* AuthenticationInterceptor.swift */, + 44A117735F022F30BB9DC641136E51C9 /* CachedResponseHandler.swift */, + B114DBD20B001B2A9D93F8F8C47903B6 /* Combine.swift */, + 1529C2E8B334710FA50E4C9A7153006A /* Concurrency.swift */, + 48BAA15044E6AD772F77A856FA5107F2 /* DispatchQueue+Alamofire.swift */, + 932C67B305CF8C8942FEBD2375BE2E1F /* EventMonitor.swift */, + DB1FAB3233CC80F28F7E66DF4DC9F2FA /* HTTPHeaders.swift */, + 5B56F1736D1127A32614C8CBBEF3E5F6 /* HTTPMethod.swift */, + 7EE41735C363063D401BC89633A455BE /* MultipartFormData.swift */, + 8BA6A6C6AEAC43638A2B248BBEA90EBD /* MultipartUpload.swift */, + A043F9EF1DF150712ECDD2FB7CD2E46B /* NetworkReachabilityManager.swift */, + FC710AFB6066D8535108054FD1115E94 /* Notifications.swift */, + A20EDDB802D0AEBA579A2FAC5B1B6BDE /* OperationQueue+Alamofire.swift */, + F6A1EDFD24518B3F930192EDAE086842 /* ParameterEncoder.swift */, + 249A902C9828CB270329EFFAFAC1843A /* ParameterEncoding.swift */, + AFFE766A6E0F3A621DFD9487DA3CAF7E /* Protected.swift */, + 499955C35F6D4A8F96E940FF129FF498 /* RedirectHandler.swift */, + 0341B04B64485596391C33DD9360D74E /* Request.swift */, + 6599B0D2C1F4B263D4D859853F0F348A /* RequestInterceptor.swift */, + A0DD802295003561B6C5741521D716BC /* RequestTaskMap.swift */, + BB099A6B514875F22EF98798B56B4BA5 /* Response.swift */, + C32A86B99F0C8B9E42B3D9C7F9730889 /* ResponseSerialization.swift */, + 2F3F986AF94568CEEB8AF631C72F03B2 /* Result+Alamofire.swift */, + 715993D57B4975C3CF94C6BC997595C6 /* RetryPolicy.swift */, + 04303942CB47C4C340CF398F8653BB98 /* ServerTrustEvaluation.swift */, + A1939B0AEE888F2EB258D6832523F67C /* Session.swift */, + B42FDB98681E0F2B54A53C6263FC0FB7 /* SessionDelegate.swift */, + D942B0884573FD857CFB17A94E6A5807 /* StringEncoding+Alamofire.swift */, + 404743B9B61FE5357078AFD049FC6B45 /* URLConvertible+URLRequestConvertible.swift */, + 072684B1154141462994E3AD2ACD3606 /* URLEncodedFormEncoder.swift */, + BB31688140758856323E0B3187769487 /* URLRequest+Alamofire.swift */, + B7407E80368EF1CE2A091B4F0F8F81FF /* URLSessionConfiguration+Alamofire.swift */, + 67030433EFF4B0333EC8811BC7EC47D6 /* Validation.swift */, + 6F1D967767CECA40AC890F7DC03EA2B8 /* Support Files */, + ); + name = Alamofire; + path = Alamofire; + sourceTree = ""; + }; + A464889D729624CA67F61A175A5D22D0 /* Products */ = { + isa = PBXGroup; + children = ( + 5D797E9A5C5782CE845840781FA1CC81 /* Alamofire */, + C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */, + 39E3E5F872763E10C58E7DEF2B8ED837 /* Pods-NewsApp */, + A5FA97D693388252B940C1E1D23434D6 /* Pods-NewsApp-NewsAppUITests */, + C8F034BD260F6DA78A6C46D3298BDF43 /* Pods-NewsAppTests */, + ); + name = Products; + sourceTree = ""; + }; + A870770456403D0615137E2308EC49A0 /* Targets Support Files */ = { + isa = PBXGroup; + children = ( + FC2102BFF4A17AAAA9D5E1D095057E7F /* Pods-NewsApp */, + AD26F883AA5D856CE0992AC15E28FC59 /* Pods-NewsApp-NewsAppUITests */, + B1A4754D391A54C496EFDA29883F93D4 /* Pods-NewsAppTests */, + ); + name = "Targets Support Files"; + sourceTree = ""; + }; + AD26F883AA5D856CE0992AC15E28FC59 /* Pods-NewsApp-NewsAppUITests */ = { + isa = PBXGroup; + children = ( + 9DE61855BE4753EF235A677A7890CE6E /* Pods-NewsApp-NewsAppUITests.modulemap */, + 44A856DC5499608A2401CFC01FE42E7C /* Pods-NewsApp-NewsAppUITests-acknowledgements.markdown */, + 470E14DB7DAE8D7C7BE98FA70AFD5AB6 /* Pods-NewsApp-NewsAppUITests-acknowledgements.plist */, + E640F25A4959F11A2B05BD5612BF5BC9 /* Pods-NewsApp-NewsAppUITests-dummy.m */, + 09496892A4F123263B81DEDFA7F99078 /* Pods-NewsApp-NewsAppUITests-frameworks.sh */, + 1D1A7C603C2A16508B0260C5D7CEEBC4 /* Pods-NewsApp-NewsAppUITests-Info.plist */, + AC42B34F4BEA8B3606A4D9B8CA9E63FB /* Pods-NewsApp-NewsAppUITests-umbrella.h */, + 031F7B599607B10186672B7EB854655B /* Pods-NewsApp-NewsAppUITests.debug.xcconfig */, + 6566364BE13E2E93E56CAB5BA17B3F15 /* Pods-NewsApp-NewsAppUITests.release.xcconfig */, + ); + name = "Pods-NewsApp-NewsAppUITests"; + path = "Target Support Files/Pods-NewsApp-NewsAppUITests"; + sourceTree = ""; + }; + B1A4754D391A54C496EFDA29883F93D4 /* Pods-NewsAppTests */ = { + isa = PBXGroup; + children = ( + 0232C9EF1ACF5315E3EAF12996CCBF4E /* Pods-NewsAppTests.modulemap */, + 9958850B94FD8741B805BA1B4D017F67 /* Pods-NewsAppTests-acknowledgements.markdown */, + 49E7E2847E30735BA63D9F1E6149D704 /* Pods-NewsAppTests-acknowledgements.plist */, + D03523C8957B09097CA8D933BF03E5EE /* Pods-NewsAppTests-dummy.m */, + 46BED43B2B37C76713033EC2A2529FD1 /* Pods-NewsAppTests-Info.plist */, + 4C9A11397034BC6E9818A86ABE37D261 /* Pods-NewsAppTests-umbrella.h */, + 91B992A3950C83A53E3762E8C537D5E1 /* Pods-NewsAppTests.debug.xcconfig */, + C00B4E3033B80A2128A5B9128FF1B260 /* Pods-NewsAppTests.release.xcconfig */, + ); + name = "Pods-NewsAppTests"; + path = "Target Support Files/Pods-NewsAppTests"; + sourceTree = ""; + }; + CF1408CF629C7361332E53B88F7BD30C = { + isa = PBXGroup; + children = ( + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, + 03C5C200A0787E300053CFA8F53CA094 /* Frameworks */, + 1E75F81CC79CF69CB4EA4C194E904C70 /* Pods */, + A464889D729624CA67F61A175A5D22D0 /* Products */, + A870770456403D0615137E2308EC49A0 /* Targets Support Files */, + ); + sourceTree = ""; + }; + FC2102BFF4A17AAAA9D5E1D095057E7F /* Pods-NewsApp */ = { + isa = PBXGroup; + children = ( + 84F3F764D390DCA958D22088CB0BFD4C /* Pods-NewsApp.modulemap */, + 8DCBD10175E2FB0BB51786C294395B93 /* Pods-NewsApp-acknowledgements.markdown */, + BBD00D878FCCB38F1C26FDC274149C65 /* Pods-NewsApp-acknowledgements.plist */, + 4A66915525E199A205AB96CEAA6F6F8B /* Pods-NewsApp-dummy.m */, + E5C029CD6D6115372FB51461CE517110 /* Pods-NewsApp-frameworks.sh */, + 0D0038510FD52FD645AC54E46EA78C31 /* Pods-NewsApp-Info.plist */, + 8753ADE1B95DA6998495A947D1C6CAB8 /* Pods-NewsApp-umbrella.h */, + E79C064D85E6621CBFB4B667ADF4915C /* Pods-NewsApp.debug.xcconfig */, + 84FCBF4EB7A598BBD98B0461C5566C7C /* Pods-NewsApp.release.xcconfig */, + ); + name = "Pods-NewsApp"; + path = "Target Support Files/Pods-NewsApp"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 2BFC0BFAA3D9E4C183D99690C76932C0 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + F1F7715C2158962AF8E13DA25F6BA69C /* Pods-NewsAppTests-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3F4C385AEA56862A8B026C46618C3CA8 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 8EAF77C22278D0A4A8D696BCBB64E7B8 /* Pods-NewsApp-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 52E6F7B26483BE3BC9393C6C05D32424 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 005B319B494ED2DAA239B9939A504DFC /* Alamofire-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7AE52B176E9872452FD890FC7F460CE1 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 122980E44B15C64CF0B14DC94D7EB5C9 /* Kingfisher-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D1BB012F698453FF83E496311D6DB304 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + AA44E3F1A54787E853CBCDA8F296061C /* Pods-NewsApp-NewsAppUITests-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 6B14A80A672BEFB65A571F51A375D8D1 /* Pods-NewsApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 09E2F4CA31DAD9C29F3135D9CA1C510B /* Build configuration list for PBXNativeTarget "Pods-NewsApp" */; + buildPhases = ( + 3F4C385AEA56862A8B026C46618C3CA8 /* Headers */, + 53B58AB8DA7F72D0FE460602613C85AC /* Sources */, + 03906A121F0D694286913186D2F8880E /* Frameworks */, + 0A97AEB2F97C4E5C5362F978DC8D254C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + A3553453518B4757B4BD7DAA2E7593BC /* PBXTargetDependency */, + B112C5233CF9A8806C40D7354A7D0329 /* PBXTargetDependency */, + ); + name = "Pods-NewsApp"; + productName = Pods_NewsApp; + productReference = 39E3E5F872763E10C58E7DEF2B8ED837 /* Pods-NewsApp */; + productType = "com.apple.product-type.framework"; + }; + 90FEA9A440C9F6B07E4A77EBDAD61359 /* Pods-NewsAppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 54D6CC13D6FC770DA5A4400048121C2A /* Build configuration list for PBXNativeTarget "Pods-NewsAppTests" */; + buildPhases = ( + 2BFC0BFAA3D9E4C183D99690C76932C0 /* Headers */, + 68A027455600357853F272B7E27A9CE5 /* Sources */, + 61E022E1087493AE9960ED2848202518 /* Frameworks */, + 998530CB76D5970B9DB6B688EC4C33AF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 08E256E5DB1571E0E449314C0D2BC3A6 /* PBXTargetDependency */, + ); + name = "Pods-NewsAppTests"; + productName = Pods_NewsAppTests; + productReference = C8F034BD260F6DA78A6C46D3298BDF43 /* Pods-NewsAppTests */; + productType = "com.apple.product-type.framework"; + }; + 9DC19C09158E11FDE08D82BB988208B3 /* Pods-NewsApp-NewsAppUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1BB6FDBC44B84CCE147492DE1D0494C0 /* Build configuration list for PBXNativeTarget "Pods-NewsApp-NewsAppUITests" */; + buildPhases = ( + D1BB012F698453FF83E496311D6DB304 /* Headers */, + 38224E4F362F2DDE6FDEE3259FDA50ED /* Sources */, + 4A5425AC96D6CDFB26665FCA97A95A49 /* Frameworks */, + C6A03E0A3623E2CF04E0D15A83EFA0BA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 66ED41C9789780DC58F5DCDE402385C1 /* PBXTargetDependency */, + 2F7A9340EDDFC5A7FD8EAA67BE298E71 /* PBXTargetDependency */, + ); + name = "Pods-NewsApp-NewsAppUITests"; + productName = Pods_NewsApp_NewsAppUITests; + productReference = A5FA97D693388252B940C1E1D23434D6 /* Pods-NewsApp-NewsAppUITests */; + productType = "com.apple.product-type.framework"; + }; + E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */ = { + isa = PBXNativeTarget; + buildConfigurationList = 69ACD8654734266A348C6FF68E734010 /* Build configuration list for PBXNativeTarget "Kingfisher" */; + buildPhases = ( + 7AE52B176E9872452FD890FC7F460CE1 /* Headers */, + 3C1DA515D615F8CE75565ACE14378882 /* Sources */, + 81DB1665E1495609510BA493822E5A85 /* Frameworks */, + CFEB3E9FD20A01120B40D65B82D8F26F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Kingfisher; + productName = Kingfisher; + productReference = C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */; + productType = "com.apple.product-type.framework"; + }; + EAAA1AD3A8A1B59AB91319EE40752C6D /* Alamofire */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8A212264186B8822192F9C369D7DE4BB /* Build configuration list for PBXNativeTarget "Alamofire" */; + buildPhases = ( + 52E6F7B26483BE3BC9393C6C05D32424 /* Headers */, + F5D2A45FBA06D86A537CB441D5BF4FF4 /* Sources */, + 15DC142A7EE833251AA37FC8E2B8E01F /* Frameworks */, + E9D4145FA41F60FFAB33A07796D9ED97 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Alamofire; + productName = Alamofire; + productReference = 5D797E9A5C5782CE845840781FA1CC81 /* Alamofire */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BFDFE7DC352907FC980B868725387E98 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1240; + LastUpgradeCheck = 1240; + }; + buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = CF1408CF629C7361332E53B88F7BD30C; + productRefGroup = A464889D729624CA67F61A175A5D22D0 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + EAAA1AD3A8A1B59AB91319EE40752C6D /* Alamofire */, + E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */, + 6B14A80A672BEFB65A571F51A375D8D1 /* Pods-NewsApp */, + 9DC19C09158E11FDE08D82BB988208B3 /* Pods-NewsApp-NewsAppUITests */, + 90FEA9A440C9F6B07E4A77EBDAD61359 /* Pods-NewsAppTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0A97AEB2F97C4E5C5362F978DC8D254C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 998530CB76D5970B9DB6B688EC4C33AF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C6A03E0A3623E2CF04E0D15A83EFA0BA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CFEB3E9FD20A01120B40D65B82D8F26F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E9D4145FA41F60FFAB33A07796D9ED97 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 38224E4F362F2DDE6FDEE3259FDA50ED /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D5E8B596B6BA50EBE333C08018168137 /* Pods-NewsApp-NewsAppUITests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3C1DA515D615F8CE75565ACE14378882 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1952485AFF7A1BCCA4D4B142E82FE627 /* AnimatedImageView.swift in Sources */, + 243D7CFE1D56ED80ACB2B3E71B4CB603 /* AuthenticationChallengeResponsable.swift in Sources */, + D603AA58EF97D461A57B2B1BCB883868 /* AVAssetImageDataProvider.swift in Sources */, + B25E07EA645911443A38DA1E68166156 /* Box.swift in Sources */, + CD7AC3E1C98EA54F7C05C36C52805220 /* CacheSerializer.swift in Sources */, + FF09824309346665E2F1F7F5A45FB10F /* CallbackQueue.swift in Sources */, + DBB8088E14A2ADEDB1CD840BAC835267 /* CPListItem+Kingfisher.swift in Sources */, + 457BE444ED617FA7D6851D6DAA9D7234 /* Delegate.swift in Sources */, + 1FEE89BF952BE7ACA46E642DA2E48CA2 /* DiskStorage.swift in Sources */, + 1185A2B40E14F2FCBC761FC99777CAD8 /* ExtensionHelpers.swift in Sources */, + 3AF7DB9AEFF47F1F7F91AF28440E4AC6 /* Filter.swift in Sources */, + DE532EF7D50A9CF68587DAD4C1A02BD7 /* FormatIndicatedCacheSerializer.swift in Sources */, + F24021BDE9B42D604E3341CAD8E34759 /* GIFAnimatedImage.swift in Sources */, + 582D59E0D2EF62E0575933C99B393704 /* GraphicsContext.swift in Sources */, + 70FEC06F54286257E1BA1ECA0C99198D /* Image.swift in Sources */, + 1AA89F327105C026976BF6E831B193A2 /* ImageBinder.swift in Sources */, + ED0C8BA7560D7324587B353E0960479F /* ImageCache.swift in Sources */, + F5414F8A5B40521D0E4AEEB28378CB49 /* ImageContext.swift in Sources */, + 80738D8956C9987CCCEDF551961E5069 /* ImageDataProcessor.swift in Sources */, + 0285857A24F66E925987A5876F0BE63B /* ImageDataProvider.swift in Sources */, + DD72DC30CF19FFC81AB19CD0B074000D /* ImageDownloader.swift in Sources */, + 881A35B28D93C56E46E305F6138B1A76 /* ImageDownloaderDelegate.swift in Sources */, + DAFC6CE6321395CF4523DD66DADBB9BA /* ImageDrawing.swift in Sources */, + E6D6C7D5E458A05CC736C340F853E9F6 /* ImageFormat.swift in Sources */, + F54DE563418B1783D6EC491A0C3A05DB /* ImageModifier.swift in Sources */, + 00BEA6029C428FEE644AC3D42AD83282 /* ImagePrefetcher.swift in Sources */, + EF9C4588CDA85AED8BBCF77451B2A35B /* ImageProcessor.swift in Sources */, + 5F852F38CBC282496CCBE37C51324B2F /* ImageProgressive.swift in Sources */, + 29FF13E23FD52E46D30530549410AD7C /* ImageTransition.swift in Sources */, + E5B664771063F1A9A372519A8466860B /* ImageView+Kingfisher.swift in Sources */, + DFCDE4638265B4CCD494ECA5D560DBEE /* Indicator.swift in Sources */, + DF4563832C19B8582C810BF502A5CA29 /* KF.swift in Sources */, + 7FFE4021A4F14124342AD41CE1117B3E /* KFAnimatedImage.swift in Sources */, + 5E27DD292D3A55657712DD7AFA7B8FCA /* KFImage.swift in Sources */, + 5ADB30DD9A03859018550A999ACB0652 /* KFImageOptions.swift in Sources */, + F9537B023E24AC4A724E301F7E372491 /* KFImageProtocol.swift in Sources */, + 0510E8EA51914CB2176AD0F173937FAB /* KFImageRenderer.swift in Sources */, + A316388A35648CB2987E761771456087 /* KFOptionsSetter.swift in Sources */, + A39D3555EC8B45B7D6B9505DDAF0F117 /* Kingfisher.swift in Sources */, + 4DCA9775E5CCF599460BDB46E77F6FA4 /* Kingfisher-dummy.m in Sources */, + 4F37E521D341C47CE73DDCF21BA95A52 /* KingfisherError.swift in Sources */, + 1FE693B5ACC6AD7320CEFC20B64546E4 /* KingfisherManager.swift in Sources */, + 05228565AAA7FCED4BAFB2B7EF71D53D /* KingfisherOptionsInfo.swift in Sources */, + 1FD2928BC156D990D68B105F518C60B6 /* MemoryStorage.swift in Sources */, + 059639E700DEFAEF08F56484E5F67BE7 /* NSButton+Kingfisher.swift in Sources */, + 59BC9047F4BEBBC06235608D974E230D /* NSTextAttachment+Kingfisher.swift in Sources */, + 506128E1CC424E40E2691546D9547549 /* Placeholder.swift in Sources */, + 2BE89C24BFD3FB663E37C607C289B3B6 /* RedirectHandler.swift in Sources */, + 509490FB1D30FEC59AE4BC21AEEBB7BB /* RequestModifier.swift in Sources */, + 22BD1346F66BFCB129AAA44EEF322AC9 /* Resource.swift in Sources */, + A88A844D5356E1690E445024CB796E09 /* Result.swift in Sources */, + 22216C300C763044344B9DBF97317E63 /* RetryStrategy.swift in Sources */, + EBB32304E8DD4BA115454E0050D47DED /* Runtime.swift in Sources */, + 7C7418FF01DD7BB909719682B634A8A5 /* SessionDataTask.swift in Sources */, + 0ED8FBFD9A86D21BF69137EC9350E575 /* SessionDelegate.swift in Sources */, + 57FC31B14C753B5C63CEF00560F8A6EF /* SizeExtensions.swift in Sources */, + 9B0A78AC22E7EDA755F51D86527E2D9C /* Source.swift in Sources */, + 25FC036BEA33CAB5D80F5A41644535D3 /* Storage.swift in Sources */, + BD382E78580D295D10100678D4F66A76 /* String+MD5.swift in Sources */, + F17B1F8F2B6580343025237455A29D61 /* TVMonogramView+Kingfisher.swift in Sources */, + 082EDC820D76DF95C71A5018112DE512 /* UIButton+Kingfisher.swift in Sources */, + 9F5FE22DA95B66B8DC21CB13BE25EC9B /* WKInterfaceImage+Kingfisher.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 53B58AB8DA7F72D0FE460602613C85AC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA4162FB68C4A59A55D9B8733D2225DC /* Pods-NewsApp-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 68A027455600357853F272B7E27A9CE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2969A24709C57F23CCDB8DB5000384EB /* Pods-NewsAppTests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F5D2A45FBA06D86A537CB441D5BF4FF4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0EA90FBF83350C49E6EF6C8A98D6F00 /* AFError.swift in Sources */, + F17A4CA4664CABB331D39FE902E06843 /* Alamofire.swift in Sources */, + 55AABB1FB38F61A3369ACC555FF3046D /* Alamofire-dummy.m in Sources */, + 1EE44196E7BCE57AD96A2C751651EF40 /* AlamofireExtended.swift in Sources */, + 7483E5327027263F7E4B94A2997191C4 /* AuthenticationInterceptor.swift in Sources */, + 2CBE3651CA006E19F5D64A2DE9B9A028 /* CachedResponseHandler.swift in Sources */, + 46A64A43AFA057B6B63C8F0C12F509B4 /* Combine.swift in Sources */, + 9C9030DEDB0DF955B16FE08C50892D57 /* Concurrency.swift in Sources */, + EEC150B66BCCD6C80FDA7E4D1975166B /* DispatchQueue+Alamofire.swift in Sources */, + CEBFFEED65D877702B2F36102528CF6D /* EventMonitor.swift in Sources */, + 7E02F5B62BE00E97847DF549FFED2490 /* HTTPHeaders.swift in Sources */, + D6B4751CED01D53E4A1B6A571AAA2F83 /* HTTPMethod.swift in Sources */, + 7FE695DA8EE7FF1286556E06B692009B /* MultipartFormData.swift in Sources */, + E9B4C89E7EB3B27D46AFCA452C3D426F /* MultipartUpload.swift in Sources */, + A29100AA1876DDEFF6F54694A51FDB0E /* NetworkReachabilityManager.swift in Sources */, + 2CCD13099063CD560E3067BD132914FA /* Notifications.swift in Sources */, + E54654D504A42C24F284A68F87F7671D /* OperationQueue+Alamofire.swift in Sources */, + 99D058E53EFEE3AC4857CDE3DBA5C004 /* ParameterEncoder.swift in Sources */, + 68FB2DCB4C77DBCAF9A6037E470F2BDE /* ParameterEncoding.swift in Sources */, + A53BDE589BDD6483F3EEDCE5EA1DCCD3 /* Protected.swift in Sources */, + 045DE6EBF9B2F63F60F5BE60C1198E06 /* RedirectHandler.swift in Sources */, + B3658C29BBDE1033F6269A92E612CB30 /* Request.swift in Sources */, + DD902FE8D6824681C929D028655AE121 /* RequestInterceptor.swift in Sources */, + DA34899BEF0668D76CBCE8C4CE47B97B /* RequestTaskMap.swift in Sources */, + 75966A9262648D4647D764E3E76BC6AC /* Response.swift in Sources */, + 824D816B1EE404F2DD400EE678695CBE /* ResponseSerialization.swift in Sources */, + 04A896288CE3A59B530250337A5F8362 /* Result+Alamofire.swift in Sources */, + 33A7D0F2D03004CE256A75E03DF33C70 /* RetryPolicy.swift in Sources */, + B704B198B9B520D449260877E300D821 /* ServerTrustEvaluation.swift in Sources */, + 81B8D2B7CEB25C2448B0BC9B33591A65 /* Session.swift in Sources */, + 1976BB7D7E26A12E29283E71154B63B3 /* SessionDelegate.swift in Sources */, + 7F1BB526AAE3ECDCE90127D9D0E10261 /* StringEncoding+Alamofire.swift in Sources */, + 8D75FC8D7476C9674234F39F1A820D8C /* URLConvertible+URLRequestConvertible.swift in Sources */, + 7930C94414B4C661867AC4FBE82E996C /* URLEncodedFormEncoder.swift in Sources */, + BC0ECA8F22DEDE8886E189CD0EAA1197 /* URLRequest+Alamofire.swift in Sources */, + 808C960C82D708FC1A42C581D6CB4940 /* URLSessionConfiguration+Alamofire.swift in Sources */, + 3C4059621E23842C19D4EB5D35B41989 /* Validation.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 08E256E5DB1571E0E449314C0D2BC3A6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "Pods-NewsApp"; + target = 6B14A80A672BEFB65A571F51A375D8D1 /* Pods-NewsApp */; + targetProxy = C5EB1635709E055474F1A5375A8B24FC /* PBXContainerItemProxy */; + }; + 2F7A9340EDDFC5A7FD8EAA67BE298E71 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Kingfisher; + target = E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */; + targetProxy = F4BABA04C3C7E4011126649748B6BC65 /* PBXContainerItemProxy */; + }; + 66ED41C9789780DC58F5DCDE402385C1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Alamofire; + target = EAAA1AD3A8A1B59AB91319EE40752C6D /* Alamofire */; + targetProxy = 9E094CE8AC863886AD47DDAF6238E99B /* PBXContainerItemProxy */; + }; + A3553453518B4757B4BD7DAA2E7593BC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Alamofire; + target = EAAA1AD3A8A1B59AB91319EE40752C6D /* Alamofire */; + targetProxy = AF0E1994F63FC3036B04577256FDD251 /* PBXContainerItemProxy */; + }; + B112C5233CF9A8806C40D7354A7D0329 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Kingfisher; + target = E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */; + targetProxy = 2EC17AF2A6A8BD0B29B98700AAE63556 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 07BA0761808D2CE0EAF95F6CFC70CB98 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 84FCBF4EB7A598BBD98B0461C5566C7C /* Pods-NewsApp.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-NewsApp/Pods-NewsApp-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-NewsApp/Pods-NewsApp.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 4214E6946993DF02ED98D89047C64016 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_DEBUG=1", + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Debug; + }; + 4C50BA37D9E48DC2A90A48C99CA5ACD8 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E79C064D85E6621CBFB4B667ADF4915C /* Pods-NewsApp.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-NewsApp/Pods-NewsApp-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-NewsApp/Pods-NewsApp.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 4F73C94406421CFC14E2D60CAD852C79 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C00B4E3033B80A2128A5B9128FF1B260 /* Pods-NewsAppTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 6B3BD3ECFB73D60D0387A53C1741F89D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6566364BE13E2E93E56CAB5BA17B3F15 /* Pods-NewsApp-NewsAppUITests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 7B416696EF061F4CB400BAFACC3DBC74 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 91B992A3950C83A53E3762E8C537D5E1 /* Pods-NewsAppTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 90A4588B06F8745E7FCD1B00204D6241 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 56B4F426A74516AD9DF8A17EA00CE61E /* Alamofire.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/Alamofire/Alamofire-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/Alamofire/Alamofire-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/Alamofire/Alamofire.modulemap"; + PRODUCT_MODULE_NAME = Alamofire; + PRODUCT_NAME = Alamofire; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.5; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 980A58862D8A5086E2825CF9017AC8DD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6E5C70357A0F35A05639673633F51624 /* Kingfisher.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/Kingfisher/Kingfisher-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/Kingfisher/Kingfisher-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/Kingfisher/Kingfisher.modulemap"; + PRODUCT_MODULE_NAME = Kingfisher; + PRODUCT_NAME = Kingfisher; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 9A2259EC1CCDB139366E47B5BEAEDE30 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 031F7B599607B10186672B7EB854655B /* Pods-NewsApp-NewsAppUITests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 9E98C04A5FA16D8AD5D48C1861179497 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 630CD17355E726BB7AF8B6C6A581C2FE /* Alamofire.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/Alamofire/Alamofire-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/Alamofire/Alamofire-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/Alamofire/Alamofire.modulemap"; + PRODUCT_MODULE_NAME = Alamofire; + PRODUCT_NAME = Alamofire; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.5; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + BFD9E4B58F44191AF73A3434AAF6831F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EDEF84AAA5BEDA67174B11EECD29C8BD /* Kingfisher.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/Kingfisher/Kingfisher-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/Kingfisher/Kingfisher-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/Kingfisher/Kingfisher.modulemap"; + PRODUCT_MODULE_NAME = Kingfisher; + PRODUCT_NAME = Kingfisher; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + CD85FD90473CFBE797E4264E7BBC70AF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_RELEASE=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 09E2F4CA31DAD9C29F3135D9CA1C510B /* Build configuration list for PBXNativeTarget "Pods-NewsApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4C50BA37D9E48DC2A90A48C99CA5ACD8 /* Debug */, + 07BA0761808D2CE0EAF95F6CFC70CB98 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1BB6FDBC44B84CCE147492DE1D0494C0 /* Build configuration list for PBXNativeTarget "Pods-NewsApp-NewsAppUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9A2259EC1CCDB139366E47B5BEAEDE30 /* Debug */, + 6B3BD3ECFB73D60D0387A53C1741F89D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4214E6946993DF02ED98D89047C64016 /* Debug */, + CD85FD90473CFBE797E4264E7BBC70AF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 54D6CC13D6FC770DA5A4400048121C2A /* Build configuration list for PBXNativeTarget "Pods-NewsAppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7B416696EF061F4CB400BAFACC3DBC74 /* Debug */, + 4F73C94406421CFC14E2D60CAD852C79 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 69ACD8654734266A348C6FF68E734010 /* Build configuration list for PBXNativeTarget "Kingfisher" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 980A58862D8A5086E2825CF9017AC8DD /* Debug */, + BFD9E4B58F44191AF73A3434AAF6831F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8A212264186B8822192F9C369D7DE4BB /* Build configuration list for PBXNativeTarget "Alamofire" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9E98C04A5FA16D8AD5D48C1861179497 /* Debug */, + 90A4588B06F8745E7FCD1B00204D6241 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BFDFE7DC352907FC980B868725387E98 /* Project object */; +} diff --git a/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Alamofire.xcscheme b/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Alamofire.xcscheme new file mode 100644 index 0000000..120775c --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Alamofire.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Kingfisher.xcscheme b/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Kingfisher.xcscheme new file mode 100644 index 0000000..0074170 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Kingfisher.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-NewsApp-NewsAppUITests.xcscheme b/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-NewsApp-NewsAppUITests.xcscheme new file mode 100644 index 0000000..e9c2153 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-NewsApp-NewsAppUITests.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-NewsApp.xcscheme b/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-NewsApp.xcscheme new file mode 100644 index 0000000..a8ae2cf --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-NewsApp.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-NewsAppTests.xcscheme b/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-NewsAppTests.xcscheme new file mode 100644 index 0000000..2eae1c1 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-NewsAppTests.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist b/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..01fbde6 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,46 @@ + + + + + SchemeUserState + + Alamofire.xcscheme + + isShown + + orderHint + 0 + + Kingfisher.xcscheme + + isShown + + orderHint + 1 + + Pods-NewsApp-NewsAppUITests.xcscheme + + isShown + + orderHint + 3 + + Pods-NewsApp.xcscheme + + isShown + + orderHint + 2 + + Pods-NewsAppTests.xcscheme + + isShown + + orderHint + 4 + + + SuppressBuildableAutocreation + + + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-Info.plist b/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-Info.plist new file mode 100644 index 0000000..a3f6596 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 5.6.1 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-dummy.m b/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-dummy.m new file mode 100644 index 0000000..a6c4594 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Alamofire : NSObject +@end +@implementation PodsDummy_Alamofire +@end diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-prefix.pch b/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-umbrella.h b/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-umbrella.h new file mode 100644 index 0000000..00014e3 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double AlamofireVersionNumber; +FOUNDATION_EXPORT const unsigned char AlamofireVersionString[]; + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire.debug.xcconfig b/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire.debug.xcconfig new file mode 100644 index 0000000..7d169c4 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire.debug.xcconfig @@ -0,0 +1,14 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Alamofire +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "CFNetwork" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/Alamofire +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire.modulemap b/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire.modulemap new file mode 100644 index 0000000..d1f125f --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire.modulemap @@ -0,0 +1,6 @@ +framework module Alamofire { + umbrella header "Alamofire-umbrella.h" + + export * + module * { export * } +} diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire.release.xcconfig b/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire.release.xcconfig new file mode 100644 index 0000000..7d169c4 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire.release.xcconfig @@ -0,0 +1,14 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Alamofire +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "CFNetwork" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/Alamofire +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist b/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist new file mode 100644 index 0000000..f35e78c --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 7.2.2 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m b/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m new file mode 100644 index 0000000..1b89d0e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Kingfisher : NSObject +@end +@implementation PodsDummy_Kingfisher +@end diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch b/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h b/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h new file mode 100644 index 0000000..75a7996 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double KingfisherVersionNumber; +FOUNDATION_EXPORT const unsigned char KingfisherVersionString[]; + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig b/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig new file mode 100644 index 0000000..2dcd91e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig @@ -0,0 +1,14 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/Kingfisher +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap b/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap new file mode 100644 index 0000000..2a20d91 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap @@ -0,0 +1,6 @@ +framework module Kingfisher { + umbrella header "Kingfisher-umbrella.h" + + export * + module * { export * } +} diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig b/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig new file mode 100644 index 0000000..2dcd91e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig @@ -0,0 +1,14 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/Kingfisher +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-Info.plist b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-Info.plist new file mode 100644 index 0000000..2243fe6 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-acknowledgements.markdown b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-acknowledgements.markdown new file mode 100644 index 0000000..115a439 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-acknowledgements.markdown @@ -0,0 +1,52 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## Alamofire + +Copyright (c) 2014-2022 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +## Kingfisher + +The MIT License (MIT) + +Copyright (c) 2019 Wei Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +Generated by CocoaPods - https://cocoapods.org diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-acknowledgements.plist b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-acknowledgements.plist new file mode 100644 index 0000000..f5305a5 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-acknowledgements.plist @@ -0,0 +1,90 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + Copyright (c) 2014-2022 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + License + MIT + Title + Alamofire + Type + PSGroupSpecifier + + + FooterText + The MIT License (MIT) + +Copyright (c) 2019 Wei Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + License + MIT + Title + Kingfisher + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-dummy.m b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-dummy.m new file mode 100644 index 0000000..727b1c5 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_NewsApp_NewsAppUITests : NSObject +@end +@implementation PodsDummy_Pods_NewsApp_NewsAppUITests +@end diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Debug-input-files.xcfilelist b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Debug-input-files.xcfilelist new file mode 100644 index 0000000..3a58ca5 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Debug-input-files.xcfilelist @@ -0,0 +1,3 @@ +${PODS_ROOT}/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks.sh +${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework \ No newline at end of file diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Debug-output-files.xcfilelist b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Debug-output-files.xcfilelist new file mode 100644 index 0000000..cfe803f --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Debug-output-files.xcfilelist @@ -0,0 +1,2 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework \ No newline at end of file diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Release-input-files.xcfilelist b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Release-input-files.xcfilelist new file mode 100644 index 0000000..3a58ca5 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Release-input-files.xcfilelist @@ -0,0 +1,3 @@ +${PODS_ROOT}/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks.sh +${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework \ No newline at end of file diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Release-output-files.xcfilelist b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Release-output-files.xcfilelist new file mode 100644 index 0000000..cfe803f --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Release-output-files.xcfilelist @@ -0,0 +1,2 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework \ No newline at end of file diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks.sh b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks.sh new file mode 100755 index 0000000..21fad2e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks.sh @@ -0,0 +1,188 @@ +#!/bin/sh +set -e +set -u +set -o pipefail + +function on_error { + echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" +} +trap 'on_error $LINENO' ERR + +if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then + # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy + # frameworks to, so exit 0 (signalling the script phase was successful). + exit 0 +fi + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" +SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" +BCSYMBOLMAP_DIR="BCSymbolMaps" + + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +# Copies and strips a vendored framework +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi + + if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then + # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied + find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do + echo "Installing $f" + install_bcsymbolmap "$f" "$destination" + rm "$f" + done + rmdir "${source}/${BCSYMBOLMAP_DIR}" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + elif [ -L "${binary}" ]; then + echo "Destination binary is symlinked..." + dirname="$(dirname "${binary}")" + binary="${dirname}/$(readlink "${binary}")" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} +# Copies and strips a vendored dSYM +install_dsym() { + local source="$1" + warn_missing_arch=${2:-true} + if [ -r "$source" ]; then + # Copy the dSYM into the targets temp dir. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" + + local basename + basename="$(basename -s .dSYM "$source")" + binary_name="$(ls "$source/Contents/Resources/DWARF")" + binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" + + # Strip invalid architectures from the dSYM. + if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then + strip_invalid_archs "$binary" "$warn_missing_arch" + fi + if [[ $STRIP_BINARY_RETVAL == 0 ]]; then + # Move the stripped file into its final destination. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" + else + # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. + mkdir -p "${DWARF_DSYM_FOLDER_PATH}" + touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" + fi + fi +} + +# Used as a return value for each invocation of `strip_invalid_archs` function. +STRIP_BINARY_RETVAL=0 + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + warn_missing_arch=${2:-true} + # Get architectures for current target binary + binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" + # Intersect them with the architectures we are building for + intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" + # If there are no archs supported by this binary then warn the user + if [[ -z "$intersected_archs" ]]; then + if [[ "$warn_missing_arch" == "true" ]]; then + echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." + fi + STRIP_BINARY_RETVAL=1 + return + fi + stripped="" + for arch in $binary_archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi + STRIP_BINARY_RETVAL=0 +} + +# Copies the bcsymbolmap files of a vendored framework +install_bcsymbolmap() { + local bcsymbolmap_path="$1" + local destination="${BUILT_PRODUCTS_DIR}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identity + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +if [[ "$CONFIGURATION" == "Debug" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework" + install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework" +fi +if [[ "$CONFIGURATION" == "Release" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework" + install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework" +fi +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-umbrella.h b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-umbrella.h new file mode 100644 index 0000000..ed652a6 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_NewsApp_NewsAppUITestsVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_NewsApp_NewsAppUITestsVersionString[]; + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.debug.xcconfig b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.debug.xcconfig new file mode 100644 index 0000000..5696585 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.debug.xcconfig @@ -0,0 +1,15 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift "$(PLATFORM_DIR)/Developer/Library/Frameworks" '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "Alamofire" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.modulemap b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.modulemap new file mode 100644 index 0000000..ed41dc7 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.modulemap @@ -0,0 +1,6 @@ +framework module Pods_NewsApp_NewsAppUITests { + umbrella header "Pods-NewsApp-NewsAppUITests-umbrella.h" + + export * + module * { export * } +} diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.release.xcconfig b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.release.xcconfig new file mode 100644 index 0000000..5696585 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.release.xcconfig @@ -0,0 +1,15 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift "$(PLATFORM_DIR)/Developer/Library/Frameworks" '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "Alamofire" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-Info.plist b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-Info.plist new file mode 100644 index 0000000..2243fe6 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-acknowledgements.markdown b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-acknowledgements.markdown new file mode 100644 index 0000000..115a439 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-acknowledgements.markdown @@ -0,0 +1,52 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## Alamofire + +Copyright (c) 2014-2022 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +## Kingfisher + +The MIT License (MIT) + +Copyright (c) 2019 Wei Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +Generated by CocoaPods - https://cocoapods.org diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-acknowledgements.plist b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-acknowledgements.plist new file mode 100644 index 0000000..f5305a5 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-acknowledgements.plist @@ -0,0 +1,90 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + Copyright (c) 2014-2022 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + License + MIT + Title + Alamofire + Type + PSGroupSpecifier + + + FooterText + The MIT License (MIT) + +Copyright (c) 2019 Wei Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + License + MIT + Title + Kingfisher + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-dummy.m b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-dummy.m new file mode 100644 index 0000000..3dbc32f --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_NewsApp : NSObject +@end +@implementation PodsDummy_Pods_NewsApp +@end diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Debug-input-files.xcfilelist b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Debug-input-files.xcfilelist new file mode 100644 index 0000000..2c7820d --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Debug-input-files.xcfilelist @@ -0,0 +1,3 @@ +${PODS_ROOT}/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks.sh +${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework \ No newline at end of file diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Debug-output-files.xcfilelist b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Debug-output-files.xcfilelist new file mode 100644 index 0000000..cfe803f --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Debug-output-files.xcfilelist @@ -0,0 +1,2 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework \ No newline at end of file diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Release-input-files.xcfilelist b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Release-input-files.xcfilelist new file mode 100644 index 0000000..2c7820d --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Release-input-files.xcfilelist @@ -0,0 +1,3 @@ +${PODS_ROOT}/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks.sh +${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework \ No newline at end of file diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Release-output-files.xcfilelist b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Release-output-files.xcfilelist new file mode 100644 index 0000000..cfe803f --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Release-output-files.xcfilelist @@ -0,0 +1,2 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework \ No newline at end of file diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks.sh b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks.sh new file mode 100755 index 0000000..21fad2e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks.sh @@ -0,0 +1,188 @@ +#!/bin/sh +set -e +set -u +set -o pipefail + +function on_error { + echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" +} +trap 'on_error $LINENO' ERR + +if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then + # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy + # frameworks to, so exit 0 (signalling the script phase was successful). + exit 0 +fi + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" +SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" +BCSYMBOLMAP_DIR="BCSymbolMaps" + + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +# Copies and strips a vendored framework +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi + + if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then + # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied + find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do + echo "Installing $f" + install_bcsymbolmap "$f" "$destination" + rm "$f" + done + rmdir "${source}/${BCSYMBOLMAP_DIR}" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + elif [ -L "${binary}" ]; then + echo "Destination binary is symlinked..." + dirname="$(dirname "${binary}")" + binary="${dirname}/$(readlink "${binary}")" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} +# Copies and strips a vendored dSYM +install_dsym() { + local source="$1" + warn_missing_arch=${2:-true} + if [ -r "$source" ]; then + # Copy the dSYM into the targets temp dir. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" + + local basename + basename="$(basename -s .dSYM "$source")" + binary_name="$(ls "$source/Contents/Resources/DWARF")" + binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" + + # Strip invalid architectures from the dSYM. + if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then + strip_invalid_archs "$binary" "$warn_missing_arch" + fi + if [[ $STRIP_BINARY_RETVAL == 0 ]]; then + # Move the stripped file into its final destination. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" + else + # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. + mkdir -p "${DWARF_DSYM_FOLDER_PATH}" + touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" + fi + fi +} + +# Used as a return value for each invocation of `strip_invalid_archs` function. +STRIP_BINARY_RETVAL=0 + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + warn_missing_arch=${2:-true} + # Get architectures for current target binary + binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" + # Intersect them with the architectures we are building for + intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" + # If there are no archs supported by this binary then warn the user + if [[ -z "$intersected_archs" ]]; then + if [[ "$warn_missing_arch" == "true" ]]; then + echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." + fi + STRIP_BINARY_RETVAL=1 + return + fi + stripped="" + for arch in $binary_archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi + STRIP_BINARY_RETVAL=0 +} + +# Copies the bcsymbolmap files of a vendored framework +install_bcsymbolmap() { + local bcsymbolmap_path="$1" + local destination="${BUILT_PRODUCTS_DIR}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identity + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +if [[ "$CONFIGURATION" == "Debug" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework" + install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework" +fi +if [[ "$CONFIGURATION" == "Release" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework" + install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework" +fi +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-umbrella.h b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-umbrella.h new file mode 100644 index 0000000..e28605e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_NewsAppVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_NewsAppVersionString[]; + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp.debug.xcconfig b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp.debug.xcconfig new file mode 100644 index 0000000..6282fcf --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp.debug.xcconfig @@ -0,0 +1,15 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "Alamofire" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp.modulemap b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp.modulemap new file mode 100644 index 0000000..c581364 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp.modulemap @@ -0,0 +1,6 @@ +framework module Pods_NewsApp { + umbrella header "Pods-NewsApp-umbrella.h" + + export * + module * { export * } +} diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp.release.xcconfig b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp.release.xcconfig new file mode 100644 index 0000000..6282fcf --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp.release.xcconfig @@ -0,0 +1,15 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "Alamofire" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-Info.plist b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-Info.plist new file mode 100644 index 0000000..2243fe6 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-acknowledgements.markdown b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-acknowledgements.markdown new file mode 100644 index 0000000..102af75 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-acknowledgements.markdown @@ -0,0 +1,3 @@ +# Acknowledgements +This application makes use of the following third party libraries: +Generated by CocoaPods - https://cocoapods.org diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-acknowledgements.plist b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-acknowledgements.plist new file mode 100644 index 0000000..7acbad1 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-acknowledgements.plist @@ -0,0 +1,29 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-dummy.m b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-dummy.m new file mode 100644 index 0000000..fcb5b3e --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_NewsAppTests : NSObject +@end +@implementation PodsDummy_Pods_NewsAppTests +@end diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-umbrella.h b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-umbrella.h new file mode 100644 index 0000000..a51526f --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_NewsAppTestsVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_NewsAppTestsVersionString[]; + diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.debug.xcconfig b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.debug.xcconfig new file mode 100644 index 0000000..e005234 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.debug.xcconfig @@ -0,0 +1,11 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "Alamofire" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.modulemap b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.modulemap new file mode 100644 index 0000000..78f819f --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.modulemap @@ -0,0 +1,6 @@ +framework module Pods_NewsAppTests { + umbrella header "Pods-NewsAppTests-umbrella.h" + + export * + module * { export * } +} diff --git a/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.release.xcconfig b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.release.xcconfig new file mode 100644 index 0000000..e005234 --- /dev/null +++ b/jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.release.xcconfig @@ -0,0 +1,11 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "Alamofire" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.pbxproj b/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.pbxproj new file mode 100644 index 0000000..5ee0c35 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.pbxproj @@ -0,0 +1,891 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 0FBE665D9811AE398E64C878 /* Pods_CatStaGram.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8FDB0A1EF9BB2BD53FAF0CE /* Pods_CatStaGram.framework */; }; + BC0DC4E327F84A030045EFD2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC4E227F84A030045EFD2 /* AppDelegate.swift */; }; + BC0DC4E527F84A030045EFD2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC4E427F84A030045EFD2 /* SceneDelegate.swift */; }; + BC0DC4EA27F84A030045EFD2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BC0DC4E827F84A030045EFD2 /* Main.storyboard */; }; + BC0DC4EC27F84A030045EFD2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BC0DC4EB27F84A030045EFD2 /* Assets.xcassets */; }; + BC0DC4EF27F84A030045EFD2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BC0DC4ED27F84A030045EFD2 /* LaunchScreen.storyboard */; }; + BC0DC4FA27F84A030045EFD2 /* CatStaGramTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC4F927F84A030045EFD2 /* CatStaGramTests.swift */; }; + BC0DC50427F84A030045EFD2 /* CatStaGramUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC50327F84A030045EFD2 /* CatStaGramUITests.swift */; }; + BC0DC50627F84A030045EFD2 /* CatStaGramUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC50527F84A030045EFD2 /* CatStaGramUITestsLaunchTests.swift */; }; + BC0DC51327F855680045EFD2 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC51227F855680045EFD2 /* LoginViewController.swift */; }; + BC0DC51527F859B00045EFD2 /* RegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC51427F859B00045EFD2 /* RegisterViewController.swift */; }; + BC0DC51727F895650045EFD2 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC51627F895650045EFD2 /* UIViewExtension.swift */; }; + BC0DC51B27F896580045EFD2 /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC51A27F896580045EFD2 /* UIViewController+Extension.swift */; }; + BC0DC51D27F89A000045EFD2 /* UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC51C27F89A000045EFD2 /* UserInfo.swift */; }; + BC46D8FC281D8AF0000B2BF5 /* PostCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC46D8FA281D8AF0000B2BF5 /* PostCollectionViewCell.swift */; }; + BC46D8FD281D8AF0000B2BF5 /* PostCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC46D8FB281D8AF0000B2BF5 /* PostCollectionViewCell.xib */; }; + BC984672281D70050092270E /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC984671281D70050092270E /* ProfileViewController.swift */; }; + BC984676281D74110092270E /* ProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC984674281D74110092270E /* ProfileCollectionViewCell.swift */; }; + BC984677281D74110092270E /* ProfileCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC984675281D74110092270E /* ProfileCollectionViewCell.xib */; }; + BCCC9172282F8E3D00914BD5 /* FeedAPIInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCCC9171282F8E3D00914BD5 /* FeedAPIInput.swift */; }; + BCCC9174282F914F00914BD5 /* FeedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCCC9173282F914F00914BD5 /* FeedModel.swift */; }; + BCCC9176282F917C00914BD5 /* FeedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCCC9175282F917C00914BD5 /* FeedDataManager.swift */; }; + BCCC91792830A63C00914BD5 /* FeedUploadInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCCC91782830A63C00914BD5 /* FeedUploadInput.swift */; }; + BCCC917B2830A67800914BD5 /* FeedUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCCC917A2830A67700914BD5 /* FeedUploadModel.swift */; }; + BCCC917D2830BBB700914BD5 /* FeedUploadDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCCC917C2830BBB700914BD5 /* FeedUploadDataManager.swift */; }; + BCCC91832830C39C00914BD5 /* UserFeedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCCC91822830C39C00914BD5 /* UserFeedModel.swift */; }; + BCCC91852830C80100914BD5 /* UserFeedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCCC91842830C80100914BD5 /* UserFeedDataManager.swift */; }; + BCCC91892830D63600914BD5 /* DeleteUserFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCCC91882830D63600914BD5 /* DeleteUserFeed.swift */; }; + BCE2ACA5280AE197007ABF95 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2ACA4280AE197007ABF95 /* HomeViewController.swift */; }; + BCE2ACA9280AE8A7007ABF95 /* FeedTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2ACA7280AE8A7007ABF95 /* FeedTableViewCell.swift */; }; + BCE2ACAA280AE8A7007ABF95 /* FeedTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BCE2ACA8280AE8A7007ABF95 /* FeedTableViewCell.xib */; }; + BCE2ACAD280B013B007ABF95 /* StoryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2ACAB280B013B007ABF95 /* StoryTableViewCell.swift */; }; + BCE2ACAE280B013B007ABF95 /* StoryTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BCE2ACAC280B013B007ABF95 /* StoryTableViewCell.xib */; }; + BCE2ACB1280B044C007ABF95 /* StoryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2ACAF280B044C007ABF95 /* StoryCollectionViewCell.swift */; }; + BCE2ACB2280B044C007ABF95 /* StoryCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BCE2ACB0280B044C007ABF95 /* StoryCollectionViewCell.xib */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + BC0DC4F627F84A030045EFD2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BC0DC4D727F84A030045EFD2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = BC0DC4DE27F84A030045EFD2; + remoteInfo = CatStaGram; + }; + BC0DC50027F84A030045EFD2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BC0DC4D727F84A030045EFD2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = BC0DC4DE27F84A030045EFD2; + remoteInfo = CatStaGram; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 1FFAFE268C76F4B4F2EEBED8 /* Pods-CatStaGram.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CatStaGram.debug.xcconfig"; path = "Target Support Files/Pods-CatStaGram/Pods-CatStaGram.debug.xcconfig"; sourceTree = ""; }; + 88E54C1965006C5A51EE5CBF /* Pods-CatStaGram.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CatStaGram.release.xcconfig"; path = "Target Support Files/Pods-CatStaGram/Pods-CatStaGram.release.xcconfig"; sourceTree = ""; }; + A8FDB0A1EF9BB2BD53FAF0CE /* Pods_CatStaGram.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CatStaGram.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BC0DC4DF27F84A030045EFD2 /* CatStaGram.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CatStaGram.app; sourceTree = BUILT_PRODUCTS_DIR; }; + BC0DC4E227F84A030045EFD2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + BC0DC4E427F84A030045EFD2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + BC0DC4E927F84A030045EFD2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + BC0DC4EB27F84A030045EFD2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + BC0DC4EE27F84A030045EFD2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + BC0DC4F027F84A030045EFD2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BC0DC4F527F84A030045EFD2 /* CatStaGramTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CatStaGramTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BC0DC4F927F84A030045EFD2 /* CatStaGramTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatStaGramTests.swift; sourceTree = ""; }; + BC0DC4FF27F84A030045EFD2 /* CatStaGramUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CatStaGramUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BC0DC50327F84A030045EFD2 /* CatStaGramUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatStaGramUITests.swift; sourceTree = ""; }; + BC0DC50527F84A030045EFD2 /* CatStaGramUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatStaGramUITestsLaunchTests.swift; sourceTree = ""; }; + BC0DC51227F855680045EFD2 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; + BC0DC51427F859B00045EFD2 /* RegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterViewController.swift; sourceTree = ""; }; + BC0DC51627F895650045EFD2 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; + BC0DC51A27F896580045EFD2 /* UIViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; + BC0DC51C27F89A000045EFD2 /* UserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfo.swift; sourceTree = ""; }; + BC46D8FA281D8AF0000B2BF5 /* PostCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCollectionViewCell.swift; sourceTree = ""; }; + BC46D8FB281D8AF0000B2BF5 /* PostCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PostCollectionViewCell.xib; sourceTree = ""; }; + BC984671281D70050092270E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; + BC984674281D74110092270E /* ProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileCollectionViewCell.swift; sourceTree = ""; }; + BC984675281D74110092270E /* ProfileCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileCollectionViewCell.xib; sourceTree = ""; }; + BCCC9171282F8E3D00914BD5 /* FeedAPIInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedAPIInput.swift; sourceTree = ""; }; + BCCC9173282F914F00914BD5 /* FeedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedModel.swift; sourceTree = ""; }; + BCCC9175282F917C00914BD5 /* FeedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedDataManager.swift; sourceTree = ""; }; + BCCC91782830A63C00914BD5 /* FeedUploadInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedUploadInput.swift; sourceTree = ""; }; + BCCC917A2830A67700914BD5 /* FeedUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedUploadModel.swift; sourceTree = ""; }; + BCCC917C2830BBB700914BD5 /* FeedUploadDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedUploadDataManager.swift; sourceTree = ""; }; + BCCC91822830C39C00914BD5 /* UserFeedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFeedModel.swift; sourceTree = ""; }; + BCCC91842830C80100914BD5 /* UserFeedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFeedDataManager.swift; sourceTree = ""; }; + BCCC91882830D63600914BD5 /* DeleteUserFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteUserFeed.swift; sourceTree = ""; }; + BCE2ACA4280AE197007ABF95 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; + BCE2ACA7280AE8A7007ABF95 /* FeedTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedTableViewCell.swift; sourceTree = ""; }; + BCE2ACA8280AE8A7007ABF95 /* FeedTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FeedTableViewCell.xib; sourceTree = ""; }; + BCE2ACAB280B013B007ABF95 /* StoryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryTableViewCell.swift; sourceTree = ""; }; + BCE2ACAC280B013B007ABF95 /* StoryTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StoryTableViewCell.xib; sourceTree = ""; }; + BCE2ACAF280B044C007ABF95 /* StoryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryCollectionViewCell.swift; sourceTree = ""; }; + BCE2ACB0280B044C007ABF95 /* StoryCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StoryCollectionViewCell.xib; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + BC0DC4DC27F84A030045EFD2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0FBE665D9811AE398E64C878 /* Pods_CatStaGram.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC0DC4F227F84A030045EFD2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC0DC4FC27F84A030045EFD2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + BC0DC4D627F84A030045EFD2 = { + isa = PBXGroup; + children = ( + BC0DC4E127F84A030045EFD2 /* CatStaGram */, + BC0DC4F827F84A030045EFD2 /* CatStaGramTests */, + BC0DC50227F84A030045EFD2 /* CatStaGramUITests */, + BC0DC4E027F84A030045EFD2 /* Products */, + D298897AE6BD65F75055C979 /* Pods */, + E81B7C2180F3CB4B44A276A4 /* Frameworks */, + ); + sourceTree = ""; + }; + BC0DC4E027F84A030045EFD2 /* Products */ = { + isa = PBXGroup; + children = ( + BC0DC4DF27F84A030045EFD2 /* CatStaGram.app */, + BC0DC4F527F84A030045EFD2 /* CatStaGramTests.xctest */, + BC0DC4FF27F84A030045EFD2 /* CatStaGramUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + BC0DC4E127F84A030045EFD2 /* CatStaGram */ = { + isa = PBXGroup; + children = ( + BCCC91802830BF9000914BD5 /* UserInterface */, + BCCC917E2830BF5400914BD5 /* App */, + BC0DC4E827F84A030045EFD2 /* Main.storyboard */, + BC0DC4EB27F84A030045EFD2 /* Assets.xcassets */, + BC0DC4ED27F84A030045EFD2 /* LaunchScreen.storyboard */, + BC0DC4F027F84A030045EFD2 /* Info.plist */, + BC0DC51627F895650045EFD2 /* UIViewExtension.swift */, + BC0DC51A27F896580045EFD2 /* UIViewController+Extension.swift */, + BC0DC51C27F89A000045EFD2 /* UserInfo.swift */, + ); + path = CatStaGram; + sourceTree = ""; + }; + BC0DC4F827F84A030045EFD2 /* CatStaGramTests */ = { + isa = PBXGroup; + children = ( + BC0DC4F927F84A030045EFD2 /* CatStaGramTests.swift */, + ); + path = CatStaGramTests; + sourceTree = ""; + }; + BC0DC50227F84A030045EFD2 /* CatStaGramUITests */ = { + isa = PBXGroup; + children = ( + BC0DC50327F84A030045EFD2 /* CatStaGramUITests.swift */, + BC0DC50527F84A030045EFD2 /* CatStaGramUITestsLaunchTests.swift */, + ); + path = CatStaGramUITests; + sourceTree = ""; + }; + BC46D8F8281D8A9C000B2BF5 /* ProfileCell */ = { + isa = PBXGroup; + children = ( + BC984674281D74110092270E /* ProfileCollectionViewCell.swift */, + BC984675281D74110092270E /* ProfileCollectionViewCell.xib */, + ); + path = ProfileCell; + sourceTree = ""; + }; + BC46D8F9281D8AA7000B2BF5 /* PostCell */ = { + isa = PBXGroup; + children = ( + BC46D8FA281D8AF0000B2BF5 /* PostCollectionViewCell.swift */, + BC46D8FB281D8AF0000B2BF5 /* PostCollectionViewCell.xib */, + ); + path = PostCell; + sourceTree = ""; + }; + BC984670281D6FD90092270E /* Profile */ = { + isa = PBXGroup; + children = ( + BCCC91812830C38000914BD5 /* UserFeedDataManager */, + BC984673281D73F00092270E /* Cell */, + BC984671281D70050092270E /* ProfileViewController.swift */, + ); + path = Profile; + sourceTree = ""; + }; + BC984673281D73F00092270E /* Cell */ = { + isa = PBXGroup; + children = ( + BC46D8F9281D8AA7000B2BF5 /* PostCell */, + BC46D8F8281D8A9C000B2BF5 /* ProfileCell */, + ); + path = Cell; + sourceTree = ""; + }; + BCCC9170282F8DFD00914BD5 /* FeedDataManager */ = { + isa = PBXGroup; + children = ( + BCCC9171282F8E3D00914BD5 /* FeedAPIInput.swift */, + BCCC9173282F914F00914BD5 /* FeedModel.swift */, + BCCC9175282F917C00914BD5 /* FeedDataManager.swift */, + ); + path = FeedDataManager; + sourceTree = ""; + }; + BCCC91772830A62500914BD5 /* FeedUploadDataManager */ = { + isa = PBXGroup; + children = ( + BCCC91782830A63C00914BD5 /* FeedUploadInput.swift */, + BCCC917A2830A67700914BD5 /* FeedUploadModel.swift */, + BCCC917C2830BBB700914BD5 /* FeedUploadDataManager.swift */, + ); + path = FeedUploadDataManager; + sourceTree = ""; + }; + BCCC917E2830BF5400914BD5 /* App */ = { + isa = PBXGroup; + children = ( + BC0DC4E227F84A030045EFD2 /* AppDelegate.swift */, + BC0DC4E427F84A030045EFD2 /* SceneDelegate.swift */, + ); + path = App; + sourceTree = ""; + }; + BCCC917F2830BF6800914BD5 /* Authentication */ = { + isa = PBXGroup; + children = ( + BC0DC51227F855680045EFD2 /* LoginViewController.swift */, + BC0DC51427F859B00045EFD2 /* RegisterViewController.swift */, + ); + path = Authentication; + sourceTree = ""; + }; + BCCC91802830BF9000914BD5 /* UserInterface */ = { + isa = PBXGroup; + children = ( + BC984670281D6FD90092270E /* Profile */, + BCE2ACA3280AE179007ABF95 /* Home */, + BCCC917F2830BF6800914BD5 /* Authentication */, + ); + path = UserInterface; + sourceTree = ""; + }; + BCCC91812830C38000914BD5 /* UserFeedDataManager */ = { + isa = PBXGroup; + children = ( + BCCC91872830D62000914BD5 /* DeleteUserFeed */, + BCCC91862830D61000914BD5 /* UserFeed */, + ); + path = UserFeedDataManager; + sourceTree = ""; + }; + BCCC91862830D61000914BD5 /* UserFeed */ = { + isa = PBXGroup; + children = ( + BCCC91822830C39C00914BD5 /* UserFeedModel.swift */, + BCCC91842830C80100914BD5 /* UserFeedDataManager.swift */, + ); + path = UserFeed; + sourceTree = ""; + }; + BCCC91872830D62000914BD5 /* DeleteUserFeed */ = { + isa = PBXGroup; + children = ( + BCCC91882830D63600914BD5 /* DeleteUserFeed.swift */, + ); + path = DeleteUserFeed; + sourceTree = ""; + }; + BCE2ACA3280AE179007ABF95 /* Home */ = { + isa = PBXGroup; + children = ( + BCCC91772830A62500914BD5 /* FeedUploadDataManager */, + BCCC9170282F8DFD00914BD5 /* FeedDataManager */, + BCE2ACA6280AE885007ABF95 /* Cell */, + BCE2ACA4280AE197007ABF95 /* HomeViewController.swift */, + ); + path = Home; + sourceTree = ""; + }; + BCE2ACA6280AE885007ABF95 /* Cell */ = { + isa = PBXGroup; + children = ( + BCE2ACA7280AE8A7007ABF95 /* FeedTableViewCell.swift */, + BCE2ACA8280AE8A7007ABF95 /* FeedTableViewCell.xib */, + BCE2ACAB280B013B007ABF95 /* StoryTableViewCell.swift */, + BCE2ACAC280B013B007ABF95 /* StoryTableViewCell.xib */, + BCE2ACAF280B044C007ABF95 /* StoryCollectionViewCell.swift */, + BCE2ACB0280B044C007ABF95 /* StoryCollectionViewCell.xib */, + ); + path = Cell; + sourceTree = ""; + }; + D298897AE6BD65F75055C979 /* Pods */ = { + isa = PBXGroup; + children = ( + 1FFAFE268C76F4B4F2EEBED8 /* Pods-CatStaGram.debug.xcconfig */, + 88E54C1965006C5A51EE5CBF /* Pods-CatStaGram.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + E81B7C2180F3CB4B44A276A4 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A8FDB0A1EF9BB2BD53FAF0CE /* Pods_CatStaGram.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + BC0DC4DE27F84A030045EFD2 /* CatStaGram */ = { + isa = PBXNativeTarget; + buildConfigurationList = BC0DC50927F84A030045EFD2 /* Build configuration list for PBXNativeTarget "CatStaGram" */; + buildPhases = ( + 9FC34730965F4F60239165B3 /* [CP] Check Pods Manifest.lock */, + BC0DC4DB27F84A030045EFD2 /* Sources */, + BC0DC4DC27F84A030045EFD2 /* Frameworks */, + BC0DC4DD27F84A030045EFD2 /* Resources */, + F6E177ACBDCD4CBBEE364C04 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CatStaGram; + productName = CatStaGram; + productReference = BC0DC4DF27F84A030045EFD2 /* CatStaGram.app */; + productType = "com.apple.product-type.application"; + }; + BC0DC4F427F84A030045EFD2 /* CatStaGramTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = BC0DC50C27F84A030045EFD2 /* Build configuration list for PBXNativeTarget "CatStaGramTests" */; + buildPhases = ( + BC0DC4F127F84A030045EFD2 /* Sources */, + BC0DC4F227F84A030045EFD2 /* Frameworks */, + BC0DC4F327F84A030045EFD2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BC0DC4F727F84A030045EFD2 /* PBXTargetDependency */, + ); + name = CatStaGramTests; + productName = CatStaGramTests; + productReference = BC0DC4F527F84A030045EFD2 /* CatStaGramTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + BC0DC4FE27F84A030045EFD2 /* CatStaGramUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = BC0DC50F27F84A030045EFD2 /* Build configuration list for PBXNativeTarget "CatStaGramUITests" */; + buildPhases = ( + BC0DC4FB27F84A030045EFD2 /* Sources */, + BC0DC4FC27F84A030045EFD2 /* Frameworks */, + BC0DC4FD27F84A030045EFD2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BC0DC50127F84A030045EFD2 /* PBXTargetDependency */, + ); + name = CatStaGramUITests; + productName = CatStaGramUITests; + productReference = BC0DC4FF27F84A030045EFD2 /* CatStaGramUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BC0DC4D727F84A030045EFD2 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1320; + LastUpgradeCheck = 1320; + TargetAttributes = { + BC0DC4DE27F84A030045EFD2 = { + CreatedOnToolsVersion = 13.2.1; + }; + BC0DC4F427F84A030045EFD2 = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = BC0DC4DE27F84A030045EFD2; + }; + BC0DC4FE27F84A030045EFD2 = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = BC0DC4DE27F84A030045EFD2; + }; + }; + }; + buildConfigurationList = BC0DC4DA27F84A030045EFD2 /* Build configuration list for PBXProject "CatStaGram" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = BC0DC4D627F84A030045EFD2; + productRefGroup = BC0DC4E027F84A030045EFD2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + BC0DC4DE27F84A030045EFD2 /* CatStaGram */, + BC0DC4F427F84A030045EFD2 /* CatStaGramTests */, + BC0DC4FE27F84A030045EFD2 /* CatStaGramUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + BC0DC4DD27F84A030045EFD2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC46D8FD281D8AF0000B2BF5 /* PostCollectionViewCell.xib in Resources */, + BC0DC4EF27F84A030045EFD2 /* LaunchScreen.storyboard in Resources */, + BCE2ACAA280AE8A7007ABF95 /* FeedTableViewCell.xib in Resources */, + BC0DC4EC27F84A030045EFD2 /* Assets.xcassets in Resources */, + BC984677281D74110092270E /* ProfileCollectionViewCell.xib in Resources */, + BCE2ACAE280B013B007ABF95 /* StoryTableViewCell.xib in Resources */, + BCE2ACB2280B044C007ABF95 /* StoryCollectionViewCell.xib in Resources */, + BC0DC4EA27F84A030045EFD2 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC0DC4F327F84A030045EFD2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC0DC4FD27F84A030045EFD2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 9FC34730965F4F60239165B3 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-CatStaGram-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F6E177ACBDCD4CBBEE364C04 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + BC0DC4DB27F84A030045EFD2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC0DC51B27F896580045EFD2 /* UIViewController+Extension.swift in Sources */, + BC0DC51727F895650045EFD2 /* UIViewExtension.swift in Sources */, + BCCC9172282F8E3D00914BD5 /* FeedAPIInput.swift in Sources */, + BCE2ACB1280B044C007ABF95 /* StoryCollectionViewCell.swift in Sources */, + BCCC91892830D63600914BD5 /* DeleteUserFeed.swift in Sources */, + BC0DC51327F855680045EFD2 /* LoginViewController.swift in Sources */, + BCCC91792830A63C00914BD5 /* FeedUploadInput.swift in Sources */, + BCE2ACA5280AE197007ABF95 /* HomeViewController.swift in Sources */, + BC0DC4E327F84A030045EFD2 /* AppDelegate.swift in Sources */, + BCCC91852830C80100914BD5 /* UserFeedDataManager.swift in Sources */, + BC984672281D70050092270E /* ProfileViewController.swift in Sources */, + BC0DC4E527F84A030045EFD2 /* SceneDelegate.swift in Sources */, + BC984676281D74110092270E /* ProfileCollectionViewCell.swift in Sources */, + BCCC917B2830A67800914BD5 /* FeedUploadModel.swift in Sources */, + BCCC917D2830BBB700914BD5 /* FeedUploadDataManager.swift in Sources */, + BC0DC51D27F89A000045EFD2 /* UserInfo.swift in Sources */, + BCCC9176282F917C00914BD5 /* FeedDataManager.swift in Sources */, + BCE2ACA9280AE8A7007ABF95 /* FeedTableViewCell.swift in Sources */, + BC0DC51527F859B00045EFD2 /* RegisterViewController.swift in Sources */, + BC46D8FC281D8AF0000B2BF5 /* PostCollectionViewCell.swift in Sources */, + BCCC91832830C39C00914BD5 /* UserFeedModel.swift in Sources */, + BCE2ACAD280B013B007ABF95 /* StoryTableViewCell.swift in Sources */, + BCCC9174282F914F00914BD5 /* FeedModel.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC0DC4F127F84A030045EFD2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC0DC4FA27F84A030045EFD2 /* CatStaGramTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC0DC4FB27F84A030045EFD2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC0DC50427F84A030045EFD2 /* CatStaGramUITests.swift in Sources */, + BC0DC50627F84A030045EFD2 /* CatStaGramUITestsLaunchTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + BC0DC4F727F84A030045EFD2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BC0DC4DE27F84A030045EFD2 /* CatStaGram */; + targetProxy = BC0DC4F627F84A030045EFD2 /* PBXContainerItemProxy */; + }; + BC0DC50127F84A030045EFD2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BC0DC4DE27F84A030045EFD2 /* CatStaGram */; + targetProxy = BC0DC50027F84A030045EFD2 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + BC0DC4E827F84A030045EFD2 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BC0DC4E927F84A030045EFD2 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + BC0DC4ED27F84A030045EFD2 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BC0DC4EE27F84A030045EFD2 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + BC0DC50727F84A030045EFD2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + BC0DC50827F84A030045EFD2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + BC0DC50A27F84A030045EFD2 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1FFAFE268C76F4B4F2EEBED8 /* Pods-CatStaGram.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = CatStaGram/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.CatStaGram; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + BC0DC50B27F84A030045EFD2 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 88E54C1965006C5A51EE5CBF /* Pods-CatStaGram.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = CatStaGram/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.CatStaGram; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + BC0DC50D27F84A030045EFD2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.CatStaGramTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CatStaGram.app/CatStaGram"; + }; + name = Debug; + }; + BC0DC50E27F84A030045EFD2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.CatStaGramTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CatStaGram.app/CatStaGram"; + }; + name = Release; + }; + BC0DC51027F84A030045EFD2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.CatStaGramUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = CatStaGram; + }; + name = Debug; + }; + BC0DC51127F84A030045EFD2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.CatStaGramUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = CatStaGram; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + BC0DC4DA27F84A030045EFD2 /* Build configuration list for PBXProject "CatStaGram" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC0DC50727F84A030045EFD2 /* Debug */, + BC0DC50827F84A030045EFD2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BC0DC50927F84A030045EFD2 /* Build configuration list for PBXNativeTarget "CatStaGram" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC0DC50A27F84A030045EFD2 /* Debug */, + BC0DC50B27F84A030045EFD2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BC0DC50C27F84A030045EFD2 /* Build configuration list for PBXNativeTarget "CatStaGramTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC0DC50D27F84A030045EFD2 /* Debug */, + BC0DC50E27F84A030045EFD2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BC0DC50F27F84A030045EFD2 /* Build configuration list for PBXNativeTarget "CatStaGramUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC0DC51027F84A030045EFD2 /* Debug */, + BC0DC51127F84A030045EFD2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BC0DC4D727F84A030045EFD2 /* Project object */; +} diff --git a/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..d8940a25f975e9110f040c647202735f77e7a931 GIT binary patch literal 72112 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 kk3LO*w*DFInZ~mX&vZEZ{TD6bd + + diff --git a/jaem/week7/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist b/jaem/week7/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..e0e54bb --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + CatStaGram.xcscheme_^#shared#^_ + + orderHint + 3 + + + + diff --git a/jaem/week7/CatStaGram/CatStaGram.xcworkspace/contents.xcworkspacedata b/jaem/week7/CatStaGram/CatStaGram.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..92ebe2c --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/jaem/week7/CatStaGram/CatStaGram.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/jaem/week7/CatStaGram/CatStaGram.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/jaem/week7/CatStaGram/CatStaGram.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week7/CatStaGram/CatStaGram.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..e3eb89479c7ae0097db1df993f96dcd141c566c9 GIT binary patch literal 60381 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 JDPa=ye*m{=V)Fn1 literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/Contents.json" new file mode 100644 index 0000000..73c0059 --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/Contents.json" @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_facebook.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_facebook.imageset/Contents.json" new file mode 100644 index 0000000..30dc307 --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_facebook.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_login_facebook.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_facebook.imageset/ic_login_facebook.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_facebook.imageset/ic_login_facebook.png" new file mode 100644 index 0000000000000000000000000000000000000000..f93cd37698964bd2ec9547b5417c4541707775ae GIT binary patch literal 6350 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 Q?*R~wpE_1{)b;ki0fakz3IG5A literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_hidden.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_hidden.imageset/Contents.json" new file mode 100644 index 0000000..a58f03a --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_hidden.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_login_hidden.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_hidden.imageset/ic_login_hidden.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_hidden.imageset/ic_login_hidden.png" new file mode 100644 index 0000000000000000000000000000000000000000..3649e7d77250938416d3a18be15177d39db5b79f GIT binary patch literal 11970 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 literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_show.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_show.imageset/Contents.json" new file mode 100644 index 0000000..fbd5cda --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_show.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_login_show.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_show.imageset/ic_login_show.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_show.imageset/ic_login_show.png" new file mode 100644 index 0000000000000000000000000000000000000000..a659b824d32348e5e5384b7b629d6b5efccbbf8e GIT binary patch literal 14608 zcmeIZXIoR<6E_+NMWuuEE*g|7MS4>~q)QiR8U&OQnsf+>-U<-~0qMPW5D*Y4i4vuQ zQlvwSG(+emgaA3)|8w4+^9ddFIHEYUmQ1{FYSuO}%0D(X(MtAkCKp>%sa^Kk-xi~U!Z69rj`H*BnC3lziAVevoRmu;cOdC+OdG$xh3`= z-Ce%!62%v4Vg*<6GOSl^tMN6>(KQo>>-S_?CyLZ;W_ubAEkD}c{J|sLyM$Ro#L*V@ zYF+tH@7(3b9VGIf(9M+Db>U%c&#lv0O3n!7;J6Ex%o{QJATl@T*K{O{m{IcO|NHy@ zWdU2y2O%_<&7P=yICua0rP;*eJv)pFX6{!E8k9lrGv8lhNzHep3A%GStAYD%No}FI zKi1EHgFn>%z;l28cx%gVxr-x4GVH!=-;ulyVgx<;fGn&{`ih-2SG+*pjjZ(^1)rL{*#deI{^h~uBiI6d5xmZ&xP zA(?klp-PSn;B?R=-k9!ypAkghjil+}5IyeUTs$g$B#jVla@X^B_j+1#K@D`TeugGY zW2ca61D(ztuX9AkBbX3-ai`2Bd3casClsXi<_M$98m6d_WI^~P=FhFbcckO*u)bJw zCkRvk$!64I3KQ98#yg^IS|Sla$R=b^Ty1jc_W)2qnZr3P#xS{UA838#wE+$2FouNm z<8l--d5Z(-nSD_5zvNG=;1&}B3n)?WWsqYSrtP|&6CgH`1_n(`9AaBfxR9Rsy{H=l zVhvjueoSlJ?RO;)0KOWTY#ryqt~J<<_#G4Ivk^=vB*p=2*>9!6237zlY3{%$` zv-fi}G!;3~P(b=y!ve-fgg{pZXfV}rY^S*?1DD)CaUC)E3-~(+I*i8(fhvrH5Py)W zoZbglUP>e7X{ZSU7i>8gK~-PRFh&@`?KP*Tm#R^1w60<*qRGa8Kzi<(;11?6rGPhe zC6#3UZFOi2@}SS%k}VKaP$WyUKzDLwb5N4|w+N2P`2sncJWwP8nA1R=;m8%kdBl)+P^Js*DzlTYe!*1J4I!VA;Kk%f{CHA@-Sn&lqk( zO*#9!mRNRx3z9;>3x-;&q1W=qH5rgfC{(-Zm2`s}zd?GS_&8Lw-<6Qtr+kuz+_|k+}vb; zzol+0HA;S4ZyoK*;sZMkKVpG%DVPXB{m9L=x+C-+z=oA|R7vM|*-?0LpH}01l5qM8~)O(Q}V+G=FG#e>VmgeDIZ0-I?qI+mmTh4!^} zv1q7-KiE-cEjGqwB+YR*Yl<0KprIwkM;Y@41lq3?Mw#ikmzrgd{lei|u)XPa#}G-vkb?g~{$ zTyjk7tR$}heq<3EV?=zNK0le2vxg${C36m4QQ4i+JIlVswPy&O9z$Vgf)8Tn5tHnZ zOY#zEQ56M2L?T}Nk=5&Rj>Y>;i|4Z5A7I)ruI!S+#$$2nbX&A?l8`QU(imi!0XKE6 z^>ayrj2O&1Qn0DBhGv+-{Hsw?*4(9{`W&-CaNj1{tncrqwhWspE|l}{Mpt6+$OW7- zF092TK-z4=sfaE2j!#3*eEoV}Dn>9Mu+65T6NOTpmfzM`m4nh_x%?Nw+`>s-+mlI?+(wZwdDwU0T$6{cwh?)KoEsB9eT#8YeJf?_gIH@S=Zd}Uj!qyX zOZ%r={k1Ta@EAF+(cNW)I=5u+Rs0PRKi=I~Le~-AHP;^c7uD%&-F_yx7}gS_vNP1@ zu1~np6o-1T`bR|MHdZUox!udM^0$k*(Djs4=-}W6W|0FdUMVUBI$>JaaXjDgaiHbB zfTt#IeF+Ux+7rY#^yJ9RT6tCZqTIVe-%%q52mc;@vWUiC!JG})ODs9o5$8D%7t={G5 zgli^oeqGb@AF7%dj%4yWB+Vw0Frj}(#?Fs0h7h{Wmqwk24a5isKz}FG&R7sU8QEhw z4rcWps&Bsm?u`|4KjltXw4O|IiJol!lH^|3gEl(p%8CnJ4zb%$LI}7^9Uf3qRY8)-o!3Kju8HAI#7zOm2ekMHj%HApn{ z9Q(Ii8ZJbr<-*o~yWwO00ZMh(A)bk7boSIs8Pb^3^fFG2U@BGxQk6sx667E)!>6`i{@G5Vnx2@?u1F zZ;b0TB~Q+vhLTY&+D{25!!CAbdP3vH$5ZFS#J7!bfzZG1A-eqX>&O9w1$(PUs7#^u zc;;V>Rz28yz zcTyXzT0!rf(ULS!ep##JzHRCRma{jv>>2%d?J7;^yO`7XL1VnO%Q2VyC67dp(9xns zo-o0NGy91t8nx6Z2YJdd>A^4dcB=^KR*gNR{7=V}0#OF@tBpzHQGT>4NsH?7{qox( z_Ngs2XEf*?`=kj>tUsrBYTj+)Nk;TWd_M&L<1#8VX;D0$I8~0LtEw`UfRGMGv)!x% zd3PY2!gXFmJ)(6|^nCo_wU;gL1l&PKp1YclEZb#(TuOI47@QRDz12pDro_z0 z_h){(RpmDb9+p6laVNQBg5rHe%y4x0UqOe#s&1?1TB2Q2W601kO~(={J6ivs!vHJW zO&qqD-!G9|)vg zja(^UQA+xCn{+#kW<=mmL{-@AsAi%~Jvc_iz8?`8p#8z*i*NB|6O^x5lcsGeS^R@rgIRF^@%+i_6K~k-K6%8v zB{wVhOz-T*mWt}b{HGnI%2e_=1Zwdty~^g#HEeDgaUJ%0s56XIBQ*#q z9*yfIUh<@w5+GL!1+`T@VRRx2z%L>+`cS3Y}U?f=L>Ig>)pA`LIjUzeJME623` zl1Cl1NGk(q74-K_8rEx{AKBErKzT+_=a!DufL-jvs-I{@g3*%H&)GRAtW!+3k01>j zu)`Z$5NQNB9>&N0CrO!dFHIBC!@D3QFan5?5cgMSAWojO>U;hevGn->@|4MsJGk_( z=$Y=9D{$Kg=Aka@$FGMj;2oN)0{Hxy$djF8bLWYU&x$LQC_jj~GhNSnNOpg(KIA4( z&W}r1$8{BY$z19>0{)B4gEVz?j9cHY3}2sT?kL{o`Kg~kD{_fClO3@lE<|VZly%4@ zI!Fn|^WMWgtG#>i1!gbakJAiy52v$R-&q+dA|}F7so_`J<_KGSKF}=P`rlNQ0tFwM z>=-a=Il=nX^Q`Q&2E?jo8=)|iD-@<3FqI?q0kM)t2vrYxU$tS@BpH{LGiQ)o9SM z%sjZp$6fdX^5{GL&gU!xtrX{}{sipaP$`!2_gSs3>V|f);3U|PmgLD>l1OAYL3q@) zqWQuYx_Z)~JpQa?iB9v~5zIb5Tc0b#Y%f&QTQZc>akeY<=)+7|+;_pA7VPCVG|g}U zdDgczMf6Wyj_oY#5eIp)D|3rE0}?G;sIA=a6cYpZl!4v|oEFX( zd&WC`2wpIMU-_32{U*JB%r#Vp@K9waYDDysol01QH&Vy&%plk67gy_vZ6_)DpEJ6H z-<+&Q`56W`gTh$+2 zLlgEp!D=r(>TbB)ih80FT)DaC!;t6jgsIZmuP}3f&O)wO;keJS7aeCkH2-N(ejsoE zdmuWWlcK->)fY>HqsP)!5<@mrq@S!jtl{m*$mqzLXDrK7P5;SvihHT-Hy*0gaHFA0 zRq%+L99@tdB^u`Rlf_!X@W&jgqnk_*o6lW-s_Z8@Vuso4fAYrVknW)@nb%LMjdiN@ z&G&nbI%aUuHD%tPgQc~(8p#>X`SY8(E8|}s^*1!0uWYQfy;=~N;`Q>w_kJVBtE8TA zP8I&6%DYqZsdtmnddL}Lw7VG7Cq1(~8n4^niUUw;V zaBJkCO8$x#qm%id^}Wah@fNk=F4)GB9HCR)TK7KaB&Ot~4WOT7`!q|2uhQ^`Pixo@tJ8TD17g&{H70 z5*9K$^*m+6%Ti%1x||i1<^|Ij**`VaWNgy<^l9OIjl{yh@n{~FqjZ|La6XEDad7=1 z;&^d$mae0&hGFe?eaV^Y#X2l0R%0q_%3h*f_hWYC-|nG5D_nxBk5%FAJKbm<7ZZ3) z##L{GXSyBb?7*qWx2b+hKiu|d^Q6sSv4f+9>upil^Lgen`2(>1kRXkP!}@sN3Q`u8 zf+#kw?i4ZfsYs5_?HN!!UVGcI(gGPC@7?6uSaCXD3Mw;lY&rhzDo@pe=kv*jf*9Z1 z2R94pb=sec`p{A3{klaW_zYGXZ0En^t9}UHbsl~&J?9az8eOjK7IT+N7BkQI9Fdo!yfFII?hmfBt?F!;_!>h zkFTo!voP^@5cRlvHVR?VUt9Ni{{lfB_3T-fr5*U<2FsG_ugBBOCk_rQ1-FFm!gcvG zO?KL1xJ()4B+%$LZ#4CP%~JZdBsP|s-EZhwveJt{(1nnJ*?un&_lnXIDdo2h9V?Ks zO8ub03U?9VN;wwkj?UaTCg7vK#xwymn=gx#1zf@`%_40ABsFM<{h~1){Y`aySD?YF!bcXnl>WzUtQn40g3p!*mQ;rylX(`t`#$ID+mh~Xg$tE)dU@ za<~8`E?iGP*47b_c9p57(Fmp`zg2XA-!wBcKj!jGUUYr6mz=y_wcC+#0xb7Sce3Yp z6ZZ#`Pu&~GJy>U9@DBDN`TUn!KG-!)ESnz1#`Tm`p}`eY5fy2q z^MDs3WFnZcE-Cz-fJM%a{ED%ppN8+9>{GX{?oxjGcVv$XjTD?k(w+Rbv`4$3dZ&=^ zeh9M%?4o{u9zeNsM_Y~3RsA`J@E8Hw;j8UMQsb^Ei=L2yNaO0$%LxT%5|>GMSegUI zYnL*_zWoW1nJ~UAhmnFtdf(_-?b}l2atEK^vfV@IK6BV^+?!~Wsvi|Hfus2^!D=g8 zos8jV0opv95Chu^ehhhul#L3M;I6khzH>hHsvLMwc}`UDpy+dr&2bMP z&sSfQ(~%akmqV)P1~<1&S@zsH8L8SsDQjVu$Ev?wI69wzKD|6SJ6KO%uM8yK_E~#B z9|P$hFs_as7#@%8Tl9jmh2I!4Un1p;+%dr{QmU3(Rn5~n zMsU&Nn}3^aS~N`8j_(5(nS`#5Q|Z@r+nj{xb*fgfs{XynIx)0pXVv}#soC|uEzk5c z7n9VdD%(N8BMEC0oht3UK(%L)y5Rz&ZIN$mteO+rP4shS^RW*xNQH?vglII?*LK?) zo3%0?FGMA{0)RS#Z7#C~}2>Mh)h>6aN8&$V6lj;$o! z*ZlI-8MALT()kdyt~MZbl^pA3G&;HnxViwwOfQkjcLMFoxvR$hkssFqJ+x$dhjRQ<92Ve`l)MXHAz+m{l&&xU#a8~sKI z6or_AG{Jyc`g`YdsSwJO@$8fs^zLIIA@%fwI^-r|n0wSOj+3QNRYHqWpME9te zkefDDM8BJ@SE%siwi{%UCn-909frGsrR&hhJl_#?B=2ZF!osYj)UNAf0@IBQ{CmU3 zS=a=&Nbx1x+OS0Bos5kAbN~<1t2HpZ=&}@s|!2Emi8X0q4>Cue0CNdc3 z?w+j@);huveDsrj5S%TROe91%CC5|S2pFI?s40{!`p}k1o}KYyUvnMg|JA1sG-j^F5JY2Xbc;o* zH-E9QOCU8eSO@#${wwKHkZlJMG}kLfcoV+RK)3K388lHWetn18EPd!jL-SK<%5XVX zE@gljr%c#02%P|t+aJCCD0-lssJHA7Dv)6X*_5UryPKF4z7B!0*mjL@%>cXBBtL#hQ(vFiBBTIQzXpgDuIv%9IW+}Df}!1W;CqF(Y3DB~?L$xDG1 zP9#9I`F|!SCY?~4C|&){>_-}o=JU1jZ$EB+VB;lv;DVlh<6mUOsP@fYy|dhZ#`FGE zyg(}wLWMJeIya;-a)3tN)WYUtzop)#7voL0MY`jf;wOR{DRVCWlu!FV%J=WtWA*?G zz2Y|gFn%WAyO3L}4XFLx(J>R>S*!r^i-yr9wyDhJNR1p1r z51OnO@~Ed@RZ_Y`z%vH$0hWMo4Bpew4=2i6TsvirO)Boem)*gD!a{-=jXV(zVpBK?hzjRpM-d|tj^;qCT&YTXZ`=7rV zq=%cGt>VMNZcFZLu)F&%4xA9@b zc;IxszDG10|3 zLN;~d?GmXe9@4+wJi}Is31M~dmB0OnD>@*n{qn{W75b}Fgo#D zz%|(!6ujI$9?9E-hUhdgkzAJ&VUYgyfr^h(ma%5aDpn(7?KW@b5oQ4E)l_H8)D^Ov zug_Z$h!6P>3$Wzpq+;1+?=tbo6W1kR6WCEBY0hS|N4%@;a(_#@y~sF7k$md?PEFEf zDCjy6;Oi*6*p05#Qeho64BWX_Nwk#rc9E%nTY)$sJ0J$-qMRFHI`g@T_`2$%CQa+R z#TbaOdtxi0W9JLIzxMDNtSB*MG4$0MbX8{JSA5@={cVilKwZTJ)!5^_WX+ry>DoiL z491hL|BNqYih`SrtI+)cGXG*M&!?oK&mBnJ27vRuF+_dJq93GR6txE&+G@L#W9Dfi z>3|%Mcn-q9hv@eO+XfT-_qW(TT*z<;bhfP_CI4H_fNLn7fOBp=Ek;a}*GIkT6?2T> z`TVrQ*V>D!Ob;JfcdK{)V=u|xKI8#}S-W+kUW>12a94|WBgHjW3Y68$7Vm8=ox0yZTnG>&M?Zcn*Dd=z`=Q2x+JlJ4hw{ze3BRN(dekqcEKgjSK>Jhg zz}?K!ZbCm49ajx(`gW5kfz{Qt<9~lq*+otrLoH~2B`sP&2GW73ToKsix8cQRX>Tz* zK)(H%WV}o1?e)2|aNWkjq8KjN+EFwvP%W5PsH2%^{=PcSsP=86e8Yiz$RD$`1I(C- z7dY4x;xG75rZgIp{+f>(ja&^-iT0Q`gTi4VM$Q{19cfLJ-p39sT8$+tSLabnst+~8 z5qvCF@Uyg&GX;&+KT!ycOjaj-_`e+QJ&@!1un|b#pf${+-~mI6D`@fAz6%f0KvtML zn-H;3rr9=V>}^aqoQsJ&vQj|*OF1>Is3|_NV_b%Y7Lr{G?y4S|ZMl5HRW3jbyvUb( zQ$Sg|HMTP$ViG830f;W_2QvbO>1J+w&2N~dcaP$*NHd@BiF=n!PL0;?n_x=fNF4IK!Eqt37kkA4JO6Tjg zZ^0Flc0Y?LUk{gUovoFb)Ur?DKg}7Cy3*zR{}OoBXYFmt8UK|<9I=z5p&fH5Lv#Dz zdPTKO^`tQMwtuw(4^B_?V-hMP$XI>oo;+CDQ(r}&nKoiG-1PQP@q*tO>GvOmqIRAd zqc%k&Fle(ppg70s=5WSkW(`cN1VW}VCTHkt*E1`i$GO}jmw>7Q_kVl zswMTs_ql_5kuosV93JyT)gC+x6pwDST)b}%|I?anvg355k->2C=H%?Ur3#JT)oPmK zA4cOrE!18o^L?}BEY_Sm|Hz~foyJd53}KfJG+KfhosdDa1l(PMZss%gBeCdbj`eh7 zC^I+#UaQKLDEZ!-VcU0?a=Sg8FYMMe*`H$dm#DW)ub{t9&;)NA>{4!Nk~uyE8TtMg z2%&@DJv;E0q*_@CRzxcymHM`rHayrx`A=hlo&RXN)Y0;!j&gcUwqS?fPDdB!&VjXx zb3E3JEDGU_lsD;`J9Q6R954<*;i+;-6p-(gK!HPXt2h+@sy%u+f`)urFz@r-kTxaL zT;`x=HmQs2+xh%9kFgqC_uE*v3v671?$XdA=fiKA#&z@|*NPR!TgLuZ5Oe?Jin-5v z88YhbnqRr(2$Z*Gns4~akxw5=k*{4ZfAf8fKK{wDky&>e{}ga{&yk07y}pU^#imi@ z2-L-)$9$rz-vj}(aU|x??Z}UWC~qSghepUmqm<-j2H0I285_WY=s{odV{ zdmsO+exN6zTs`)`w}T$IDbAVO8`G$a*wlHpvgF9^c{}xO|W&}ZH(Lh3Y<~U+o{BO)d{B4eMN!WDOoHh z&rfuIm<@vyq&`3LT0&vyms*2V+n#3x*AX@hgY3?jJ-N1X6a|V!Pp*)8{i__%j%DQxdz zILfnb;`~gWFqv)M&k9|A(lK{)4(9UUG1&I8C(c=UkDq#fnog_s2ai>I-ks<~+Wf0A z_5f*d41@{|Nw)YQxQNeMnFk8VmzvJ2iH2z(EW8X94`BF8zdtPIqqZdjRGPact*Aj_ z-}}o4p1gtI07bnxiJvj*GSJGm3oiRPcc$o|0^;Qqz01fw@z7Z<-TG)r@`xQ-u<&W|`!+0%xFJxx2Ty`VARY}>0<&D%FSVrF z#kkv3r=BgX*uf-qYV2DY(>@tcAFiVpUxkfjnaq86y<5|~@OCQQO&-c{xS2XpAW|n` z=U&qeYifRPdYb+(#scBVQGQZ+d6otZi7A;h+Qc3d*v`hU2+6n+QZ%2gEDbi6Q;G-3BPaExe!-=-HfFri$EV{@e&i-NwrHs(MZi zBie^P$^{C53lkp=ubYLOYjAWsY*sxO28X5BdC3=rYob|K8K8aDoJ1O+$SAp{Ohqz& zwfTD$+WH-6T@f;45fYZRWi(sGnM$Xz2tlMX(C;zp+r;XZ6Ni61trHV@S|N~l1@hJ1 zHPpJkrl_^`QC;>lHn5GjzVH+#0qLz`I4o@&NqiOOFqZ>xY7TcFPJ=vK(_q^#;&@U9) zT1Ffm_l03fjcuoekLbyha~D&j{kzkuWEkZB*EG~+k9}0hD8m3ZftM3h*5i_zTdxnO zavgy_WS*?UG=Neof7F6&YWWZ16?ivU7`8PV6ha4d(Sx{?wm1yutD<^FPueb@X0#(c z-vnsecKqERen@`cgq&#Nq1BZ@r*J1_BYXbWV7-NZv-`gWYpXxPDb*R7pByt3-@6R- zR~NK!PP>|p9MhnVbUtKt)JV@fcs*wp`KPBdLTV-+8QF*4G+1K9`~sW>!HXw=(>0`A zTFM@0m0C3@nI}#9x-sn1C)V&{rG?8$mD|&?sI#d)(vlns9Zo#%1?}9)nECIs+7 zFPj4ZGNe&`^6TpzQf28X{Tijc3|2v%NehIjfo+E;sVg+1baueTkJPS4r0j(Irn1rj zxl^;eRQ5Kk@{T%EEm^fah$pii$NJp%!QH58AeQxB^bTh~x;i{sH_D8g-~UCycVn31 zeJ&p)NYY66ZIu|T$l>S>RFWahT-)b6B<|^pfOO1>e}D$!1t+7k19?Rf=hn{Xr$dH1 zNpurWZWGe^f{&ybOc1*I9cUk&nGMWil~(DkeV+xpLloq_w1IwB?@5x{6}znQTR7;h zrteRO^EoyTH7m875cag`?-m&ec_s*ZTw@KhA;&DvgM=*F&J%5I`uW6C)Q@g5&-L<8WmdYGcjjIZOWmJ$1F)308EeIa zAqXT7E_nYo9Dr1?arh0(`!v4{)XHCYFsYk!6So!c7JXs+0{(Ir(cJkCJf6#~(U+xz zPVGg;e%icbe}9#oF!jyRuS%emOf>_591`mYUHQ7o%VC!~o3;+tm}y-l6U^-DGO;jk z>NzKarY#+o7Q(hA^xfb2vM5K6NW1;f(XJ$in}hD}F0``wvygK;YHBWd)Kcy^i^d~X zIZS@b6)uDp{c7$%Qh-WGNxDo7>|lUAE~Rl-5I_9NCMz5h+Pg(`U-7~x#1cGt{P`NP z{@CEOs&oteKOAVrOi6Vc)hZ4FO%lSK3sw78pyPLmT1K>E?nApGboxQPf z=e@S*VdnqfIE?o%5T}pZqytC~Id;sV#CPVR9q~tNnb?Os&|3?4@c(sBU>-pG){OBD zlbu+`vmxw%4UTbsl&V%=6sfY6v-K?Urw1+?>s($4!1qS9xIQJG{PfG5)|2@;Q&^yq zVgp!%3>oN+pOo?SXR$%H=*n8d!An2JkO1FeO3UTy+AGG0n#`FB!*{Bo^HjT>e@72`|GMH?PU z3gutqMXPE(K?8j;4K}GkNQnzi)r~eDTsVSmAE& zN&$C&?*9XB?L5Yr@4A}ugOT0sema%)8+W759gZO3465Z41caG=Ky18uH@UTpO~lni^bjI zP}P36AMNec##$%>307|nw;)=nl+V6;AAtom6wiNKLhCdI3NBo;$)8H#TlPe+DF(4A z%#g5Z2h|Q(=LYY-7;TT0XAdd=kZ(EZoQ!pk|AFDew$*V3|K57-jAWw?O`#qS40d8r z&Ml?mPc(m041-&;T{#mOBMz1n59*&{bxzC2(0hnGE3p_k=jMuWi82n+D7Iwk*P7d# zlE1XJCd`VBZ+sBmnb{=Go^(5yn7#_pbbb0>8=3!(da2e#g=;|4g3jaF&pN9KR>_2) zF1OFtE9Ba8?k`+@Dl~>9(XRK!eED^)6)Xy7C3ZY5wc5BNlo`!Ju20u+*WQLX(aJP$ z+{4)~p~KcQsu8VV6AhSiCEp%$l9uD(xuNgbquZqvZ6Xt~Q*=L^qLvT%pJ3c;_f%t`^DV`CK zUR69hu7}R=ips+zNF##pNj^gj1Wi>De^Gn*VEksJ2?5i95$%oHQN8v8GZM$&7mI)J z_+>P*6~xQVIQsY>HZ%g4QK{Ni0vn>yd2Yg>Ii!tIMNa-!8h|%)Z7brWF||viZit8Q zw)n%ti?c3PUk~I|PodyAhL@ zaNQU|f2QF^AK)b3WIYi&Vo|u}741f%?=!fc+|!s$*}6e?^J13^D@ zUFdR*iBc5=U$Hb_R<;W;2ADVlQz$1m7Ot49Ht^`$&Li}DSDcrRM7&5a{sn8w4i@HJXvF&2Qy=@nSY^`8CNMz);U}3~YpeLp%p!Pef|b zTd%qH2um>N!jJSF^_WG#IQcCQ6`gR&577XyKJO-!{X^jRNW+=$xv&h`L?OGT<&w{~ zwK21LIks#+MLn9&v_$#Dx(fYfk6YD!+77zjttoZuwu-JU=7f$iOCuAxJcZN>n@0dU z`0XLkvT)?wX{s=qJGRHpmToyj*u*9$%{5}4G%Ozu3q=$4iD7Bh+He0OJu6~M;7pE}+< zz`_a1S&y^>bb4cvf&g$o3xM>85cN!XqL}Ig-VuCmY}-Nu7w6jyEa%lhKWqji5{9E0 z6@Ij0rOIPu~lxGC^bv+PS@y69Q7p^f zmBw9m>aUdxW7IPUzocM@qyLnSuzB?s6N>UXNxz(KppBtHyl&wO1EwyfuC&Qm$$yHy z-%VA=`<$x;zQHDK8k0}s$B_5ViKh}JN3X+{gJj(QhYq)SX-HEKv16C%mwj#H%?4V8 zbZxliI`JH+0V55gKDcmm!C5nM67lh7QiwYql|;!AGIE zMT9OOLv$|=!|)8!#w(EOX_xcQ4869bYCjF&jQ#t8Cy{tXk4Y6n}; zdH~A>#vnLJi~}YUdv;>ws}8z1CId>4*ktVhtukMo?8dyrOk+1rBLUiGVCp5yM{fHW z^!m2ULnC0oLvoXDSX^g+M3RBV>JKp3B0n9^$E?Mx#<0p#!IzD60C{@1hVrF~w2HmdJqrkZbZlw|+!6!A2Vx2)aL*M)QROQ;5kK z=d8TS#>KeqG@Qm3mA+QFr4D-cUFC&ZJ#O1F;NCsI?6~NIdZ8p&(7F=Sm5C%Us4_YT z0Y<(sJkjEt5^uT}O%%bY);cNS%S8{Z1c8~Pc}Hd;Iw6o2R6pd~L~{d08v1|+|nIv&-q8f{@&89AK?p_G`7>`r&m%UGJtua5zZE7 z2AsnUZW!z_x1@TIQOMey9j@tz`X(+(z~L);pcDi|NhI{tp?Af6v{3+h(9 z;plLkuuA-u@5vP^+(Pfs3}*%H8J@-r)3%5l_<6^T|J%D9E4=CCi+gaFfvAR_~F{kmHZpZ`C9&(}%- literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/Contents.json" new file mode 100644 index 0000000..73c0059 --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/Contents.json" @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_comment.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_comment.imageset/Contents.json" new file mode 100644 index 0000000..b63fc29 --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_comment.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_reels_comment.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_comment.imageset/ic_reels_comment.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_comment.imageset/ic_reels_comment.png" new file mode 100644 index 0000000000000000000000000000000000000000..cdadce0e35bc2a02398b9e4832950d3f60e5b4ce GIT binary patch literal 12247 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 literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_send.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_send.imageset/Contents.json" new file mode 100644 index 0000000..91f2f39 --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_send.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_reels_send.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_send.imageset/ic_reels_send.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_send.imageset/ic_reels_send.png" new file mode 100644 index 0000000000000000000000000000000000000000..25ffcde6231e556d621e5a3f37ef8b1ed9620a5b GIT binary patch literal 18159 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 B+nfLZ literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/Contents.json" new file mode 100644 index 0000000..73c0059 --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/Contents.json" @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_hamburger.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_hamburger.imageset/Contents.json" new file mode 100644 index 0000000..7787869 --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_hamburger.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_my_hamburger.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_hamburger.imageset/ic_my_hamburger.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_hamburger.imageset/ic_my_hamburger.png" new file mode 100644 index 0000000000000000000000000000000000000000..653738eeb98cd98424e1421a7644f80e41032a81 GIT binary patch literal 2793 zcmdUw`%_cb7RMI}FQJK|7AZ*SSSwmouoBSlQXV401%!kk5D20mB)r4`H$05?3ilR; z=}@JDfDDRo5(pF__<+QVma05#@c{CU53mOVjCe%CBlpDq3H{-YduGqxd(GZ^*39~T zzUx2!zMd2VQv(2?cpY>P03Z=t66os^%e(ZaDq_)2arI*86C+dq*uRPMs-%NQQUIv; zHB;-N71fLgesRhp_*7tG^r^JSlgA+~Ep2CP0yiZpGU@ov#FH^4(~hP92GL&b`x)t1 zlp{`gr5+|7im@`9dT{Efsu-~4*OYR9tnr9XMR%-ALEqsO%$9MUn2 zTC@3R)cN35hm&Y|XEzs9wnRR7o}4_gxn%3!o?UnPncbadPTS8%PTaadr1A+5j$?}Y zsFH$?#`*CR`58@(gQ*KqTCgFxnI4|SGOwdDW>_-zqXO&eFu+!d%4+JC{fxAPyRIPb zS=OvE-p!;+(s(iU09(M2$Ho-+xw#SLqK4?qWN1@$mNL zHe{mTr;Xu6OWg#V^-_ivdQY{e&q>4i35&#~)L#@Wr(z^Y_`+X zW{{uo_1oE|3j@3@sHBmq1?RE**Ho!}+dz2Oyp)j}Q%V98w5Sl1ivx*=suOh;*0TX_ zF<${ECA2Q}AEx2D0`9MoAWXh{q%!k>?jM2|3=-$2- zY~m$BGf!tX<>*1O2-3pf>oR6<9+KRj%F&NGh|OCB3lG0DdmB5ao(2g7o^Nq;RB6 zZ0z}&4*2xUqc3WoElmxRu3`uAefUHD3K0Ie_vvhpT7<8go2;wF7)>%&rNBm_Ly}*7 z>4P7_!}!lL4Ri_$x3$40`Wx*M4P13=@kktZ-Z3UA`*je)q(E7`RoR=j6O!~&2|h|LrayQLC|&^`^AF{ zRZ1Qng1h323&vs95L`W4J6-0x6FA!COg~mfpWhV;w8&KTw__h^DjdpdX-_+*99I#j zscbd!-jav9p?_fVV_h%bV@eHD&xS)<`;%l6WPw&_&Gfrast+a)cA$s&^()S#0d`jD zkcPWb-k#}*5GT?=QI`eS-7%+>>)Q1{QOpDvFM&L$K5WD>_Jgf_Vl{-e^U-yMIio8C zHw`g=%+{}ldj9Zk)^ga$o{!LE7mIvZtRDI*uqo~>%#&z=U5OVyC3Yhiq3^0`E9Dyi zhv8|7Dvl85c>sTPjczlw9^}=|#oX71cb`7_LLUZ(FNxTNIYnnii<3=&voSHql2uL) zKaoyNAA0i^ATYJ+b|amx?w4DmI9jpR!2z!6Ys zOLOYD<@-ywxFR}JDOsuhvXgrTlD^p+*c#x(D%~@7*6?#+5?+MiIa~IpU0(x2b>l1S z0`@FtXvJznNXyFF@dK%h46}qaqpXp`N}t03jY(DX(b?*;4U~wtSx=sfum1Zg{DglP z|6%vMb>FDJvi3b=K1j-P5XBd{7(h(ro z(q0pH8b6759Wb>27V2uk#buJZ+q~aF80!$)8r15)oZY6g;af09(nNP|6dHoby%@9? zZNS$}TEVo$S)5ROlBlrC*lc>%{D?zK8-bd07a{MGhevV$rkiw|BfD?43LG>@*-3+D zXu~t2xa_W%-e_QyB?%5Bdp8A`i)94_)De@-#HDCUv;4)PpaOV*d`?f;mEhTa<4hOr z)CY%4TU?<2^X)8qKT~H)J<-tmlGynDYZX!=%zty)*6|G)c;@q+Oc$1Y_eW^TCy-tg zZS*C<`H6QgpL0Rj6&H;Q^m7UHt&^zAbN8lUqP5xH$%ec zVOn-e$6a;yX}n0#TH*DCy-gd&)x2ARNY$7Pht@$p2sYn{<_Hds8ho-7GKDB;w`a>t5%|Ir~40(X7e< literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_invite.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_invite.imageset/Contents.json" new file mode 100644 index 0000000..cfcd8aa --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_invite.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_my_invite.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_invite.imageset/ic_my_invite.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_invite.imageset/ic_my_invite.png" new file mode 100644 index 0000000000000000000000000000000000000000..f656727d93fb7c02363555e12096beb8a6e9ceda GIT binary patch literal 13230 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

    -*b}~`RC$L8E#NI-7CPt(7FH?ol(zqi?ZR5 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 A-2eap literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_upload.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_upload.imageset/Contents.json" new file mode 100644 index 0000000..3efc94c --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_upload.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_my_upload.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_upload.imageset/ic_my_upload.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_upload.imageset/ic_my_upload.png" new file mode 100644 index 0000000000000000000000000000000000000000..a5252f5137d705f9a1a58f451470598217c3885b GIT binary patch literal 9855 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 literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/Contents.json" new file mode 100644 index 0000000..73c0059 --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/Contents.json" @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_black.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_black.imageset/Contents.json" new file mode 100644 index 0000000..d97cae5 --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_black.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_bookmark_black.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_black.imageset/ic_bookmark_black.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_black.imageset/ic_bookmark_black.png" new file mode 100644 index 0000000000000000000000000000000000000000..01ac1c8ccd7196c0fc9c2dedf0ea502c2b3cb31a GIT binary patch literal 5921 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 literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_homd_search_light.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_homd_search_light.imageset/Contents.json" new file mode 100644 index 0000000..447961b --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_homd_search_light.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_homd_search_light.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_homd_search_light.imageset/ic_homd_search_light.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_homd_search_light.imageset/ic_homd_search_light.png" new file mode 100644 index 0000000000000000000000000000000000000000..9461207603ed1cb2f52e3a0f1bccd194a49bb0ef GIT binary patch literal 14511 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 E1B35HAOHXW literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_comment.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_comment.imageset/Contents.json" new file mode 100644 index 0000000..9498248 --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_comment.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_home_comment.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_comment.imageset/ic_home_comment.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_comment.imageset/ic_home_comment.png" new file mode 100644 index 0000000000000000000000000000000000000000..550ae9deceaec25f05c40f67b1fbcba0dc01e6eb GIT binary patch literal 10913 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 D6XIx) literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart_full.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart_full.imageset/Contents.json" new file mode 100644 index 0000000..6d330ee --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart_full.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_home_heart_full.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart_full.imageset/ic_home_heart_full.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart_full.imageset/ic_home_heart_full.png" new file mode 100644 index 0000000000000000000000000000000000000000..64f702def40a1d37cbe1672bbf8e3a7f6abbf0fb GIT binary patch literal 11641 zcmds7g+ zK34d(67+Qoe*NY559<0p_zB(T^aTF?+vlpK9{?Pm7=MU(DGpxv@C5pb1=_^h1s!

    +Kingfisher +

    + +

    + + + + + +
    + + +

    + +Kingfisher is a powerful, pure-Swift library for downloading and caching images from the web. It provides you a chance to use a pure-Swift way to work with remote images in your next app. + +## Features + +- [x] Asynchronous image downloading and caching. +- [x] Loading image from either `URLSession`-based networking or local provided data. +- [x] Useful image processors and filters provided. +- [x] Multiple-layer hybrid cache for both memory and disk. +- [x] Fine control on cache behavior. Customizable expiration date and size limit. +- [x] Cancelable downloading and auto-reusing previous downloaded content to improve performance. +- [x] Independent components. Use the downloader, caching system, and image processors separately as you need. +- [x] Prefetching images and showing them from the cache to boost your app. +- [x] View extensions for `UIImageView`, `NSImageView`, `NSButton` and `UIButton` to directly set an image from a URL. +- [x] Built-in transition animation when setting images. +- [x] Customizable placeholder and indicator while loading images. +- [x] Extensible image processing and image format easily. +- [x] Low Data Mode support. +- [x] SwiftUI support. + +### Kingfisher 101 + +The simplest use-case is setting an image to an image view with the `UIImageView` extension: + +```swift +import Kingfisher + +let url = URL(string: "https://example.com/image.png") +imageView.kf.setImage(with: url) +``` + +Kingfisher will download the image from `url`, send it to both memory cache and disk cache, and display it in `imageView`. +When you set with the same URL later, the image will be retrieved from the cache and shown immediately. + +It also works if you use SwiftUI: + +```swift +var body: some View { + KFImage(URL(string: "https://example.com/image.png")!) +} +``` + +### A More Advanced Example + +With the powerful options, you can do hard tasks with Kingfisher in a simple way. For example, the code below: + +1. Downloads a high-resolution image. +2. Downsamples it to match the image view size. +3. Makes it round cornered with a given radius. +4. Shows a system indicator and a placeholder image while downloading. +5. When prepared, it animates the small thumbnail image with a "fade in" effect. +6. The original large image is also cached to disk for later use, to get rid of downloading it again in a detail view. +7. A console log is printed when the task finishes, either for success or failure. + +```swift +let url = URL(string: "https://example.com/high_resolution_image.png") +let processor = DownsamplingImageProcessor(size: imageView.bounds.size) + |> RoundCornerImageProcessor(cornerRadius: 20) +imageView.kf.indicatorType = .activity +imageView.kf.setImage( + with: url, + placeholder: UIImage(named: "placeholderImage"), + options: [ + .processor(processor), + .scaleFactor(UIScreen.main.scale), + .transition(.fade(1)), + .cacheOriginalImage + ]) +{ + result in + switch result { + case .success(let value): + print("Task done for: \(value.source.url?.absoluteString ?? "")") + case .failure(let error): + print("Job failed: \(error.localizedDescription)") + } +} +``` + +It is a common situation I can meet in my daily work. Think about how many lines you need to write without +Kingfisher! + +### Method Chaining + +If you are not a fan of the `kf` extension, you can also prefer to use the `KF` builder and chained the method +invocations. The code below is doing the same thing: + +```swift +// Use `kf` extension +imageView.kf.setImage( + with: url, + placeholder: placeholderImage, + options: [ + .processor(processor), + .loadDiskFileSynchronously, + .cacheOriginalImage, + .transition(.fade(0.25)), + .lowDataMode(.network(lowResolutionURL)) + ], + progressBlock: { receivedSize, totalSize in + // Progress updated + }, + completionHandler: { result in + // Done + } +) + +// Use `KF` builder +KF.url(url) + .placeholder(placeholderImage) + .setProcessor(processor) + .loadDiskFileSynchronously() + .cacheMemoryOnly() + .fade(duration: 0.25) + .lowDataModeSource(.network(lowResolutionURL)) + .onProgress { receivedSize, totalSize in } + .onSuccess { result in } + .onFailure { error in } + .set(to: imageView) +``` + +And even better, if later you want to switch to SwiftUI, just change the `KF` above to `KFImage`, and you've done: + +```swift +struct ContentView: View { + var body: some View { + KFImage.url(url) + .placeholder(placeholderImage) + .setProcessor(processor) + .loadDiskFileSynchronously() + .cacheMemoryOnly() + .fade(duration: 0.25) + .lowDataModeSource(.network(lowResolutionURL)) + .onProgress { receivedSize, totalSize in } + .onSuccess { result in } + .onFailure { error in } + } +} +``` + +### Learn More + +To learn the use of Kingfisher by more examples, take a look at the well-prepared [Cheat Sheet](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet). +There we summarized the most common tasks in Kingfisher, you can get a better idea of what this framework can do. +There are also some performance tips, remember to check them too. + +## Requirements + +- iOS 12.0+ / macOS 10.14+ / tvOS 12.0+ / watchOS 5.0+ (if you use only UIKit/AppKit) +- iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+ (if you use it in SwiftUI) +- Swift 5.0+ + +> If you need to support from iOS 10 (UIKit/AppKit) or iOS 13 (SwiftUI), use Kingfisher version 6.x. But it won't work +> with Xcode 13.0 and Xcode 13.1 [#1802](https://github.com/onevcat/Kingfisher/issues/1802). +> +> If you need to use Xcode 13.0 and 13.1 but cannot upgrade to v7, use the `version6-xcode13` branch. However, you have to drop +> iOS 10 support due to another Xcode 13 bug. +> +> | UIKit | SwiftUI | Xcode | Kingfisher | +> |---|---|---|---| +> | iOS 10+ | iOS 13+ | 12 | ~> 6.3.1 | +> | iOS 11+ | iOS 13+ | 13 | `version6-xcode13` | +> | iOS 12+ | iOS 14+ | 13 | ~> 7.0 | + +### Installation + +A detailed guide for installation can be found in [Installation Guide](https://github.com/onevcat/Kingfisher/wiki/Installation-Guide). + +#### Swift Package Manager + +- File > Swift Packages > Add Package Dependency +- Add `https://github.com/onevcat/Kingfisher.git` +- Select "Up to Next Major" with "7.0.0" + +#### CocoaPods + +```ruby +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '12.0' +use_frameworks! + +target 'MyApp' do + pod 'Kingfisher', '~> 7.0' +end +``` + +#### Carthage + +``` +github "onevcat/Kingfisher" ~> 7.0 +``` + + +### Migrating + +[Kingfisher 7.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-7.0-Migration-Guide) - Kingfisher 7.x is NOT fully compatible with the previous version. However, changes should be trivial or not required at all. Please follow the [migration guide](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-7.0-Migration-Guide) when you prepare to upgrade Kingfisher in your project. + +If you are using an even earlier version, see the guides below to know the steps for migrating. + +> - [Kingfisher 6.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-6.0-Migration-Guide) - Kingfisher 6.x is NOT fully compatible with the previous version. However, the migration is not difficult. Depending on your use cases, it may take no effect or several minutes to modify your existing code for the new version. Please follow the [migration guide](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-6.0-Migration-Guide) when you prepare to upgrade Kingfisher in your project. +> - [Kingfisher 5.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-5.0-Migration-Guide) - If you are upgrading to Kingfisher 5.x from 4.x, please read this for more information. +> - Kingfisher 4.0 Migration - Kingfisher 3.x should be source compatible to Kingfisher 4. The reason for a major update is that we need to specify the Swift version explicitly for Xcode. All deprecated methods in Kingfisher 3 were removed, so please ensure you have no warning left before you migrate from Kingfisher 3 to Kingfisher 4. If you have any trouble when migrating, please open an issue to discuss. +> - [Kingfisher 3.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-3.0-Migration-Guide) - If you are upgrading to Kingfisher 3.x from an earlier version, please read this for more information. + +## Next Steps + +We prepared a [wiki page](https://github.com/onevcat/Kingfisher/wiki). You can find tons of useful things there. + +* [Installation Guide](https://github.com/onevcat/Kingfisher/wiki/Installation-Guide) - Follow it to integrate Kingfisher into your project. +* [Cheat Sheet](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet)- Curious about what Kingfisher could do and how would it look like when used in your project? See this page for useful code snippets. If you are already familiar with Kingfisher, you could also learn new tricks to improve the way you use Kingfisher! +* [API Reference](https://kingfisher.onevcat.com/) - Lastly, please remember to read the full API reference whenever you need more detailed documentation. + +## Other + +### Future of Kingfisher + +I want to keep Kingfisher lightweight. This framework focuses on providing a simple solution for downloading and caching images. This doesn’t mean the framework can’t be improved. Kingfisher is far from perfect, so necessary and useful updates will be made to make it better. + +### Developments and Tests + +Any contributing and pull requests are warmly welcome. However, before you plan to implement some features or try to fix an uncertain issue, it is recommended to open a discussion first. It would be appreciated if your pull requests could build and with all tests green. :) + +### About the logo + +The logo of Kingfisher is inspired by [Tangram (七巧板)](http://en.wikipedia.org/wiki/Tangram), a dissection puzzle consisting of seven flat shapes from China. I believe she's a kingfisher bird instead of a swift, but someone insists that she is a pigeon. I guess I should give her a name. Hi, guys, do you have any suggestions? + +### Contact + +Follow and contact me on [Twitter](http://twitter.com/onevcat) or [Sina Weibo](http://weibo.com/onevcat). If you find an issue, [open a ticket](https://github.com/onevcat/Kingfisher/issues/new). Pull requests are warmly welcome as well. + +## Backers & Sponsors + +Open-source projects cannot live long without your help. If you find Kingfisher is useful, please consider supporting this +project by becoming a sponsor. Your user icon or company logo shows up [on my blog](https://onevcat.com/tabs/about/) with a link to your home page. + +Become a sponsor through [GitHub Sponsors](https://github.com/sponsors/onevcat). :heart: + +Special thanks to: + +[![imgly](https://user-images.githubusercontent.com/1812216/106253726-271ed000-6218-11eb-98e0-c9c681925770.png)](https://img.ly/) + +### License + +Kingfisher is released under the MIT license. See LICENSE for details. diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift new file mode 100644 index 0000000..8cb09f2 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift @@ -0,0 +1,117 @@ +// +// CacheSerializer.swift +// Kingfisher +// +// Created by Wei Wang on 2016/09/02. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CoreGraphics + +/// An `CacheSerializer` is used to convert some data to an image object after +/// retrieving it from disk storage, and vice versa, to convert an image to data object +/// for storing to the disk storage. +public protocol CacheSerializer { + + /// Gets the serialized data from a provided image + /// and optional original data for caching to disk. + /// + /// - Parameters: + /// - image: The image needed to be serialized. + /// - original: The original data which is just downloaded. + /// If the image is retrieved from cache instead of + /// downloaded, it will be `nil`. + /// - Returns: The data object for storing to disk, or `nil` when no valid + /// data could be serialized. + func data(with image: KFCrossPlatformImage, original: Data?) -> Data? + + /// Gets an image from provided serialized data. + /// + /// - Parameters: + /// - data: The data from which an image should be deserialized. + /// - options: The parsed options for deserialization. + /// - Returns: An image deserialized or `nil` when no valid image + /// could be deserialized. + func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? +} + +/// Represents a basic and default `CacheSerializer` used in Kingfisher disk cache system. +/// It could serialize and deserialize images in PNG, JPEG and GIF format. For +/// image other than these formats, a normalized `pngRepresentation` will be used. +public struct DefaultCacheSerializer: CacheSerializer { + + /// The default general cache serializer used across Kingfisher's cache. + public static let `default` = DefaultCacheSerializer() + + /// The compression quality when converting image to a lossy format data. Default is 1.0. + public var compressionQuality: CGFloat = 1.0 + + /// Whether the original data should be preferred when serializing the image. + /// If `true`, the input original data will be checked first and used unless the data is `nil`. + /// In that case, the serialization will fall back to creating data from image. + public var preferCacheOriginalData: Bool = false + + /// Creates a cache serializer that serialize and deserialize images in PNG, JPEG and GIF format. + /// + /// - Note: + /// Use `DefaultCacheSerializer.default` unless you need to specify your own properties. + /// + public init() { } + + /// - Parameters: + /// - image: The image needed to be serialized. + /// - original: The original data which is just downloaded. + /// If the image is retrieved from cache instead of + /// downloaded, it will be `nil`. + /// - Returns: The data object for storing to disk, or `nil` when no valid + /// data could be serialized. + /// + /// - Note: + /// Only when `original` contains valid PNG, JPEG and GIF format data, the `image` will be + /// converted to the corresponding data type. Otherwise, if the `original` is provided but it is not + /// If `original` is `nil`, the input `image` will be encoded as PNG data. + public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? { + if preferCacheOriginalData { + return original ?? + image.kf.data( + format: original?.kf.imageFormat ?? .unknown, + compressionQuality: compressionQuality + ) + } else { + return image.kf.data( + format: original?.kf.imageFormat ?? .unknown, + compressionQuality: compressionQuality + ) + } + } + + /// Gets an image deserialized from provided data. + /// + /// - Parameters: + /// - data: The data from which an image should be deserialized. + /// - options: Options for deserialization. + /// - Returns: An image deserialized or `nil` when no valid image + /// could be deserialized. + public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/DiskStorage.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/DiskStorage.swift new file mode 100644 index 0000000..debe7f4 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/DiskStorage.swift @@ -0,0 +1,586 @@ +// +// DiskStorage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + + +/// Represents a set of conception related to storage which stores a certain type of value in disk. +/// This is a namespace for the disk storage types. A `Backend` with a certain `Config` will be used to describe the +/// storage. See these composed types for more information. +public enum DiskStorage { + + /// Represents a storage back-end for the `DiskStorage`. The value is serialized to data + /// and stored as file in the file system under a specified location. + /// + /// You can config a `DiskStorage.Backend` in its initializer by passing a `DiskStorage.Config` value. + /// or modifying the `config` property after it being created. `DiskStorage` will use file's attributes to keep + /// track of a file for its expiration or size limitation. + public class Backend { + /// The config used for this disk storage. + public var config: Config + + // The final storage URL on disk, with `name` and `cachePathBlock` considered. + public let directoryURL: URL + + let metaChangingQueue: DispatchQueue + + var maybeCached : Set? + let maybeCachedCheckingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.maybeCachedCheckingQueue") + + // `false` if the storage initialized with an error. This prevents unexpected forcibly crash when creating + // storage in the default cache. + private var storageReady: Bool = true + + /// Creates a disk storage with the given `DiskStorage.Config`. + /// + /// - Parameter config: The config used for this disk storage. + /// - Throws: An error if the folder for storage cannot be got or created. + public convenience init(config: Config) throws { + self.init(noThrowConfig: config, creatingDirectory: false) + try prepareDirectory() + } + + // If `creatingDirectory` is `false`, the directory preparation will be skipped. + // We need to call `prepareDirectory` manually after this returns. + init(noThrowConfig config: Config, creatingDirectory: Bool) { + var config = config + + let creation = Creation(config) + self.directoryURL = creation.directoryURL + + // Break any possible retain cycle set by outside. + config.cachePathBlock = nil + self.config = config + + metaChangingQueue = DispatchQueue(label: creation.cacheName) + setupCacheChecking() + + if creatingDirectory { + try? prepareDirectory() + } + } + + private func setupCacheChecking() { + maybeCachedCheckingQueue.async { + do { + self.maybeCached = Set() + try self.config.fileManager.contentsOfDirectory(atPath: self.directoryURL.path).forEach { fileName in + self.maybeCached?.insert(fileName) + } + } catch { + // Just disable the functionality if we fail to initialize it properly. This will just revert to + // the behavior which is to check file existence on disk directly. + self.maybeCached = nil + } + } + } + + // Creates the storage folder. + private func prepareDirectory() throws { + let fileManager = config.fileManager + let path = directoryURL.path + + guard !fileManager.fileExists(atPath: path) else { return } + + do { + try fileManager.createDirectory( + atPath: path, + withIntermediateDirectories: true, + attributes: nil) + } catch { + self.storageReady = false + throw KingfisherError.cacheError(reason: .cannotCreateDirectory(path: path, error: error)) + } + } + + /// Stores a value to the storage under the specified key and expiration policy. + /// - Parameters: + /// - value: The value to be stored. + /// - key: The key to which the `value` will be stored. If there is already a value under the key, + /// the old value will be overwritten by `value`. + /// - expiration: The expiration policy used by this store action. + /// - writeOptions: Data writing options used the new files. + /// - Throws: An error during converting the value to a data format or during writing it to disk. + public func store( + value: T, + forKey key: String, + expiration: StorageExpiration? = nil, + writeOptions: Data.WritingOptions = []) throws + { + guard storageReady else { + throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL)) + } + + let expiration = expiration ?? config.expiration + // The expiration indicates that already expired, no need to store. + guard !expiration.isExpired else { return } + + let data: Data + do { + data = try value.toData() + } catch { + throw KingfisherError.cacheError(reason: .cannotConvertToData(object: value, error: error)) + } + + let fileURL = cacheFileURL(forKey: key) + do { + try data.write(to: fileURL, options: writeOptions) + } catch { + throw KingfisherError.cacheError( + reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error) + ) + } + + let now = Date() + let attributes: [FileAttributeKey : Any] = [ + // The last access date. + .creationDate: now.fileAttributeDate, + // The estimated expiration date. + .modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate + ] + do { + try config.fileManager.setAttributes(attributes, ofItemAtPath: fileURL.path) + } catch { + try? config.fileManager.removeItem(at: fileURL) + throw KingfisherError.cacheError( + reason: .cannotSetCacheFileAttribute( + filePath: fileURL.path, + attributes: attributes, + error: error + ) + ) + } + + maybeCachedCheckingQueue.async { + self.maybeCached?.insert(fileURL.lastPathComponent) + } + } + + /// Gets a value from the storage. + /// - Parameters: + /// - key: The cache key of value. + /// - extendingExpiration: The expiration policy used by this getting action. + /// - Throws: An error during converting the data to a value or during operation of disk files. + /// - Returns: The value under `key` if it is valid and found in the storage. Otherwise, `nil`. + public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) throws -> T? { + return try value(forKey: key, referenceDate: Date(), actuallyLoad: true, extendingExpiration: extendingExpiration) + } + + func value( + forKey key: String, + referenceDate: Date, + actuallyLoad: Bool, + extendingExpiration: ExpirationExtending) throws -> T? + { + guard storageReady else { + throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL)) + } + + let fileManager = config.fileManager + let fileURL = cacheFileURL(forKey: key) + let filePath = fileURL.path + + let fileMaybeCached = maybeCachedCheckingQueue.sync { + return maybeCached?.contains(fileURL.lastPathComponent) ?? true + } + guard fileMaybeCached else { + return nil + } + guard fileManager.fileExists(atPath: filePath) else { + return nil + } + + let meta: FileMeta + do { + let resourceKeys: Set = [.contentModificationDateKey, .creationDateKey] + meta = try FileMeta(fileURL: fileURL, resourceKeys: resourceKeys) + } catch { + throw KingfisherError.cacheError( + reason: .invalidURLResource(error: error, key: key, url: fileURL)) + } + + if meta.expired(referenceDate: referenceDate) { + return nil + } + if !actuallyLoad { return T.empty } + + do { + let data = try Data(contentsOf: fileURL) + let obj = try T.fromData(data) + metaChangingQueue.async { + meta.extendExpiration(with: fileManager, extendingExpiration: extendingExpiration) + } + return obj + } catch { + throw KingfisherError.cacheError(reason: .cannotLoadDataFromDisk(url: fileURL, error: error)) + } + } + + /// Whether there is valid cached data under a given key. + /// - Parameter key: The cache key of value. + /// - Returns: If there is valid data under the key, `true`. Otherwise, `false`. + /// + /// - Note: + /// This method does not actually load the data from disk, so it is faster than directly loading the cached value + /// by checking the nullability of `value(forKey:extendingExpiration:)` method. + /// + public func isCached(forKey key: String) -> Bool { + return isCached(forKey: key, referenceDate: Date()) + } + + /// Whether there is valid cached data under a given key and a reference date. + /// - Parameters: + /// - key: The cache key of value. + /// - referenceDate: A reference date to check whether the cache is still valid. + /// - Returns: If there is valid data under the key, `true`. Otherwise, `false`. + /// + /// - Note: + /// If you pass `Date()` to `referenceDate`, this method is identical to `isCached(forKey:)`. Use the + /// `referenceDate` to determine whether the cache is still valid for a future date. + public func isCached(forKey key: String, referenceDate: Date) -> Bool { + do { + let result = try value( + forKey: key, + referenceDate: referenceDate, + actuallyLoad: false, + extendingExpiration: .none + ) + return result != nil + } catch { + return false + } + } + + /// Removes a value from a specified key. + /// - Parameter key: The cache key of value. + /// - Throws: An error during removing the value. + public func remove(forKey key: String) throws { + let fileURL = cacheFileURL(forKey: key) + try removeFile(at: fileURL) + } + + func removeFile(at url: URL) throws { + try config.fileManager.removeItem(at: url) + } + + /// Removes all values in this storage. + /// - Throws: An error during removing the values. + public func removeAll() throws { + try removeAll(skipCreatingDirectory: false) + } + + func removeAll(skipCreatingDirectory: Bool) throws { + try config.fileManager.removeItem(at: directoryURL) + if !skipCreatingDirectory { + try prepareDirectory() + } + } + + /// The URL of the cached file with a given computed `key`. + /// + /// - Parameter key: The final computed key used when caching the image. Please note that usually this is not + /// the `cacheKey` of an image `Source`. It is the computed key with processor identifier considered. + /// + /// - Note: + /// This method does not guarantee there is an image already cached in the returned URL. It just gives your + /// the URL that the image should be if it exists in disk storage, with the give key. + /// + public func cacheFileURL(forKey key: String) -> URL { + let fileName = cacheFileName(forKey: key) + return directoryURL.appendingPathComponent(fileName, isDirectory: false) + } + + func cacheFileName(forKey key: String) -> String { + if config.usesHashedFileName { + let hashedKey = key.kf.md5 + if let ext = config.pathExtension { + return "\(hashedKey).\(ext)" + } else if config.autoExtAfterHashedFileName, + let ext = key.kf.ext { + return "\(hashedKey).\(ext)" + } + return hashedKey + } else { + if let ext = config.pathExtension { + return "\(key).\(ext)" + } + return key + } + } + + func allFileURLs(for propertyKeys: [URLResourceKey]) throws -> [URL] { + let fileManager = config.fileManager + + guard let directoryEnumerator = fileManager.enumerator( + at: directoryURL, includingPropertiesForKeys: propertyKeys, options: .skipsHiddenFiles) else + { + throw KingfisherError.cacheError(reason: .fileEnumeratorCreationFailed(url: directoryURL)) + } + + guard let urls = directoryEnumerator.allObjects as? [URL] else { + throw KingfisherError.cacheError(reason: .invalidFileEnumeratorContent(url: directoryURL)) + } + return urls + } + + /// Removes all expired values from this storage. + /// - Throws: A file manager error during removing the file. + /// - Returns: The URLs for removed files. + public func removeExpiredValues() throws -> [URL] { + return try removeExpiredValues(referenceDate: Date()) + } + + func removeExpiredValues(referenceDate: Date) throws -> [URL] { + let propertyKeys: [URLResourceKey] = [ + .isDirectoryKey, + .contentModificationDateKey + ] + + let urls = try allFileURLs(for: propertyKeys) + let keys = Set(propertyKeys) + let expiredFiles = urls.filter { fileURL in + do { + let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys) + if meta.isDirectory { + return false + } + return meta.expired(referenceDate: referenceDate) + } catch { + return true + } + } + try expiredFiles.forEach { url in + try removeFile(at: url) + } + return expiredFiles + } + + /// Removes all size exceeded values from this storage. + /// - Throws: A file manager error during removing the file. + /// - Returns: The URLs for removed files. + /// + /// - Note: This method checks `config.sizeLimit` and remove cached files in an LRU (Least Recently Used) way. + func removeSizeExceededValues() throws -> [URL] { + + if config.sizeLimit == 0 { return [] } // Back compatible. 0 means no limit. + + var size = try totalSize() + if size < config.sizeLimit { return [] } + + let propertyKeys: [URLResourceKey] = [ + .isDirectoryKey, + .creationDateKey, + .fileSizeKey + ] + let keys = Set(propertyKeys) + + let urls = try allFileURLs(for: propertyKeys) + var pendings: [FileMeta] = urls.compactMap { fileURL in + guard let meta = try? FileMeta(fileURL: fileURL, resourceKeys: keys) else { + return nil + } + return meta + } + // Sort by last access date. Most recent file first. + pendings.sort(by: FileMeta.lastAccessDate) + + var removed: [URL] = [] + let target = config.sizeLimit / 2 + while size > target, let meta = pendings.popLast() { + size -= UInt(meta.fileSize) + try removeFile(at: meta.url) + removed.append(meta.url) + } + return removed + } + + /// Gets the total file size of the folder in bytes. + public func totalSize() throws -> UInt { + let propertyKeys: [URLResourceKey] = [.fileSizeKey] + let urls = try allFileURLs(for: propertyKeys) + let keys = Set(propertyKeys) + let totalSize: UInt = urls.reduce(0) { size, fileURL in + do { + let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys) + return size + UInt(meta.fileSize) + } catch { + return size + } + } + return totalSize + } + } +} + +extension DiskStorage { + /// Represents the config used in a `DiskStorage`. + public struct Config { + + /// The file size limit on disk of the storage in bytes. 0 means no limit. + public var sizeLimit: UInt + + /// The `StorageExpiration` used in this disk storage. Default is `.days(7)`, + /// means that the disk cache would expire in one week. + public var expiration: StorageExpiration = .days(7) + + /// The preferred extension of cache item. It will be appended to the file name as its extension. + /// Default is `nil`, means that the cache file does not contain a file extension. + public var pathExtension: String? = nil + + /// Default is `true`, means that the cache file name will be hashed before storing. + public var usesHashedFileName = true + + /// Default is `false` + /// If set to `true`, image extension will be extracted from original file name and append to + /// the hased file name and used as the cache key on disk. + public var autoExtAfterHashedFileName = false + + let name: String + let fileManager: FileManager + let directory: URL? + + var cachePathBlock: ((_ directory: URL, _ cacheName: String) -> URL)! = { + (directory, cacheName) in + return directory.appendingPathComponent(cacheName, isDirectory: true) + } + + /// Creates a config value based on given parameters. + /// + /// - Parameters: + /// - name: The name of cache. It is used as a part of storage folder. It is used to identify the disk + /// storage. Two storages with the same `name` would share the same folder in disk, and it should + /// be prevented. + /// - sizeLimit: The size limit in bytes for all existing files in the disk storage. + /// - fileManager: The `FileManager` used to manipulate files on disk. Default is `FileManager.default`. + /// - directory: The URL where the disk storage should live. The storage will use this as the root folder, + /// and append a path which is constructed by input `name`. Default is `nil`, indicates that + /// the cache directory under user domain mask will be used. + public init( + name: String, + sizeLimit: UInt, + fileManager: FileManager = .default, + directory: URL? = nil) + { + self.name = name + self.fileManager = fileManager + self.directory = directory + self.sizeLimit = sizeLimit + } + } +} + +extension DiskStorage { + struct FileMeta { + + let url: URL + + let lastAccessDate: Date? + let estimatedExpirationDate: Date? + let isDirectory: Bool + let fileSize: Int + + static func lastAccessDate(lhs: FileMeta, rhs: FileMeta) -> Bool { + return lhs.lastAccessDate ?? .distantPast > rhs.lastAccessDate ?? .distantPast + } + + init(fileURL: URL, resourceKeys: Set) throws { + let meta = try fileURL.resourceValues(forKeys: resourceKeys) + self.init( + fileURL: fileURL, + lastAccessDate: meta.creationDate, + estimatedExpirationDate: meta.contentModificationDate, + isDirectory: meta.isDirectory ?? false, + fileSize: meta.fileSize ?? 0) + } + + init( + fileURL: URL, + lastAccessDate: Date?, + estimatedExpirationDate: Date?, + isDirectory: Bool, + fileSize: Int) + { + self.url = fileURL + self.lastAccessDate = lastAccessDate + self.estimatedExpirationDate = estimatedExpirationDate + self.isDirectory = isDirectory + self.fileSize = fileSize + } + + func expired(referenceDate: Date) -> Bool { + return estimatedExpirationDate?.isPast(referenceDate: referenceDate) ?? true + } + + func extendExpiration(with fileManager: FileManager, extendingExpiration: ExpirationExtending) { + guard let lastAccessDate = lastAccessDate, + let lastEstimatedExpiration = estimatedExpirationDate else + { + return + } + + let attributes: [FileAttributeKey : Any] + + switch extendingExpiration { + case .none: + // not extending expiration time here + return + case .cacheTime: + let originalExpiration: StorageExpiration = + .seconds(lastEstimatedExpiration.timeIntervalSince(lastAccessDate)) + attributes = [ + .creationDate: Date().fileAttributeDate, + .modificationDate: originalExpiration.estimatedExpirationSinceNow.fileAttributeDate + ] + case .expirationTime(let expirationTime): + attributes = [ + .creationDate: Date().fileAttributeDate, + .modificationDate: expirationTime.estimatedExpirationSinceNow.fileAttributeDate + ] + } + + try? fileManager.setAttributes(attributes, ofItemAtPath: url.path) + } + } +} + +extension DiskStorage { + struct Creation { + let directoryURL: URL + let cacheName: String + + init(_ config: Config) { + let url: URL + if let directory = config.directory { + url = directory + } else { + url = config.fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] + } + + cacheName = "com.onevcat.Kingfisher.ImageCache.\(config.name)" + directoryURL = config.cachePathBlock(url, cacheName) + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift new file mode 100644 index 0000000..cdfb7c3 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift @@ -0,0 +1,118 @@ +// +// RequestModifier.swift +// Kingfisher +// +// Created by Junyu Kuang on 5/28/17. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CoreGraphics + +/// `FormatIndicatedCacheSerializer` lets you indicate an image format for serialized caches. +/// +/// It could serialize and deserialize PNG, JPEG and GIF images. For +/// image other than these formats, a normalized `pngRepresentation` will be used. +/// +/// Example: +/// ```` +/// let profileImageSize = CGSize(width: 44, height: 44) +/// +/// // A round corner image. +/// let imageProcessor = RoundCornerImageProcessor( +/// cornerRadius: profileImageSize.width / 2, targetSize: profileImageSize) +/// +/// let optionsInfo: KingfisherOptionsInfo = [ +/// .cacheSerializer(FormatIndicatedCacheSerializer.png), +/// .processor(imageProcessor)] +/// +/// A URL pointing to a JPEG image. +/// let url = URL(string: "https://example.com/image.jpg")! +/// +/// // Image will be always cached as PNG format to preserve alpha channel for round rectangle. +/// // So when you load it from cache again later, it will be still round cornered. +/// // Otherwise, the corner part would be filled by white color (since JPEG does not contain an alpha channel). +/// imageView.kf.setImage(with: url, options: optionsInfo) +/// ```` +public struct FormatIndicatedCacheSerializer: CacheSerializer { + + /// A `FormatIndicatedCacheSerializer` which converts image from and to PNG format. If the image cannot be + /// represented by PNG format, it will fallback to its real format which is determined by `original` data. + public static let png = FormatIndicatedCacheSerializer(imageFormat: .PNG, jpegCompressionQuality: nil) + + /// A `FormatIndicatedCacheSerializer` which converts image from and to JPEG format. If the image cannot be + /// represented by JPEG format, it will fallback to its real format which is determined by `original` data. + /// The compression quality is 1.0 when using this serializer. If you need to set a customized compression quality, + /// use `jpeg(compressionQuality:)`. + public static let jpeg = FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: 1.0) + + /// A `FormatIndicatedCacheSerializer` which converts image from and to JPEG format with a settable compression + /// quality. If the image cannot be represented by JPEG format, it will fallback to its real format which is + /// determined by `original` data. + /// - Parameter compressionQuality: The compression quality when converting image to JPEG data. + public static func jpeg(compressionQuality: CGFloat) -> FormatIndicatedCacheSerializer { + return FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: compressionQuality) + } + + /// A `FormatIndicatedCacheSerializer` which converts image from and to GIF format. If the image cannot be + /// represented by GIF format, it will fallback to its real format which is determined by `original` data. + public static let gif = FormatIndicatedCacheSerializer(imageFormat: .GIF, jpegCompressionQuality: nil) + + /// The indicated image format. + private let imageFormat: ImageFormat + + /// The compression quality used for loss image format (like JPEG). + private let jpegCompressionQuality: CGFloat? + + /// Creates data which represents the given `image` under a format. + public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? { + + func imageData(withFormat imageFormat: ImageFormat) -> Data? { + return autoreleasepool { () -> Data? in + switch imageFormat { + case .PNG: return image.kf.pngRepresentation() + case .JPEG: return image.kf.jpegRepresentation(compressionQuality: jpegCompressionQuality ?? 1.0) + case .GIF: return image.kf.gifRepresentation() + case .unknown: return nil + } + } + } + + // generate data with indicated image format + if let data = imageData(withFormat: imageFormat) { + return data + } + + let originalFormat = original?.kf.imageFormat ?? .unknown + + // generate data with original image's format + if originalFormat != imageFormat, let data = imageData(withFormat: originalFormat) { + return data + } + + return original ?? image.kf.normalized.kf.pngRepresentation() + } + + /// Same implementation as `DefaultCacheSerializer`. + public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/ImageCache.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/ImageCache.swift new file mode 100644 index 0000000..b73100a --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/ImageCache.swift @@ -0,0 +1,875 @@ +// +// ImageCache.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +extension Notification.Name { + /// This notification will be sent when the disk cache got cleaned either there are cached files expired or the + /// total size exceeding the max allowed size. The manually invoking of `clearDiskCache` method will not trigger + /// this notification. + /// + /// The `object` of this notification is the `ImageCache` object which sends the notification. + /// A list of removed hashes (files) could be retrieved by accessing the array under + /// `KingfisherDiskCacheCleanedHashKey` key in `userInfo` of the notification object you received. + /// By checking the array, you could know the hash codes of files are removed. + public static let KingfisherDidCleanDiskCache = + Notification.Name("com.onevcat.Kingfisher.KingfisherDidCleanDiskCache") +} + +/// Key for array of cleaned hashes in `userInfo` of `KingfisherDidCleanDiskCacheNotification`. +public let KingfisherDiskCacheCleanedHashKey = "com.onevcat.Kingfisher.cleanedHash" + +/// Cache type of a cached image. +/// - none: The image is not cached yet when retrieving it. +/// - memory: The image is cached in memory. +/// - disk: The image is cached in disk. +public enum CacheType { + /// The image is not cached yet when retrieving it. + case none + /// The image is cached in memory. + case memory + /// The image is cached in disk. + case disk + + /// Whether the cache type represents the image is already cached or not. + public var cached: Bool { + switch self { + case .memory, .disk: return true + case .none: return false + } + } +} + +/// Represents the caching operation result. +public struct CacheStoreResult { + + /// The cache result for memory cache. Caching an image to memory will never fail. + public let memoryCacheResult: Result<(), Never> + + /// The cache result for disk cache. If an error happens during caching operation, + /// you can get it from `.failure` case of this `diskCacheResult`. + public let diskCacheResult: Result<(), KingfisherError> +} + +extension KFCrossPlatformImage: CacheCostCalculable { + /// Cost of an image + public var cacheCost: Int { return kf.cost } +} + +extension Data: DataTransformable { + public func toData() throws -> Data { + return self + } + + public static func fromData(_ data: Data) throws -> Data { + return data + } + + public static let empty = Data() +} + + +/// Represents the getting image operation from the cache. +/// +/// - disk: The image can be retrieved from disk cache. +/// - memory: The image can be retrieved memory cache. +/// - none: The image does not exist in the cache. +public enum ImageCacheResult { + + /// The image can be retrieved from disk cache. + case disk(KFCrossPlatformImage) + + /// The image can be retrieved memory cache. + case memory(KFCrossPlatformImage) + + /// The image does not exist in the cache. + case none + + /// Extracts the image from cache result. It returns the associated `Image` value for + /// `.disk` and `.memory` case. For `.none` case, `nil` is returned. + public var image: KFCrossPlatformImage? { + switch self { + case .disk(let image): return image + case .memory(let image): return image + case .none: return nil + } + } + + /// Returns the corresponding `CacheType` value based on the result type of `self`. + public var cacheType: CacheType { + switch self { + case .disk: return .disk + case .memory: return .memory + case .none: return .none + } + } +} + +/// Represents a hybrid caching system which is composed by a `MemoryStorage.Backend` and a `DiskStorage.Backend`. +/// `ImageCache` is a high level abstract for storing an image as well as its data to memory and disk, and +/// retrieving them back. +/// +/// While a default image cache object will be used if you prefer the extension methods of Kingfisher, you can create +/// your own cache object and configure its storages as your need. This class also provide an interface for you to set +/// the memory and disk storage config. +open class ImageCache { + + // MARK: Singleton + /// The default `ImageCache` object. Kingfisher will use this cache for its related methods if there is no + /// other cache specified. The `name` of this default cache is "default", and you should not use this name + /// for any of your customize cache. + public static let `default` = ImageCache(name: "default") + + + // MARK: Public Properties + /// The `MemoryStorage.Backend` object used in this cache. This storage holds loaded images in memory with a + /// reasonable expire duration and a maximum memory usage. To modify the configuration of a storage, just set + /// the storage `config` and its properties. + public let memoryStorage: MemoryStorage.Backend + + /// The `DiskStorage.Backend` object used in this cache. This storage stores loaded images in disk with a + /// reasonable expire duration and a maximum disk usage. To modify the configuration of a storage, just set + /// the storage `config` and its properties. + public let diskStorage: DiskStorage.Backend + + private let ioQueue: DispatchQueue + + /// Closure that defines the disk cache path from a given path and cacheName. + public typealias DiskCachePathClosure = (URL, String) -> URL + + // MARK: Initializers + + /// Creates an `ImageCache` from a customized `MemoryStorage` and `DiskStorage`. + /// + /// - Parameters: + /// - memoryStorage: The `MemoryStorage.Backend` object to use in the image cache. + /// - diskStorage: The `DiskStorage.Backend` object to use in the image cache. + public init( + memoryStorage: MemoryStorage.Backend, + diskStorage: DiskStorage.Backend) + { + self.memoryStorage = memoryStorage + self.diskStorage = diskStorage + let ioQueueName = "com.onevcat.Kingfisher.ImageCache.ioQueue.\(UUID().uuidString)" + ioQueue = DispatchQueue(label: ioQueueName) + + let notifications: [(Notification.Name, Selector)] + #if !os(macOS) && !os(watchOS) + notifications = [ + (UIApplication.didReceiveMemoryWarningNotification, #selector(clearMemoryCache)), + (UIApplication.willTerminateNotification, #selector(cleanExpiredDiskCache)), + (UIApplication.didEnterBackgroundNotification, #selector(backgroundCleanExpiredDiskCache)) + ] + #elseif os(macOS) + notifications = [ + (NSApplication.willResignActiveNotification, #selector(cleanExpiredDiskCache)), + ] + #else + notifications = [] + #endif + notifications.forEach { + NotificationCenter.default.addObserver(self, selector: $0.1, name: $0.0, object: nil) + } + } + + /// Creates an `ImageCache` with a given `name`. Both `MemoryStorage` and `DiskStorage` will be created + /// with a default config based on the `name`. + /// + /// - Parameter name: The name of cache object. It is used to setup disk cache directories and IO queue. + /// You should not use the same `name` for different caches, otherwise, the disk storage would + /// be conflicting to each other. The `name` should not be an empty string. + public convenience init(name: String) { + self.init(noThrowName: name, cacheDirectoryURL: nil, diskCachePathClosure: nil) + } + + /// Creates an `ImageCache` with a given `name`, cache directory `path` + /// and a closure to modify the cache directory. + /// + /// - Parameters: + /// - name: The name of cache object. It is used to setup disk cache directories and IO queue. + /// You should not use the same `name` for different caches, otherwise, the disk storage would + /// be conflicting to each other. + /// - cacheDirectoryURL: Location of cache directory URL on disk. It will be internally pass to the + /// initializer of `DiskStorage` as the disk cache directory. If `nil`, the cache + /// directory under user domain mask will be used. + /// - diskCachePathClosure: Closure that takes in an optional initial path string and generates + /// the final disk cache path. You could use it to fully customize your cache path. + /// - Throws: An error that happens during image cache creating, such as unable to create a directory at the given + /// path. + public convenience init( + name: String, + cacheDirectoryURL: URL?, + diskCachePathClosure: DiskCachePathClosure? = nil + ) throws + { + if name.isEmpty { + fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.") + } + + let memoryStorage = ImageCache.createMemoryStorage() + + let config = ImageCache.createConfig( + name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure + ) + let diskStorage = try DiskStorage.Backend(config: config) + self.init(memoryStorage: memoryStorage, diskStorage: diskStorage) + } + + convenience init( + noThrowName name: String, + cacheDirectoryURL: URL?, + diskCachePathClosure: DiskCachePathClosure? + ) + { + if name.isEmpty { + fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.") + } + + let memoryStorage = ImageCache.createMemoryStorage() + + let config = ImageCache.createConfig( + name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure + ) + let diskStorage = DiskStorage.Backend(noThrowConfig: config, creatingDirectory: true) + self.init(memoryStorage: memoryStorage, diskStorage: diskStorage) + } + + private static func createMemoryStorage() -> MemoryStorage.Backend { + let totalMemory = ProcessInfo.processInfo.physicalMemory + let costLimit = totalMemory / 4 + let memoryStorage = MemoryStorage.Backend(config: + .init(totalCostLimit: (costLimit > Int.max) ? Int.max : Int(costLimit))) + return memoryStorage + } + + private static func createConfig( + name: String, + cacheDirectoryURL: URL?, + diskCachePathClosure: DiskCachePathClosure? = nil + ) -> DiskStorage.Config + { + var diskConfig = DiskStorage.Config( + name: name, + sizeLimit: 0, + directory: cacheDirectoryURL + ) + if let closure = diskCachePathClosure { + diskConfig.cachePathBlock = closure + } + return diskConfig + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: Storing Images + + open func store(_ image: KFCrossPlatformImage, + original: Data? = nil, + forKey key: String, + options: KingfisherParsedOptionsInfo, + toDisk: Bool = true, + completionHandler: ((CacheStoreResult) -> Void)? = nil) + { + let identifier = options.processor.identifier + let callbackQueue = options.callbackQueue + + let computedKey = key.computedKey(with: identifier) + // Memory storage should not throw. + memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration) + + guard toDisk else { + if let completionHandler = completionHandler { + let result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(())) + callbackQueue.execute { completionHandler(result) } + } + return + } + + ioQueue.async { + let serializer = options.cacheSerializer + if let data = serializer.data(with: image, original: original) { + self.syncStoreToDisk( + data, + forKey: key, + processorIdentifier: identifier, + callbackQueue: callbackQueue, + expiration: options.diskCacheExpiration, + writeOptions: options.diskStoreWriteOptions, + completionHandler: completionHandler) + } else { + guard let completionHandler = completionHandler else { return } + + let diskError = KingfisherError.cacheError( + reason: .cannotSerializeImage(image: image, original: original, serializer: serializer)) + let result = CacheStoreResult( + memoryCacheResult: .success(()), + diskCacheResult: .failure(diskError)) + callbackQueue.execute { completionHandler(result) } + } + } + } + + /// Stores an image to the cache. + /// + /// - Parameters: + /// - image: The image to be stored. + /// - original: The original data of the image. This value will be forwarded to the provided `serializer` for + /// further use. By default, Kingfisher uses a `DefaultCacheSerializer` to serialize the image to + /// data for caching in disk, it checks the image format based on `original` data to determine in + /// which image format should be used. For other types of `serializer`, it depends on their + /// implementation detail on how to use this original data. + /// - key: The key used for caching the image. + /// - identifier: The identifier of processor being used for caching. If you are using a processor for the + /// image, pass the identifier of processor to this parameter. + /// - serializer: The `CacheSerializer` + /// - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory. + /// Otherwise, it is cached in both memory storage and disk storage. Default is `true`. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`. For case + /// that `toDisk` is `false`, a `.untouch` queue means `callbackQueue` will be invoked from the + /// caller queue of this method. If `toDisk` is `true`, the `completionHandler` will be called + /// from an internal file IO queue. To change this behavior, specify another `CallbackQueue` + /// value. + /// - completionHandler: A closure which is invoked when the cache operation finishes. + open func store(_ image: KFCrossPlatformImage, + original: Data? = nil, + forKey key: String, + processorIdentifier identifier: String = "", + cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default, + toDisk: Bool = true, + callbackQueue: CallbackQueue = .untouch, + completionHandler: ((CacheStoreResult) -> Void)? = nil) + { + struct TempProcessor: ImageProcessor { + let identifier: String + func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return nil + } + } + + let options = KingfisherParsedOptionsInfo([ + .processor(TempProcessor(identifier: identifier)), + .cacheSerializer(serializer), + .callbackQueue(callbackQueue) + ]) + store(image, original: original, forKey: key, options: options, + toDisk: toDisk, completionHandler: completionHandler) + } + + open func storeToDisk( + _ data: Data, + forKey key: String, + processorIdentifier identifier: String = "", + expiration: StorageExpiration? = nil, + callbackQueue: CallbackQueue = .untouch, + completionHandler: ((CacheStoreResult) -> Void)? = nil) + { + ioQueue.async { + self.syncStoreToDisk( + data, + forKey: key, + processorIdentifier: identifier, + callbackQueue: callbackQueue, + expiration: expiration, + completionHandler: completionHandler) + } + } + + private func syncStoreToDisk( + _ data: Data, + forKey key: String, + processorIdentifier identifier: String = "", + callbackQueue: CallbackQueue = .untouch, + expiration: StorageExpiration? = nil, + writeOptions: Data.WritingOptions = [], + completionHandler: ((CacheStoreResult) -> Void)? = nil) + { + let computedKey = key.computedKey(with: identifier) + let result: CacheStoreResult + do { + try self.diskStorage.store(value: data, forKey: computedKey, expiration: expiration, writeOptions: writeOptions) + result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(())) + } catch { + let diskError: KingfisherError + if let error = error as? KingfisherError { + diskError = error + } else { + diskError = .cacheError(reason: .cannotConvertToData(object: data, error: error)) + } + + result = CacheStoreResult( + memoryCacheResult: .success(()), + diskCacheResult: .failure(diskError) + ) + } + if let completionHandler = completionHandler { + callbackQueue.execute { completionHandler(result) } + } + } + + // MARK: Removing Images + + /// Removes the image for the given key from the cache. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: The identifier of processor being used for caching. If you are using a processor for the + /// image, pass the identifier of processor to this parameter. + /// - fromMemory: Whether this image should be removed from memory storage or not. + /// If `false`, the image won't be removed from the memory storage. Default is `true`. + /// - fromDisk: Whether this image should be removed from disk storage or not. + /// If `false`, the image won't be removed from the disk storage. Default is `true`. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`. + /// - completionHandler: A closure which is invoked when the cache removing operation finishes. + open func removeImage(forKey key: String, + processorIdentifier identifier: String = "", + fromMemory: Bool = true, + fromDisk: Bool = true, + callbackQueue: CallbackQueue = .untouch, + completionHandler: (() -> Void)? = nil) + { + let computedKey = key.computedKey(with: identifier) + + if fromMemory { + memoryStorage.remove(forKey: computedKey) + } + + if fromDisk { + ioQueue.async{ + try? self.diskStorage.remove(forKey: computedKey) + if let completionHandler = completionHandler { + callbackQueue.execute { completionHandler() } + } + } + } else { + if let completionHandler = completionHandler { + callbackQueue.execute { completionHandler() } + } + } + } + + // MARK: Getting Images + + /// Gets an image for a given key from the cache, either from memory storage or disk storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The `KingfisherParsedOptionsInfo` options setting used for retrieving the image. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.mainCurrentOrAsync`. + /// - completionHandler: A closure which is invoked when the image getting operation finishes. If the + /// image retrieving operation finishes without problem, an `ImageCacheResult` value + /// will be sent to this closure as result. Otherwise, a `KingfisherError` result + /// with detail failing reason will be sent. + open func retrieveImage( + forKey key: String, + options: KingfisherParsedOptionsInfo, + callbackQueue: CallbackQueue = .mainCurrentOrAsync, + completionHandler: ((Result) -> Void)?) + { + // No completion handler. No need to start working and early return. + guard let completionHandler = completionHandler else { return } + + // Try to check the image from memory cache first. + if let image = retrieveImageInMemoryCache(forKey: key, options: options) { + callbackQueue.execute { completionHandler(.success(.memory(image))) } + } else if options.fromMemoryCacheOrRefresh { + callbackQueue.execute { completionHandler(.success(.none)) } + } else { + + // Begin to disk search. + self.retrieveImageInDiskCache(forKey: key, options: options, callbackQueue: callbackQueue) { + result in + switch result { + case .success(let image): + + guard let image = image else { + // No image found in disk storage. + callbackQueue.execute { completionHandler(.success(.none)) } + return + } + + // Cache the disk image to memory. + // We are passing `false` to `toDisk`, the memory cache does not change + // callback queue, we can call `completionHandler` without another dispatch. + var cacheOptions = options + cacheOptions.callbackQueue = .untouch + self.store( + image, + forKey: key, + options: cacheOptions, + toDisk: false) + { + _ in + callbackQueue.execute { completionHandler(.success(.disk(image))) } + } + case .failure(let error): + callbackQueue.execute { completionHandler(.failure(error)) } + } + } + } + } + + /// Gets an image for a given key from the cache, either from memory storage or disk storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.mainCurrentOrAsync`. + /// - completionHandler: A closure which is invoked when the image getting operation finishes. If the + /// image retrieving operation finishes without problem, an `ImageCacheResult` value + /// will be sent to this closure as result. Otherwise, a `KingfisherError` result + /// with detail failing reason will be sent. + /// + /// Note: This method is marked as `open` for only compatible purpose. Do not overide this method. Instead, override + /// the version receives `KingfisherParsedOptionsInfo` instead. + open func retrieveImage(forKey key: String, + options: KingfisherOptionsInfo? = nil, + callbackQueue: CallbackQueue = .mainCurrentOrAsync, + completionHandler: ((Result) -> Void)?) + { + retrieveImage( + forKey: key, + options: KingfisherParsedOptionsInfo(options), + callbackQueue: callbackQueue, + completionHandler: completionHandler) + } + + /// Gets an image for a given key from the memory storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The `KingfisherParsedOptionsInfo` options setting used for retrieving the image. + /// - Returns: The image stored in memory cache, if exists and valid. Otherwise, if the image does not exist or + /// has already expired, `nil` is returned. + open func retrieveImageInMemoryCache( + forKey key: String, + options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? + { + let computedKey = key.computedKey(with: options.processor.identifier) + return memoryStorage.value(forKey: computedKey, extendingExpiration: options.memoryCacheAccessExtendingExpiration) + } + + /// Gets an image for a given key from the memory storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image. + /// - Returns: The image stored in memory cache, if exists and valid. Otherwise, if the image does not exist or + /// has already expired, `nil` is returned. + /// + /// Note: This method is marked as `open` for only compatible purpose. Do not overide this method. Instead, override + /// the version receives `KingfisherParsedOptionsInfo` instead. + open func retrieveImageInMemoryCache( + forKey key: String, + options: KingfisherOptionsInfo? = nil) -> KFCrossPlatformImage? + { + return retrieveImageInMemoryCache(forKey: key, options: KingfisherParsedOptionsInfo(options)) + } + + func retrieveImageInDiskCache( + forKey key: String, + options: KingfisherParsedOptionsInfo, + callbackQueue: CallbackQueue = .untouch, + completionHandler: @escaping (Result) -> Void) + { + let computedKey = key.computedKey(with: options.processor.identifier) + let loadingQueue: CallbackQueue = options.loadDiskFileSynchronously ? .untouch : .dispatch(ioQueue) + loadingQueue.execute { + do { + var image: KFCrossPlatformImage? = nil + if let data = try self.diskStorage.value(forKey: computedKey, extendingExpiration: options.diskCacheAccessExtendingExpiration) { + image = options.cacheSerializer.image(with: data, options: options) + } + callbackQueue.execute { completionHandler(.success(image)) } + } catch { + if let error = error as? KingfisherError { + callbackQueue.execute { completionHandler(.failure(error)) } + } else { + assertionFailure("The internal thrown error should be a `KingfisherError`.") + } + } + } + } + + /// Gets an image for a given key from the disk storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`. + /// - completionHandler: A closure which is invoked when the operation finishes. + open func retrieveImageInDiskCache( + forKey key: String, + options: KingfisherOptionsInfo? = nil, + callbackQueue: CallbackQueue = .untouch, + completionHandler: @escaping (Result) -> Void) + { + retrieveImageInDiskCache( + forKey: key, + options: KingfisherParsedOptionsInfo(options), + callbackQueue: callbackQueue, + completionHandler: completionHandler) + } + + // MARK: Cleaning + /// Clears the memory & disk storage of this cache. This is an async operation. + /// + /// - Parameter handler: A closure which is invoked when the cache clearing operation finishes. + /// This `handler` will be called from the main queue. + public func clearCache(completion handler: (() -> Void)? = nil) { + clearMemoryCache() + clearDiskCache(completion: handler) + } + + /// Clears the memory storage of this cache. + @objc public func clearMemoryCache() { + memoryStorage.removeAll() + } + + /// Clears the disk storage of this cache. This is an async operation. + /// + /// - Parameter handler: A closure which is invoked when the cache clearing operation finishes. + /// This `handler` will be called from the main queue. + open func clearDiskCache(completion handler: (() -> Void)? = nil) { + ioQueue.async { + do { + try self.diskStorage.removeAll() + } catch _ { } + if let handler = handler { + DispatchQueue.main.async { handler() } + } + } + } + + /// Clears the expired images from memory & disk storage. This is an async operation. + open func cleanExpiredCache(completion handler: (() -> Void)? = nil) { + cleanExpiredMemoryCache() + cleanExpiredDiskCache(completion: handler) + } + + /// Clears the expired images from disk storage. + open func cleanExpiredMemoryCache() { + memoryStorage.removeExpired() + } + + /// Clears the expired images from disk storage. This is an async operation. + @objc func cleanExpiredDiskCache() { + cleanExpiredDiskCache(completion: nil) + } + + /// Clears the expired images from disk storage. This is an async operation. + /// + /// - Parameter handler: A closure which is invoked when the cache clearing operation finishes. + /// This `handler` will be called from the main queue. + open func cleanExpiredDiskCache(completion handler: (() -> Void)? = nil) { + ioQueue.async { + do { + var removed: [URL] = [] + let removedExpired = try self.diskStorage.removeExpiredValues() + removed.append(contentsOf: removedExpired) + + let removedSizeExceeded = try self.diskStorage.removeSizeExceededValues() + removed.append(contentsOf: removedSizeExceeded) + + if !removed.isEmpty { + DispatchQueue.main.async { + let cleanedHashes = removed.map { $0.lastPathComponent } + NotificationCenter.default.post( + name: .KingfisherDidCleanDiskCache, + object: self, + userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes]) + } + } + + if let handler = handler { + DispatchQueue.main.async { handler() } + } + } catch {} + } + } + +#if !os(macOS) && !os(watchOS) + /// Clears the expired images from disk storage when app is in background. This is an async operation. + /// In most cases, you should not call this method explicitly. + /// It will be called automatically when `UIApplicationDidEnterBackgroundNotification` received. + @objc public func backgroundCleanExpiredDiskCache() { + // if 'sharedApplication()' is unavailable, then return + guard let sharedApplication = KingfisherWrapper.shared else { return } + + func endBackgroundTask(_ task: inout UIBackgroundTaskIdentifier) { + sharedApplication.endBackgroundTask(task) + task = UIBackgroundTaskIdentifier.invalid + } + + var backgroundTask: UIBackgroundTaskIdentifier! + backgroundTask = sharedApplication.beginBackgroundTask { + endBackgroundTask(&backgroundTask!) + } + + cleanExpiredDiskCache { + endBackgroundTask(&backgroundTask!) + } + } +#endif + + // MARK: Image Cache State + + /// Returns the cache type for a given `key` and `identifier` combination. + /// This method is used for checking whether an image is cached in current cache. + /// It also provides information on which kind of cache can it be found in the return value. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: Processor identifier which used for this image. Default is the `identifier` of + /// `DefaultImageProcessor.default`. + /// - Returns: A `CacheType` instance which indicates the cache status. + /// `.none` means the image is not in cache or it is already expired. + open func imageCachedType( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> CacheType + { + let computedKey = key.computedKey(with: identifier) + if memoryStorage.isCached(forKey: computedKey) { return .memory } + if diskStorage.isCached(forKey: computedKey) { return .disk } + return .none + } + + /// Returns whether the file exists in cache for a given `key` and `identifier` combination. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: Processor identifier which used for this image. Default is the `identifier` of + /// `DefaultImageProcessor.default`. + /// - Returns: A `Bool` which indicates whether a cache could match the given `key` and `identifier` combination. + /// + /// - Note: + /// The return value does not contain information about from which kind of storage the cache matches. + /// To get the information about cache type according `CacheType`, + /// use `imageCachedType(forKey:processorIdentifier:)` instead. + public func isCached( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> Bool + { + return imageCachedType(forKey: key, processorIdentifier: identifier).cached + } + + /// Gets the hash used as cache file name for the key. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: Processor identifier which used for this image. Default is the `identifier` of + /// `DefaultImageProcessor.default`. + /// - Returns: The hash which is used as the cache file name. + /// + /// - Note: + /// By default, for a given combination of `key` and `identifier`, `ImageCache` will use the value + /// returned by this method as the cache file name. You can use this value to check and match cache file + /// if you need. + open func hash( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> String + { + let computedKey = key.computedKey(with: identifier) + return diskStorage.cacheFileName(forKey: computedKey) + } + + /// Calculates the size taken by the disk storage. + /// It is the total file size of all cached files in the `diskStorage` on disk in bytes. + /// + /// - Parameter handler: Called with the size calculating finishes. This closure is invoked from the main queue. + open func calculateDiskStorageSize(completion handler: @escaping ((Result) -> Void)) { + ioQueue.async { + do { + let size = try self.diskStorage.totalSize() + DispatchQueue.main.async { handler(.success(size)) } + } catch { + if let error = error as? KingfisherError { + DispatchQueue.main.async { handler(.failure(error)) } + } else { + assertionFailure("The internal thrown error should be a `KingfisherError`.") + } + + } + } + } + + /// Gets the cache path for the key. + /// It is useful for projects with web view or anyone that needs access to the local file path. + /// + /// i.e. Replacing the `` tag in your HTML. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: Processor identifier which used for this image. Default is the `identifier` of + /// `DefaultImageProcessor.default`. + /// - Returns: The disk path of cached image under the given `key` and `identifier`. + /// + /// - Note: + /// This method does not guarantee there is an image already cached in the returned path. It just gives your + /// the path that the image should be, if it exists in disk storage. + /// + /// You could use `isCached(forKey:)` method to check whether the image is cached under that key in disk. + open func cachePath( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> String + { + let computedKey = key.computedKey(with: identifier) + return diskStorage.cacheFileURL(forKey: computedKey).path + } +} + +extension Dictionary { + func keysSortedByValue(_ isOrderedBefore: (Value, Value) -> Bool) -> [Key] { + return Array(self).sorted{ isOrderedBefore($0.1, $1.1) }.map{ $0.0 } + } +} + +#if !os(macOS) && !os(watchOS) +// MARK: - For App Extensions +extension UIApplication: KingfisherCompatible { } +extension KingfisherWrapper where Base: UIApplication { + public static var shared: UIApplication? { + let selector = NSSelectorFromString("sharedApplication") + guard Base.responds(to: selector) else { return nil } + return Base.perform(selector).takeUnretainedValue() as? UIApplication + } +} +#endif + +extension String { + func computedKey(with identifier: String) -> String { + if identifier.isEmpty { + return self + } else { + return appending("@\(identifier)") + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift new file mode 100644 index 0000000..b8b474e --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift @@ -0,0 +1,285 @@ +// +// MemoryStorage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a set of conception related to storage which stores a certain type of value in memory. +/// This is a namespace for the memory storage types. A `Backend` with a certain `Config` will be used to describe the +/// storage. See these composed types for more information. +public enum MemoryStorage { + + /// Represents a storage which stores a certain type of value in memory. It provides fast access, + /// but limited storing size. The stored value type needs to conform to `CacheCostCalculable`, + /// and its `cacheCost` will be used to determine the cost of size for the cache item. + /// + /// You can config a `MemoryStorage.Backend` in its initializer by passing a `MemoryStorage.Config` value. + /// or modifying the `config` property after it being created. The backend of `MemoryStorage` has + /// upper limitation on cost size in memory and item count. All items in the storage has an expiration + /// date. When retrieved, if the target item is already expired, it will be recognized as it does not + /// exist in the storage. The `MemoryStorage` also contains a scheduled self clean task, to evict expired + /// items from memory. + public class Backend { + let storage = NSCache>() + + // Keys trackes the objects once inside the storage. For object removing triggered by user, the corresponding + // key would be also removed. However, for the object removing triggered by cache rule/policy of system, the + // key will be remained there until next `removeExpired` happens. + // + // Breaking the strict tracking could save additional locking behaviors. + // See https://github.com/onevcat/Kingfisher/issues/1233 + var keys = Set() + + private var cleanTimer: Timer? = nil + private let lock = NSLock() + + /// The config used in this storage. It is a value you can set and + /// use to config the storage in air. + public var config: Config { + didSet { + storage.totalCostLimit = config.totalCostLimit + storage.countLimit = config.countLimit + } + } + + /// Creates a `MemoryStorage` with a given `config`. + /// + /// - Parameter config: The config used to create the storage. It determines the max size limitation, + /// default expiration setting and more. + public init(config: Config) { + self.config = config + storage.totalCostLimit = config.totalCostLimit + storage.countLimit = config.countLimit + + cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in + guard let self = self else { return } + self.removeExpired() + } + } + + /// Removes the expired values from the storage. + public func removeExpired() { + lock.lock() + defer { lock.unlock() } + for key in keys { + let nsKey = key as NSString + guard let object = storage.object(forKey: nsKey) else { + // This could happen if the object is moved by cache `totalCostLimit` or `countLimit` rule. + // We didn't remove the key yet until now, since we do not want to introduce additional lock. + // See https://github.com/onevcat/Kingfisher/issues/1233 + keys.remove(key) + continue + } + if object.estimatedExpiration.isPast { + storage.removeObject(forKey: nsKey) + keys.remove(key) + } + } + } + + /// Stores a value to the storage under the specified key and expiration policy. + /// - Parameters: + /// - value: The value to be stored. + /// - key: The key to which the `value` will be stored. + /// - expiration: The expiration policy used by this store action. + /// - Throws: No error will + public func store( + value: T, + forKey key: String, + expiration: StorageExpiration? = nil) + { + storeNoThrow(value: value, forKey: key, expiration: expiration) + } + + // The no throw version for storing value in cache. Kingfisher knows the detail so it + // could use this version to make syntax simpler internally. + func storeNoThrow( + value: T, + forKey key: String, + expiration: StorageExpiration? = nil) + { + lock.lock() + defer { lock.unlock() } + let expiration = expiration ?? config.expiration + // The expiration indicates that already expired, no need to store. + guard !expiration.isExpired else { return } + + let object: StorageObject + if config.keepWhenEnteringBackground { + object = BackgroundKeepingStorageObject(value, key: key, expiration: expiration) + } else { + object = StorageObject(value, key: key, expiration: expiration) + } + storage.setObject(object, forKey: key as NSString, cost: value.cacheCost) + keys.insert(key) + } + + /// Gets a value from the storage. + /// + /// - Parameters: + /// - key: The cache key of value. + /// - extendingExpiration: The expiration policy used by this getting action. + /// - Returns: The value under `key` if it is valid and found in the storage. Otherwise, `nil`. + public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? { + guard let object = storage.object(forKey: key as NSString) else { + return nil + } + if object.expired { + return nil + } + object.extendExpiration(extendingExpiration) + return object.value + } + + /// Whether there is valid cached data under a given key. + /// - Parameter key: The cache key of value. + /// - Returns: If there is valid data under the key, `true`. Otherwise, `false`. + public func isCached(forKey key: String) -> Bool { + guard let _ = value(forKey: key, extendingExpiration: .none) else { + return false + } + return true + } + + /// Removes a value from a specified key. + /// - Parameter key: The cache key of value. + public func remove(forKey key: String) { + lock.lock() + defer { lock.unlock() } + storage.removeObject(forKey: key as NSString) + keys.remove(key) + } + + /// Removes all values in this storage. + public func removeAll() { + lock.lock() + defer { lock.unlock() } + storage.removeAllObjects() + keys.removeAll() + } + } +} + +extension MemoryStorage { + /// Represents the config used in a `MemoryStorage`. + public struct Config { + + /// Total cost limit of the storage in bytes. + public var totalCostLimit: Int + + /// The item count limit of the memory storage. + public var countLimit: Int = .max + + /// The `StorageExpiration` used in this memory storage. Default is `.seconds(300)`, + /// means that the memory cache would expire in 5 minutes. + public var expiration: StorageExpiration = .seconds(300) + + /// The time interval between the storage do clean work for swiping expired items. + public var cleanInterval: TimeInterval + + /// Whether the newly added items to memory cache should be purged when the app goes to background. + /// + /// By default, the cached items in memory will be purged as soon as the app goes to background to ensure + /// least memory footprint. Enabling this would prevent this behavior and keep the items alive in cache even + /// when your app is not in foreground anymore. + /// + /// Default is `false`. After setting `true`, only the newly added cache objects are affected. Existing + /// objects which are already in the cache while this value was `false` will be still be purged when entering + /// background. + public var keepWhenEnteringBackground: Bool = false + + /// Creates a config from a given `totalCostLimit` value. + /// + /// - Parameters: + /// - totalCostLimit: Total cost limit of the storage in bytes. + /// - cleanInterval: The time interval between the storage do clean work for swiping expired items. + /// Default is 120, means the auto eviction happens once per two minutes. + /// + /// - Note: + /// Other members of `MemoryStorage.Config` will use their default values when created. + public init(totalCostLimit: Int, cleanInterval: TimeInterval = 120) { + self.totalCostLimit = totalCostLimit + self.cleanInterval = cleanInterval + } + } +} + +extension MemoryStorage { + + class BackgroundKeepingStorageObject: StorageObject, NSDiscardableContent { + var accessing = true + func beginContentAccess() -> Bool { + if value != nil { + accessing = true + } else { + accessing = false + } + return accessing + } + + func endContentAccess() { + accessing = false + } + + func discardContentIfPossible() { + value = nil + } + + func isContentDiscarded() -> Bool { + return value == nil + } + } + + class StorageObject { + var value: T? + let expiration: StorageExpiration + let key: String + + private(set) var estimatedExpiration: Date + + init(_ value: T, key: String, expiration: StorageExpiration) { + self.value = value + self.key = key + self.expiration = expiration + + self.estimatedExpiration = expiration.estimatedExpirationSinceNow + } + + func extendExpiration(_ extendingExpiration: ExpirationExtending = .cacheTime) { + switch extendingExpiration { + case .none: + return + case .cacheTime: + self.estimatedExpiration = expiration.estimatedExpirationSinceNow + case .expirationTime(let expirationTime): + self.estimatedExpiration = expirationTime.estimatedExpirationSinceNow + } + } + + var expired: Bool { + return estimatedExpiration.isPast + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/Storage.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/Storage.swift new file mode 100644 index 0000000..ca0f8bb --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/Storage.swift @@ -0,0 +1,113 @@ +// +// Storage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Constants for some time intervals +struct TimeConstants { + static let secondsInOneMinute = 60 + static let minutesInOneHour = 60 + static let hoursInOneDay = 24 + static let secondsInOneDay = 86_400 +} + +/// Represents the expiration strategy used in storage. +/// +/// - never: The item never expires. +/// - seconds: The item expires after a time duration of given seconds from now. +/// - days: The item expires after a time duration of given days from now. +/// - date: The item expires after a given date. +public enum StorageExpiration { + /// The item never expires. + case never + /// The item expires after a time duration of given seconds from now. + case seconds(TimeInterval) + /// The item expires after a time duration of given days from now. + case days(Int) + /// The item expires after a given date. + case date(Date) + /// Indicates the item is already expired. Use this to skip cache. + case expired + + func estimatedExpirationSince(_ date: Date) -> Date { + switch self { + case .never: return .distantFuture + case .seconds(let seconds): + return date.addingTimeInterval(seconds) + case .days(let days): + let duration: TimeInterval = TimeInterval(TimeConstants.secondsInOneDay) * TimeInterval(days) + return date.addingTimeInterval(duration) + case .date(let ref): + return ref + case .expired: + return .distantPast + } + } + + var estimatedExpirationSinceNow: Date { + return estimatedExpirationSince(Date()) + } + + var isExpired: Bool { + return timeInterval <= 0 + } + + var timeInterval: TimeInterval { + switch self { + case .never: return .infinity + case .seconds(let seconds): return seconds + case .days(let days): return TimeInterval(TimeConstants.secondsInOneDay) * TimeInterval(days) + case .date(let ref): return ref.timeIntervalSinceNow + case .expired: return -(.infinity) + } + } +} + +/// Represents the expiration extending strategy used in storage to after access. +/// +/// - none: The item expires after the original time, without extending after access. +/// - cacheTime: The item expiration extends by the original cache time after each access. +/// - expirationTime: The item expiration extends by the provided time after each access. +public enum ExpirationExtending { + /// The item expires after the original time, without extending after access. + case none + /// The item expiration extends by the original cache time after each access. + case cacheTime + /// The item expiration extends by the provided time after each access. + case expirationTime(_ expiration: StorageExpiration) +} + +/// Represents types which cost in memory can be calculated. +public protocol CacheCostCalculable { + var cacheCost: Int { get } +} + +/// Represents types which can be converted to and from data. +public protocol DataTransformable { + func toData() throws -> Data + static func fromData(_ data: Data) throws -> Self + static var empty: Self { get } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift new file mode 100644 index 0000000..d49c2f6 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift @@ -0,0 +1,258 @@ + +// +// CPListItem+Kingfisher.swift +// Kingfisher +// +// Created by Wayne Hartman on 2021-08-29. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +import CarPlay + +@available(iOS 14.0, *) +extension KingfisherWrapper where Base: CPListItem { + + // MARK: Setting Image + + /// Sets an image to the image view with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? [])) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + /** + * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 + * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, + * which allows >=14.5 SDK to set a `nil` image. This compile check allows + * newer SDK users to set the image to `nil`, while still allowing older SDK + * users to compile the framework. + */ + #if compiler(>=5.4) + self.base.setImage(placeholder) + #else + if let placeholder = placeholder { + self.base.setImage(placeholder) + } + #endif + + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + /** + * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 + * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, + * which allows >=14.5 SDK to set a `nil` image. This compile check allows + * newer SDK users to set the image to `nil`, while still allowing older SDK + * users to compile the framework. + */ + #if compiler(>=5.4) + self.base.setImage(placeholder) + #else // Let older SDK users deal with the older behavior. + if let placeholder = placeholder { + self.base.setImage(placeholder) + } + #endif + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.setImage(image) + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + self.base.setImage(value.image) + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + /** + * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 + * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, + * which allows >=14.5 SDK to set a `nil` image. This compile check allows + * newer SDK users to set the image to `nil`, while still allowing older SDK + * users to compile the framework. + */ + #if compiler(>=5.4) + self.base.setImage(image) + #else // Let older SDK users deal with the older behavior. + if let unwrapped = image { + self.base.setImage(unwrapped) + } + #endif + + } else { + #if compiler(>=5.4) + self.base.setImage(nil) + #endif + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Image + + /// Cancel the image download task bounded to the image view if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} + +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +extension KingfisherWrapper where Base: CPListItem { + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift new file mode 100644 index 0000000..6ead7d5 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift @@ -0,0 +1,545 @@ +// +// ImageView+Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if !os(watchOS) + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +extension KingfisherWrapper where Base: KFCrossPlatformImageView { + + // MARK: Setting Image + + /// Sets an image to the image view with a `Source`. + /// + /// - Parameters: + /// - source: The `Source` object defines data information from network or a data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ``` + /// // Set image from a network source. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: .network(url)) + /// + /// // Or set image from a data provider. + /// let provider = LocalFileImageDataProvider(fileURL: fileURL) + /// imageView.kf.setImage(with: .provider(provider)) + /// ``` + /// + /// For both `.network` and `.provider` source, there are corresponding view extension methods. So the code + /// above is equivalent to: + /// + /// ``` + /// imageView.kf.setImage(with: url) + /// imageView.kf.setImage(with: provider) + /// ``` + /// + /// Internally, this method will use `KingfisherManager` to get the source. + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage(with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler) + } + + /// Sets an image to the image view with a `Source`. + /// + /// - Parameters: + /// - source: The `Source` object defines data information from network or a data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ``` + /// // Set image from a network source. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: .network(url)) + /// + /// // Or set image from a data provider. + /// let provider = LocalFileImageDataProvider(fileURL: fileURL) + /// imageView.kf.setImage(with: .provider(provider)) + /// ``` + /// + /// For both `.network` and `.provider` source, there are corresponding view extension methods. So the code + /// above is equivalent to: + /// + /// ``` + /// imageView.kf.setImage(with: url) + /// imageView.kf.setImage(with: provider) + /// ``` + /// + /// Internally, this method will use `KingfisherManager` to get the source. + /// Since this method will perform UI changes, you must call it from the main thread. + /// The `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: source, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// This is the easiest way to use Kingfisher to boost the image setting process from network. Since all parameters + /// have a default value except the `resource`, you can set an image from a certain URL to an image view like this: + /// + /// ``` + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: url) + /// ``` + /// + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + /// Sets an image to the image view with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// This is the easiest way to use Kingfisher to boost the image setting process from network. Since all parameters + /// have a default value except the `resource`, you can set an image from a certain URL to an image view like this: + /// + /// ``` + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: url) + /// ``` + /// + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// The `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a data provider. + /// + /// - Parameters: + /// - provider: The `ImageDataProvider` object contains information about the data. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// Internally, this method will use `KingfisherManager` to get the image data, from either cache + /// or the data provider. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with provider: ImageDataProvider?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: provider.map { .provider($0) }, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + /// Sets an image to the image view with a data provider. + /// + /// - Parameters: + /// - provider: The `ImageDataProvider` object contains information about the data. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// Internally, this method will use `KingfisherManager` to get the image data, from either cache + /// or the data provider. Since this method will perform UI changes, you must call it from the main thread. + /// The `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with provider: ImageDataProvider?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: provider, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + + func setImage( + with source: Source?, + placeholder: Placeholder? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + mutatingSelf.placeholder = placeholder + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + + let isEmptyImage = base.image == nil && self.placeholder == nil + if !options.keepCurrentImageWhileLoading || isEmptyImage { + // Always set placeholder while there is no image/placeholder yet. + mutatingSelf.placeholder = placeholder + } + + let maybeIndicator = indicator + maybeIndicator?.startAnimatingView() + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if base.shouldPreloadAllAnimation() { + options.preloadAllAnimationData = true + } + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.image = image + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + maybeIndicator?.stopAnimatingView() + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + guard self.needsTransition(options: options, cacheType: value.cacheType) else { + mutatingSelf.placeholder = nil + self.base.image = value.image + completionHandler?(result) + return + } + + self.makeTransition(image: value.image, transition: options.transition) { + completionHandler?(result) + } + + case .failure: + if let image = options.onFailureImage { + mutatingSelf.placeholder = nil + self.base.image = image + } + completionHandler?(result) + } + } + } + ) + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Downloading Task + + /// Cancels the image download task of the image view if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } + + private func needsTransition(options: KingfisherParsedOptionsInfo, cacheType: CacheType) -> Bool { + switch options.transition { + case .none: + return false + #if os(macOS) + case .fade: // Fade is only a placeholder for SwiftUI on macOS. + return false + #else + default: + if options.forceTransition { return true } + if cacheType == .none { return true } + return false + #endif + } + } + + private func makeTransition(image: KFCrossPlatformImage, transition: ImageTransition, done: @escaping () -> Void) { + #if !os(macOS) + // Force hiding the indicator without transition first. + UIView.transition( + with: self.base, + duration: 0.0, + options: [], + animations: { self.indicator?.stopAnimatingView() }, + completion: { _ in + var mutatingSelf = self + mutatingSelf.placeholder = nil + UIView.transition( + with: self.base, + duration: transition.duration, + options: [transition.animationOptions, .allowUserInteraction], + animations: { transition.animations?(self.base, image) }, + completion: { finished in + transition.completion?(finished) + done() + } + ) + } + ) + #else + done() + #endif + } +} + +// MARK: - Associated Object +private var taskIdentifierKey: Void? +private var indicatorKey: Void? +private var indicatorTypeKey: Void? +private var placeholderKey: Void? +private var imageTaskKey: Void? + +extension KingfisherWrapper where Base: KFCrossPlatformImageView { + + // MARK: Properties + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + /// Holds which indicator type is going to be used. + /// Default is `.none`, means no indicator will be shown while downloading. + public var indicatorType: IndicatorType { + get { + return getAssociatedObject(base, &indicatorTypeKey) ?? .none + } + + set { + switch newValue { + case .none: indicator = nil + case .activity: indicator = ActivityIndicator() + case .image(let data): indicator = ImageIndicator(imageData: data) + case .custom(let anIndicator): indicator = anIndicator + } + + setRetainedAssociatedObject(base, &indicatorTypeKey, newValue) + } + } + + /// Holds any type that conforms to the protocol `Indicator`. + /// The protocol `Indicator` has a `view` property that will be shown when loading an image. + /// It will be `nil` if `indicatorType` is `.none`. + public private(set) var indicator: Indicator? { + get { + let box: Box? = getAssociatedObject(base, &indicatorKey) + return box?.value + } + + set { + // Remove previous + if let previousIndicator = indicator { + previousIndicator.view.removeFromSuperview() + } + + // Add new + if let newIndicator = newValue { + // Set default indicator layout + let view = newIndicator.view + + base.addSubview(view) + view.translatesAutoresizingMaskIntoConstraints = false + view.centerXAnchor.constraint( + equalTo: base.centerXAnchor, constant: newIndicator.centerOffset.x).isActive = true + view.centerYAnchor.constraint( + equalTo: base.centerYAnchor, constant: newIndicator.centerOffset.y).isActive = true + + switch newIndicator.sizeStrategy(in: base) { + case .intrinsicSize: + break + case .full: + view.heightAnchor.constraint(equalTo: base.heightAnchor, constant: 0).isActive = true + view.widthAnchor.constraint(equalTo: base.widthAnchor, constant: 0).isActive = true + case .size(let size): + view.heightAnchor.constraint(equalToConstant: size.height).isActive = true + view.widthAnchor.constraint(equalToConstant: size.width).isActive = true + } + + newIndicator.view.isHidden = true + } + + // Save in associated object + // Wrap newValue with Box to workaround an issue that Swift does not recognize + // and casting protocol for associate object correctly. https://github.com/onevcat/Kingfisher/issues/872 + setRetainedAssociatedObject(base, &indicatorKey, newValue.map(Box.init)) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } + + /// Represents the `Placeholder` used for this image view. A `Placeholder` will be shown in the view while + /// it is downloading an image. + public private(set) var placeholder: Placeholder? { + get { return getAssociatedObject(base, &placeholderKey) } + set { + if let previousPlaceholder = placeholder { + previousPlaceholder.remove(from: base) + } + + if let newPlaceholder = newValue { + newPlaceholder.add(to: base) + } else { + base.image = nil + } + setRetainedAssociatedObject(base, &placeholderKey, newValue) + } + } +} + + +extension KFCrossPlatformImageView { + @objc func shouldPreloadAllAnimation() -> Bool { return true } +} + +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift new file mode 100644 index 0000000..566eb1e --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift @@ -0,0 +1,370 @@ +// +// NSButton+Kingfisher.swift +// Kingfisher +// +// Created by Jie Zhang on 14/04/2016. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) + +import AppKit + +extension KingfisherWrapper where Base: NSButton { + + // MARK: Setting Image + + /// Sets an image to the button with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about how to get the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested source. + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the button with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.image = placeholder + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.image = placeholder + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.image = image + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + self.base.image = value.image + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.image = image + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Downloading Task + + /// Cancels the image download task of the button if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelImageDownloadTask() { + imageTask?.cancel() + } + + // MARK: Setting Alternate Image + + @discardableResult + public func setAlternateImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setAlternateImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an alternate image to the button with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setAlternateImage( + with resource: Resource?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setAlternateImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setAlternateImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.alternateImage = placeholder + mutatingSelf.alternateTaskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.alternateImage = placeholder + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.alternateTaskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.alternateImage = image + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.alternateTaskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.alternateImageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.alternateTaskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.alternateImageTask = nil + mutatingSelf.alternateTaskIdentifier = nil + + switch result { + case .success(let value): + self.base.alternateImage = value.image + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.alternateImage = image + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.alternateImageTask = task + return task + } + + // MARK: Cancelling Alternate Image Downloading Task + + /// Cancels the alternate image download task of the button if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelAlternateImageDownloadTask() { + alternateImageTask?.cancel() + } +} + + +// MARK: - Associated Object +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +private var alternateTaskIdentifierKey: Void? +private var alternateImageTaskKey: Void? + +extension KingfisherWrapper where Base: NSButton { + + // MARK: Properties + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } + + public private(set) var alternateTaskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &alternateTaskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &alternateTaskIdentifierKey, box) + } + } + + private var alternateImageTask: DownloadTask? { + get { return getAssociatedObject(base, &alternateImageTaskKey) } + set { setRetainedAssociatedObject(base, &alternateImageTaskKey, newValue)} + } +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift new file mode 100644 index 0000000..adb5490 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift @@ -0,0 +1,279 @@ +// +// NSTextAttachment+Kingfisher.swift +// Kingfisher +// +// Created by Benjamin Briggs on 22/07/2019. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if !os(watchOS) + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +extension KingfisherWrapper where Base: NSTextAttachment { + + // MARK: Setting Image + + /// Sets an image to the text attachment with a source. + /// + /// - Parameters: + /// - source: The `Source` object defines data information from network or a data provider. + /// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// + /// The retrieved image will be set to `NSTextAttachment.image` property. Because it is not an image view based + /// rendering, options related to view, such as `.transition`, are not supported. + /// + /// Kingfisher will call `setNeedsDisplay` on the `attributedView` when the image task done. It gives the view a + /// chance to render the attributed string again for displaying the downloaded image. For example, if you set an + /// attributed with this `NSTextAttachment` to a `UILabel` object, pass it as the `attributedView` parameter. + /// + /// Here is a typical use case: + /// + /// ```swift + /// let attributedText = NSMutableAttributedString(string: "Hello World") + /// let textAttachment = NSTextAttachment() + /// + /// textAttachment.kf.setImage( + /// with: URL(string: "https://onevcat.com/assets/images/avatar.jpg")!, + /// attributedView: label, + /// options: [ + /// .processor( + /// ResizingImageProcessor(referenceSize: .init(width: 30, height: 30)) + /// |> RoundCornerImageProcessor(cornerRadius: 15)) + /// ] + /// ) + /// attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment)) + /// label.attributedText = attributedText + /// ``` + /// + @discardableResult + public func setImage( + with source: Source?, + attributedView: @autoclosure @escaping () -> KFCrossPlatformView, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + attributedView: attributedView, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the text attachment with a source. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// + /// The retrieved image will be set to `NSTextAttachment.image` property. Because it is not an image view based + /// rendering, options related to view, such as `.transition`, are not supported. + /// + /// Kingfisher will call `setNeedsDisplay` on the `attributedView` when the image task done. It gives the view a + /// chance to render the attributed string again for displaying the downloaded image. For example, if you set an + /// attributed with this `NSTextAttachment` to a `UILabel` object, pass it as the `attributedView` parameter. + /// + /// Here is a typical use case: + /// + /// ```swift + /// let attributedText = NSMutableAttributedString(string: "Hello World") + /// let textAttachment = NSTextAttachment() + /// + /// textAttachment.kf.setImage( + /// with: URL(string: "https://onevcat.com/assets/images/avatar.jpg")!, + /// attributedView: label, + /// options: [ + /// .processor( + /// ResizingImageProcessor(referenceSize: .init(width: 30, height: 30)) + /// |> RoundCornerImageProcessor(cornerRadius: 15)) + /// ] + /// ) + /// attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment)) + /// label.attributedText = attributedText + /// ``` + /// + @discardableResult + public func setImage( + with resource: Resource?, + attributedView: @autoclosure @escaping () -> KFCrossPlatformView, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: resource.map { .network($0) }, + attributedView: attributedView, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + func setImage( + with source: Source?, + attributedView: @escaping () -> KFCrossPlatformView, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.image = placeholder + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.image = placeholder + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.image = image + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + self.base.image = value.image + let view = attributedView() + #if canImport(UIKit) + view.setNeedsDisplay() + #else + view.setNeedsDisplay(view.bounds) + #endif + case .failure: + if let image = options.onFailureImage { + self.base.image = image + } + } + completionHandler?(result) + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Image + + /// Cancel the image download task bounded to the text attachment if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} + +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +extension KingfisherWrapper where Base: NSTextAttachment { + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} + +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/TVMonogramView+Kingfisher.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/TVMonogramView+Kingfisher.swift new file mode 100644 index 0000000..8f0948c --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/TVMonogramView+Kingfisher.swift @@ -0,0 +1,217 @@ +// +// TVMonogramView+Kingfisher.swift +// Kingfisher +// +// Created by Marvin Nazari on 2020-12-07. +// +// Copyright (c) 2020 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +#if canImport(TVUIKit) + +import TVUIKit + +@available(tvOS 12.0, *) +extension KingfisherWrapper where Base: TVMonogramView { + + // MARK: Setting Image + + /// Sets an image to the image view with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.image = placeholder + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.image = placeholder + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.image = image + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + self.base.image = value.image + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.image = image + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + /// Sets an image to the image view with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + // MARK: Cancelling Image + + /// Cancel the image download task bounded to the image view if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} + +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +@available(tvOS 12.0, *) +extension KingfisherWrapper where Base: TVMonogramView { + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} + +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift new file mode 100644 index 0000000..e30fdd6 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift @@ -0,0 +1,416 @@ +// +// UIButton+Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/13. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if !os(watchOS) + +#if canImport(UIKit) +import UIKit + +extension KingfisherWrapper where Base: UIButton { + + // MARK: Setting Image + /// Sets an image to the button for a specified state with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested source, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + for: state, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the button for a specified state with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + for: state, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + @discardableResult + public func setImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + guard let source = source else { + base.setImage(placeholder, for: state) + setTaskIdentifier(nil, for: state) + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.setImage(placeholder, for: state) + } + + var mutatingSelf = self + let issuedIdentifier = Source.Identifier.next() + setTaskIdentifier(issuedIdentifier, for: state) + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.setImage(image, for: state) + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier(for: state) } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.taskIdentifier(for: state) else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.setTaskIdentifier(nil, for: state) + + switch result { + case .success(let value): + self.base.setImage(value.image, for: state) + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.setImage(image, for: state) + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Downloading Task + + /// Cancels the image download task of the button if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelImageDownloadTask() { + imageTask?.cancel() + } + + // MARK: Setting Background Image + + /// Sets a background image to the button for a specified state with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setBackgroundImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setBackgroundImage( + with: source, + for: state, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets a background image to the button for a specified state with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setBackgroundImage( + with resource: Resource?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setBackgroundImage( + with: resource?.convertToSource(), + for: state, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setBackgroundImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + guard let source = source else { + base.setBackgroundImage(placeholder, for: state) + setBackgroundTaskIdentifier(nil, for: state) + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.setBackgroundImage(placeholder, for: state) + } + + var mutatingSelf = self + let issuedIdentifier = Source.Identifier.next() + setBackgroundTaskIdentifier(issuedIdentifier, for: state) + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.setBackgroundImage(image, for: state) + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.backgroundTaskIdentifier(for: state) } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.backgroundImageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.backgroundTaskIdentifier(for: state) else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.backgroundImageTask = nil + mutatingSelf.setBackgroundTaskIdentifier(nil, for: state) + + switch result { + case .success(let value): + self.base.setBackgroundImage(value.image, for: state) + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.setBackgroundImage(image, for: state) + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.backgroundImageTask = task + return task + } + + // MARK: Cancelling Background Downloading Task + + /// Cancels the background image download task of the button if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelBackgroundImageDownloadTask() { + backgroundImageTask?.cancel() + } +} + +// MARK: - Associated Object +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +extension KingfisherWrapper where Base: UIButton { + + private typealias TaskIdentifier = Box<[UInt: Source.Identifier.Value]> + + public func taskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? { + return taskIdentifierInfo.value[state.rawValue] + } + + private func setTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) { + taskIdentifierInfo.value[state.rawValue] = identifier + } + + private var taskIdentifierInfo: TaskIdentifier { + return getAssociatedObject(base, &taskIdentifierKey) ?? { + setRetainedAssociatedObject(base, &taskIdentifierKey, $0) + return $0 + } (TaskIdentifier([:])) + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} + + +private var backgroundTaskIdentifierKey: Void? +private var backgroundImageTaskKey: Void? + +// MARK: Background Properties +extension KingfisherWrapper where Base: UIButton { + + public func backgroundTaskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? { + return backgroundTaskIdentifierInfo.value[state.rawValue] + } + + private func setBackgroundTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) { + backgroundTaskIdentifierInfo.value[state.rawValue] = identifier + } + + private var backgroundTaskIdentifierInfo: TaskIdentifier { + return getAssociatedObject(base, &backgroundTaskIdentifierKey) ?? { + setRetainedAssociatedObject(base, &backgroundTaskIdentifierKey, $0) + return $0 + } (TaskIdentifier([:])) + } + + private var backgroundImageTask: DownloadTask? { + get { return getAssociatedObject(base, &backgroundImageTaskKey) } + mutating set { setRetainedAssociatedObject(base, &backgroundImageTaskKey, newValue) } + } +} +#endif + +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/WKInterfaceImage+Kingfisher.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/WKInterfaceImage+Kingfisher.swift new file mode 100644 index 0000000..ce424de --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/WKInterfaceImage+Kingfisher.swift @@ -0,0 +1,212 @@ +// +// WKInterfaceImage+Kingfisher.swift +// Kingfisher +// +// Created by Rodrigo Borges Soares on 04/05/18. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(WatchKit) + +import WatchKit + +extension KingfisherWrapper where Base: WKInterfaceImage { + + // MARK: Setting Image + + /// Sets an image to the image view with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.setImage(placeholder) + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.setImage(placeholder) + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.setImage(image) + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + self.base.setImage(value.image) + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.setImage(image) + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Image + + /// Cancel the image download task bounded to the image view if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} + +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +extension KingfisherWrapper where Base: WKInterfaceImage { + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift new file mode 100644 index 0000000..db5e2b4 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift @@ -0,0 +1,137 @@ +// +// AVAssetImageDataProvider.swift +// Kingfisher +// +// Created by onevcat on 2020/08/09. +// +// Copyright (c) 2020 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if !os(watchOS) + +import Foundation +import AVKit + +#if canImport(MobileCoreServices) +import MobileCoreServices +#else +import CoreServices +#endif + +/// A data provider to provide thumbnail data from a given AVKit asset. +public struct AVAssetImageDataProvider: ImageDataProvider { + + /// The possible error might be caused by the `AVAssetImageDataProvider`. + /// - userCancelled: The data provider process is cancelled. + /// - invalidImage: The retrieved image is invalid. + public enum AVAssetImageDataProviderError: Error { + case userCancelled + case invalidImage(_ image: CGImage?) + } + + /// The asset image generator bound to `self`. + public let assetImageGenerator: AVAssetImageGenerator + + /// The time at which the image should be generate in the asset. + public let time: CMTime + + private var internalKey: String { + return (assetImageGenerator.asset as? AVURLAsset)?.url.absoluteString ?? UUID().uuidString + } + + /// The cache key used by `self`. + public var cacheKey: String { + return "\(internalKey)_\(time.seconds)" + } + + /// Creates an asset image data provider. + /// - Parameters: + /// - assetImageGenerator: The asset image generator controls data providing behaviors. + /// - time: At which time in the asset the image should be generated. + public init(assetImageGenerator: AVAssetImageGenerator, time: CMTime) { + self.assetImageGenerator = assetImageGenerator + self.time = time + } + + /// Creates an asset image data provider. + /// - Parameters: + /// - assetURL: The URL of asset for providing image data. + /// - time: At which time in the asset the image should be generated. + /// + /// This method uses `assetURL` to create an `AVAssetImageGenerator` object and calls + /// the `init(assetImageGenerator:time:)` initializer. + /// + public init(assetURL: URL, time: CMTime) { + let asset = AVAsset(url: assetURL) + let generator = AVAssetImageGenerator(asset: asset) + self.init(assetImageGenerator: generator, time: time) + } + + /// Creates an asset image data provider. + /// + /// - Parameters: + /// - assetURL: The URL of asset for providing image data. + /// - seconds: At which time in seconds in the asset the image should be generated. + /// + /// This method uses `assetURL` to create an `AVAssetImageGenerator` object, uses `seconds` to create a `CMTime`, + /// and calls the `init(assetImageGenerator:time:)` initializer. + /// + public init(assetURL: URL, seconds: TimeInterval) { + let time = CMTime(seconds: seconds, preferredTimescale: 600) + self.init(assetURL: assetURL, time: time) + } + + public func data(handler: @escaping (Result) -> Void) { + assetImageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: time)]) { + (requestedTime, image, imageTime, result, error) in + if let error = error { + handler(.failure(error)) + return + } + + if result == .cancelled { + handler(.failure(AVAssetImageDataProviderError.userCancelled)) + return + } + + guard let cgImage = image, let data = cgImage.jpegData else { + handler(.failure(AVAssetImageDataProviderError.invalidImage(image))) + return + } + + handler(.success(data)) + } + } +} + +extension CGImage { + var jpegData: Data? { + guard let mutableData = CFDataCreateMutable(nil, 0), + let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) + else { + return nil + } + CGImageDestinationAddImage(destination, self, nil) + guard CGImageDestinationFinalize(destination) else { return nil } + return mutableData as Data + } +} + +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift new file mode 100644 index 0000000..e90ece6 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift @@ -0,0 +1,170 @@ +// +// ImageDataProvider.swift +// Kingfisher +// +// Created by onevcat on 2018/11/13. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a data provider to provide image data to Kingfisher when setting with +/// `Source.provider` source. Compared to `Source.network` member, it gives a chance +/// to load some image data in your own way, as long as you can provide the data +/// representation for the image. +public protocol ImageDataProvider { + + /// The key used in cache. + var cacheKey: String { get } + + /// Provides the data which represents image. Kingfisher uses the data you pass in the + /// handler to process images and caches it for later use. + /// + /// - Parameter handler: The handler you should call when you prepared your data. + /// If the data is loaded successfully, call the handler with + /// a `.success` with the data associated. Otherwise, call it + /// with a `.failure` and pass the error. + /// + /// - Note: + /// If the `handler` is called with a `.failure` with error, a `dataProviderError` of + /// `ImageSettingErrorReason` will be finally thrown out to you as the `KingfisherError` + /// from the framework. + func data(handler: @escaping (Result) -> Void) + + /// The content URL represents this provider, if exists. + var contentURL: URL? { get } +} + +public extension ImageDataProvider { + var contentURL: URL? { return nil } + func convertToSource() -> Source { + .provider(self) + } +} + +/// Represents an image data provider for loading from a local file URL on disk. +/// Uses this type for adding a disk image to Kingfisher. Compared to loading it +/// directly, you can get benefit of using Kingfisher's extension methods, as well +/// as applying `ImageProcessor`s and storing the image to `ImageCache` of Kingfisher. +public struct LocalFileImageDataProvider: ImageDataProvider { + + // MARK: Public Properties + + /// The file URL from which the image be loaded. + public let fileURL: URL + private let loadingQueue: ExecutionQueue + + // MARK: Initializers + + /// Creates an image data provider by supplying the target local file URL. + /// + /// - Parameters: + /// - fileURL: The file URL from which the image be loaded. + /// - cacheKey: The key is used for caching the image data. By default, + /// the `absoluteString` of `fileURL` is used. + /// - loadingQueue: The queue where the file loading should happen. By default, the dispatch queue of + /// `.global(qos: .userInitiated)` will be used. + public init( + fileURL: URL, + cacheKey: String? = nil, + loadingQueue: ExecutionQueue = .dispatch(DispatchQueue.global(qos: .userInitiated)) + ) { + self.fileURL = fileURL + self.cacheKey = cacheKey ?? fileURL.localFileCacheKey + self.loadingQueue = loadingQueue + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public var cacheKey: String + + public func data(handler:@escaping (Result) -> Void) { + loadingQueue.execute { + handler(Result(catching: { try Data(contentsOf: fileURL) })) + } + } + + /// The URL of the local file on the disk. + public var contentURL: URL? { + return fileURL + } +} + +/// Represents an image data provider for loading image from a given Base64 encoded string. +public struct Base64ImageDataProvider: ImageDataProvider { + + // MARK: Public Properties + /// The encoded Base64 string for the image. + public let base64String: String + + // MARK: Initializers + + /// Creates an image data provider by supplying the Base64 encoded string. + /// + /// - Parameters: + /// - base64String: The Base64 encoded string for an image. + /// - cacheKey: The key is used for caching the image data. You need a different key for any different image. + public init(base64String: String, cacheKey: String) { + self.base64String = base64String + self.cacheKey = cacheKey + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public var cacheKey: String + + public func data(handler: (Result) -> Void) { + let data = Data(base64Encoded: base64String)! + handler(.success(data)) + } +} + +/// Represents an image data provider for a raw data object. +public struct RawImageDataProvider: ImageDataProvider { + + // MARK: Public Properties + + /// The raw data object to provide to Kingfisher image loader. + public let data: Data + + // MARK: Initializers + + /// Creates an image data provider by the given raw `data` value and a `cacheKey` be used in Kingfisher cache. + /// + /// - Parameters: + /// - data: The raw data reprensents an image. + /// - cacheKey: The key is used for caching the image data. You need a different key for any different image. + public init(data: Data, cacheKey: String) { + self.data = data + self.cacheKey = cacheKey + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public var cacheKey: String + + public func data(handler: @escaping (Result) -> Void) { + handler(.success(data)) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift new file mode 100644 index 0000000..1496f3d --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift @@ -0,0 +1,115 @@ +// +// Resource.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents an image resource at a certain url and a given cache key. +/// Kingfisher will use a `Resource` to download a resource from network and cache it with the cache key when +/// using `Source.network` as its image setting source. +public protocol Resource { + + /// The key used in cache. + var cacheKey: String { get } + + /// The target image URL. + var downloadURL: URL { get } +} + +extension Resource { + + /// Converts `self` to a valid `Source` based on its `downloadURL` scheme. A `.provider` with + /// `LocalFileImageDataProvider` associated will be returned if the URL points to a local file. Otherwise, + /// `.network` is returned. + public func convertToSource(overrideCacheKey: String? = nil) -> Source { + let key = overrideCacheKey ?? cacheKey + return downloadURL.isFileURL ? + .provider(LocalFileImageDataProvider(fileURL: downloadURL, cacheKey: key)) : + .network(ImageResource(downloadURL: downloadURL, cacheKey: key)) + } +} + +/// ImageResource is a simple combination of `downloadURL` and `cacheKey`. +/// When passed to image view set methods, Kingfisher will try to download the target +/// image from the `downloadURL`, and then store it with the `cacheKey` as the key in cache. +public struct ImageResource: Resource { + + // MARK: - Initializers + + /// Creates an image resource. + /// + /// - Parameters: + /// - downloadURL: The target image URL from where the image can be downloaded. + /// - cacheKey: The cache key. If `nil`, Kingfisher will use the `absoluteString` of `downloadURL` as the key. + /// Default is `nil`. + public init(downloadURL: URL, cacheKey: String? = nil) { + self.downloadURL = downloadURL + self.cacheKey = cacheKey ?? downloadURL.cacheKey + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public let cacheKey: String + + /// The target image URL. + public let downloadURL: URL +} + +/// URL conforms to `Resource` in Kingfisher. +/// The `absoluteString` of this URL is used as `cacheKey`. And the URL itself will be used as `downloadURL`. +/// If you need customize the url and/or cache key, use `ImageResource` instead. +extension URL: Resource { + public var cacheKey: String { return isFileURL ? localFileCacheKey : absoluteString } + public var downloadURL: URL { return self } +} + +extension URL { + static let localFileCacheKeyPrefix = "kingfisher.local.cacheKey" + + // The special version of cache key for a local file on disk. Every time the app is reinstalled on the disk, + // the system assigns a new container folder to hold the .app (and the extensions, .appex) folder. So the URL for + // the same image in bundle might be different. + // + // This getter only uses the fixed part in the URL (until the bundle name folder) to provide a stable cache key + // for the image under the same path inside the bundle. + // + // See #1825 (https://github.com/onevcat/Kingfisher/issues/1825) + var localFileCacheKey: String { + var validComponents: [String] = [] + for part in pathComponents.reversed() { + validComponents.append(part) + if part.hasSuffix(".app") || part.hasSuffix(".appex") { + break + } + } + let fixedPath = "\(Self.localFileCacheKeyPrefix)/\(validComponents.reversed().joined(separator: "/"))" + if let q = query { + return "\(fixedPath)?\(q)" + } else { + return fixedPath + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/Source.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/Source.swift new file mode 100644 index 0000000..0fcf28b --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/Source.swift @@ -0,0 +1,123 @@ +// +// Source.swift +// Kingfisher +// +// Created by onevcat on 2018/11/17. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents an image setting source for Kingfisher methods. +/// +/// A `Source` value indicates the way how the target image can be retrieved and cached. +/// +/// - network: The target image should be got from network remotely. The associated `Resource` +/// value defines detail information like image URL and cache key. +/// - provider: The target image should be provided in a data format. Normally, it can be an image +/// from local storage or in any other encoding format (like Base64). +public enum Source { + + /// Represents the source task identifier when setting an image to a view with extension methods. + public enum Identifier { + + /// The underlying value type of source identifier. + public typealias Value = UInt + static var current: Value = 0 + static func next() -> Value { + current += 1 + return current + } + } + + // MARK: Member Cases + + /// The target image should be got from network remotely. The associated `Resource` + /// value defines detail information like image URL and cache key. + case network(Resource) + + /// The target image should be provided in a data format. Normally, it can be an image + /// from local storage or in any other encoding format (like Base64). + case provider(ImageDataProvider) + + // MARK: Getting Properties + + /// The cache key defined for this source value. + public var cacheKey: String { + switch self { + case .network(let resource): return resource.cacheKey + case .provider(let provider): return provider.cacheKey + } + } + + /// The URL defined for this source value. + /// + /// For a `.network` source, it is the `downloadURL` of associated `Resource` instance. + /// For a `.provider` value, it is always `nil`. + public var url: URL? { + switch self { + case .network(let resource): return resource.downloadURL + case .provider(let provider): return provider.contentURL + } + } +} + +extension Source: Hashable { + public static func == (lhs: Source, rhs: Source) -> Bool { + switch (lhs, rhs) { + case (.network(let r1), .network(let r2)): + return r1.cacheKey == r2.cacheKey && r1.downloadURL == r2.downloadURL + case (.provider(let p1), .provider(let p2)): + return p1.cacheKey == p2.cacheKey && p1.contentURL == p2.contentURL + case (.provider(_), .network(_)): + return false + case (.network(_), .provider(_)): + return false + } + } + + public func hash(into hasher: inout Hasher) { + switch self { + case .network(let r): + hasher.combine(r.cacheKey) + hasher.combine(r.downloadURL) + case .provider(let p): + hasher.combine(p.cacheKey) + hasher.combine(p.contentURL) + } + } +} + +extension Source { + var asResource: Resource? { + guard case .network(let resource) = self else { + return nil + } + return resource + } + + var asProvider: ImageDataProvider? { + guard case .provider(let provider) = self else { + return nil + } + return provider + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KF.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KF.swift new file mode 100644 index 0000000..3b033b9 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KF.swift @@ -0,0 +1,442 @@ +// +// KF.swift +// Kingfisher +// +// Created by onevcat on 2020/09/21. +// +// Copyright (c) 2020 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(UIKit) +import UIKit +#endif + +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +import CarPlay +#endif + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif + +#if canImport(WatchKit) +import WatchKit +#endif + +#if canImport(TVUIKit) +import TVUIKit +#endif + +/// A helper type to create image setting tasks in a builder pattern. +/// Use methods in this type to create a `KF.Builder` instance and configure image tasks there. +public enum KF { + + /// Creates a builder for a given `Source`. + /// - Parameter source: The `Source` object defines data information from network or a data provider. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func source(_ source: Source?) -> KF.Builder { + Builder(source: source) + } + + /// Creates a builder for a given `Resource`. + /// - Parameter resource: The `Resource` object defines data information like key or URL. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func resource(_ resource: Resource?) -> KF.Builder { + source(resource?.convertToSource()) + } + + /// Creates a builder for a given `URL` and an optional cache key. + /// - Parameters: + /// - url: The URL where the image should be downloaded. + /// - cacheKey: The key used to store the downloaded image in cache. + /// If `nil`, the `absoluteString` of `url` is used as the cache key. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func url(_ url: URL?, cacheKey: String? = nil) -> KF.Builder { + source(url?.convertToSource(overrideCacheKey: cacheKey)) + } + + /// Creates a builder for a given `ImageDataProvider`. + /// - Parameter provider: The `ImageDataProvider` object contains information about the data. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func dataProvider(_ provider: ImageDataProvider?) -> KF.Builder { + source(provider?.convertToSource()) + } + + /// Creates a builder for some given raw data and a cache key. + /// - Parameters: + /// - data: The data object from which the image should be created. + /// - cacheKey: The key used to store the downloaded image in cache. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func data(_ data: Data?, cacheKey: String) -> KF.Builder { + if let data = data { + return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey)) + } else { + return dataProvider(nil) + } + } +} + + +extension KF { + + /// A builder class to configure an image retrieving task and set it to a holder view or component. + public class Builder { + private let source: Source? + + #if os(watchOS) + private var placeholder: KFCrossPlatformImage? + #else + private var placeholder: Placeholder? + #endif + + public var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions) + + public let onFailureDelegate = Delegate() + public let onSuccessDelegate = Delegate() + public let onProgressDelegate = Delegate<(Int64, Int64), Void>() + + init(source: Source?) { + self.source = source + } + + private var resultHandler: ((Result) -> Void)? { + { + switch $0 { + case .success(let result): + self.onSuccessDelegate(result) + case .failure(let error): + self.onFailureDelegate(error) + } + } + } + + private var progressBlock: DownloadProgressBlock { + { self.onProgressDelegate(($0, $1)) } + } + } +} + +extension KF.Builder { + #if !os(watchOS) + + /// Builds the image task request and sets it to an image view. + /// - Parameter imageView: The image view which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to imageView: KFCrossPlatformImageView) -> DownloadTask? { + imageView.kf.setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + /// Builds the image task request and sets it to an `NSTextAttachment` object. + /// - Parameters: + /// - attachment: The text attachment object which loads the task and should be set with the image. + /// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to attachment: NSTextAttachment, attributedView: @autoclosure @escaping () -> KFCrossPlatformView) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return attachment.kf.setImage( + with: source, + attributedView: attributedView, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + #if canImport(UIKit) + + /// Builds the image task request and sets it to a button. + /// - Parameters: + /// - button: The button which loads the task and should be set with the image. + /// - state: The button state to which the image should be set. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to button: UIButton, for state: UIControl.State) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setImage( + with: source, + for: state, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + /// Builds the image task request and sets it to the background image for a button. + /// - Parameters: + /// - button: The button which loads the task and should be set with the image. + /// - state: The button state to which the image should be set. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func setBackground(to button: UIButton, for state: UIControl.State) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setBackgroundImage( + with: source, + for: state, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(UIKit) + + #if canImport(CarPlay) && !targetEnvironment(macCatalyst) + + /// Builds the image task request and sets it to the image for a list item. + /// - Parameters: + /// - listItem: The list item which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @available(iOS 14.0, *) + @discardableResult + public func set(to listItem: CPListItem) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return listItem.kf.setImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + + } + + #endif + + #if canImport(AppKit) && !targetEnvironment(macCatalyst) + /// Builds the image task request and sets it to a button. + /// - Parameter button: The button which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to button: NSButton) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + /// Builds the image task request and sets it to the alternative image for a button. + /// - Parameter button: The button which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func setAlternative(to button: NSButton) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setAlternateImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(AppKit) + #endif // end of !os(watchOS) + + #if canImport(WatchKit) + /// Builds the image task request and sets it to a `WKInterfaceImage` object. + /// - Parameter interfaceImage: The watch interface image which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to interfaceImage: WKInterfaceImage) -> DownloadTask? { + return interfaceImage.kf.setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(WatchKit) + + #if canImport(TVUIKit) + /// Builds the image task request and sets it to a TV monogram view. + /// - Parameter monogramView: The monogram view which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @available(tvOS 12.0, *) + @discardableResult + public func set(to monogramView: TVMonogramView) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return monogramView.kf.setImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(TVUIKit) +} + +#if !os(watchOS) +extension KF.Builder { + #if os(iOS) || os(tvOS) + + /// Sets a placeholder which is used while retrieving the image. + /// - Parameter placeholder: A placeholder to show while retrieving the image from its source. + /// - Returns: A `KF.Builder` with changes applied. + public func placeholder(_ placeholder: Placeholder?) -> Self { + self.placeholder = placeholder + return self + } + #endif + + /// Sets a placeholder image which is used while retrieving the image. + /// - Parameter placeholder: An image to show while retrieving the image from its source. + /// - Returns: A `KF.Builder` with changes applied. + public func placeholder(_ image: KFCrossPlatformImage?) -> Self { + self.placeholder = image + return self + } +} +#endif + +extension KF.Builder { + + #if os(iOS) || os(tvOS) + /// Sets the transition for the image task. + /// - Parameter transition: The desired transition effect when setting the image to image view. + /// - Returns: A `KF.Builder` with changes applied. + /// + /// Kingfisher will use the `transition` to animate the image in if it is downloaded from web. + /// The transition will not happen when the + /// image is retrieved from either memory or disk cache by default. If you need to do the transition even when + /// the image being retrieved from cache, also call `forceRefresh()` on the returned `KF.Builder`. + public func transition(_ transition: ImageTransition) -> Self { + options.transition = transition + return self + } + + /// Sets a fade transition for the image task. + /// - Parameter duration: The duration of the fade transition. + /// - Returns: A `KF.Builder` with changes applied. + /// + /// Kingfisher will use the fade transition to animate the image in if it is downloaded from web. + /// The transition will not happen when the + /// image is retrieved from either memory or disk cache by default. If you need to do the transition even when + /// the image being retrieved from cache, also call `forceRefresh()` on the returned `KF.Builder`. + public func fade(duration: TimeInterval) -> Self { + options.transition = .fade(duration) + return self + } + #endif + + /// Sets whether keeping the existing image of image view while setting another image to it. + /// - Parameter enabled: Whether the existing image should be kept. + /// - Returns: A `KF.Builder` with changes applied. + /// + /// By setting this option, the placeholder image parameter of image view extension method + /// will be ignored and the current image will be kept while loading or downloading the new image. + /// + public func keepCurrentImageWhileLoading(_ enabled: Bool = true) -> Self { + options.keepCurrentImageWhileLoading = enabled + return self + } + + /// Sets whether only the first frame from an animated image file should be loaded as a single image. + /// - Parameter enabled: Whether the only the first frame should be loaded. + /// - Returns: A `KF.Builder` with changes applied. + /// + /// Loading an animated images may take too much memory. It will be useful when you want to display a + /// static preview of the first frame from an animated image. + /// + /// This option will be ignored if the target image is not animated image data. + /// + public func onlyLoadFirstFrame(_ enabled: Bool = true) -> Self { + options.onlyLoadFirstFrame = enabled + return self + } + + /// Enables progressive image loading with a specified `ImageProgressive` setting to process the + /// progressive JPEG data and display it in a progressive way. + /// - Parameter progressive: The progressive settings which is used while loading. + /// - Returns: A `KF.Builder` with changes applied. + public func progressiveJPEG(_ progressive: ImageProgressive? = .default) -> Self { + options.progressiveJPEG = progressive + return self + } +} + +// MARK: - Deprecated +extension KF.Builder { + /// Starts the loading process of `self` immediately. + /// + /// By default, a `KFImage` will not load its source until the `onAppear` is called. This is a lazily loading + /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a + /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once + /// could help avoiding the flickering, with some performance trade-off. + /// + /// - Deprecated: This is not necessary anymore since `@StateObject` is used for holding the image data. + /// It does nothing now and please just remove it. + /// + /// - Returns: The `Self` value with changes applied. + @available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.") + public func loadImmediately(_ start: Bool = true) -> Self { + return self + } +} + +// MARK: - Redirect Handler +extension KF { + + /// Represents the detail information when a task redirect happens. It is wrapping necessary information for a + /// `ImageDownloadRedirectHandler`. See that protocol for more information. + public struct RedirectPayload { + + /// The related session data task when the redirect happens. It is + /// the current `SessionDataTask` which triggers this redirect. + public let task: SessionDataTask + + /// The response received during redirection. + public let response: HTTPURLResponse + + /// The request for redirection which can be modified. + public let newRequest: URLRequest + + /// A closure for being called with modified request. + public let completionHandler: (URLRequest?) -> Void + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift new file mode 100644 index 0000000..c773593 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift @@ -0,0 +1,707 @@ +// +// KFOptionsSetter.swift +// Kingfisher +// +// Created by onevcat on 2020/12/22. +// +// Copyright (c) 2020 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CoreGraphics + +public protocol KFOptionSetter { + var options: KingfisherParsedOptionsInfo { get nonmutating set } + + var onFailureDelegate: Delegate { get } + var onSuccessDelegate: Delegate { get } + var onProgressDelegate: Delegate<(Int64, Int64), Void> { get } + + var delegateObserver: AnyObject { get } +} + +extension KF.Builder: KFOptionSetter { + public var delegateObserver: AnyObject { self } +} + +// MARK: - Life cycles +extension KFOptionSetter { + /// Sets the progress block to current builder. + /// - Parameter block: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. If `block` is `nil`, the callback + /// will be reset. + /// - Returns: A `Self` value with changes applied. + public func onProgress(_ block: DownloadProgressBlock?) -> Self { + onProgressDelegate.delegate(on: delegateObserver) { (observer, result) in + block?(result.0, result.1) + } + return self + } + + /// Sets the the done block to current builder. + /// - Parameter block: Called when the image task successfully completes and the the image set is done. If `block` + /// is `nil`, the callback will be reset. + /// - Returns: A `KF.Builder` with changes applied. + public func onSuccess(_ block: ((RetrieveImageResult) -> Void)?) -> Self { + onSuccessDelegate.delegate(on: delegateObserver) { (observer, result) in + block?(result) + } + return self + } + + /// Sets the catch block to current builder. + /// - Parameter block: Called when an error happens during the image task. If `block` + /// is `nil`, the callback will be reset. + /// - Returns: A `KF.Builder` with changes applied. + public func onFailure(_ block: ((KingfisherError) -> Void)?) -> Self { + onFailureDelegate.delegate(on: delegateObserver) { (observer, error) in + block?(error) + } + return self + } +} + +// MARK: - Basic options settings. +extension KFOptionSetter { + /// Sets the target image cache for this task. + /// - Parameter cache: The target cache is about to be used for the task. + /// - Returns: A `Self` value with changes applied. + /// + /// Kingfisher will use the associated `ImageCache` object when handling related operations, + /// including trying to retrieve the cached images and store the downloaded image to it. + /// + public func targetCache(_ cache: ImageCache) -> Self { + options.targetCache = cache + return self + } + + /// Sets the target image cache to store the original downloaded image for this task. + /// - Parameter cache: The target cache is about to be used for storing the original downloaded image from the task. + /// - Returns: A `Self` value with changes applied. + /// + /// The `ImageCache` for storing and retrieving original images. If `originalCache` is + /// contained in the options, it will be preferred for storing and retrieving original images. + /// If there is no `.originalCache` in the options, `.targetCache` will be used to store original images. + /// + /// When using KingfisherManager to download and store an image, if `cacheOriginalImage` is + /// applied in the option, the original image will be stored to this `originalCache`. At the + /// same time, if a requested final image (with processor applied) cannot be found in `targetCache`, + /// Kingfisher will try to search the original image to check whether it is already there. If found, + /// it will be used and applied with the given processor. It is an optimization for not downloading + /// the same image for multiple times. + /// + public func originalCache(_ cache: ImageCache) -> Self { + options.originalCache = cache + return self + } + + /// Sets the downloader used to perform the image download task. + /// - Parameter downloader: The downloader which is about to be used for downloading. + /// - Returns: A `Self` value with changes applied. + /// + /// Kingfisher will use the set `ImageDownloader` object to download the requested images. + public func downloader(_ downloader: ImageDownloader) -> Self { + options.downloader = downloader + return self + } + + /// Sets the download priority for the image task. + /// - Parameter priority: The download priority of image download task. + /// - Returns: A `Self` value with changes applied. + /// + /// The `priority` value will be set as the priority of the image download task. The value for it should be + /// between 0.0~1.0. You can choose a value between `URLSessionTask.defaultPriority`, `URLSessionTask.lowPriority` + /// or `URLSessionTask.highPriority`. If this option not set, the default value (`URLSessionTask.defaultPriority`) + /// will be used. + public func downloadPriority(_ priority: Float) -> Self { + options.downloadPriority = priority + return self + } + + /// Sets whether Kingfisher should ignore the cache and try to start a download task for the image source. + /// - Parameter enabled: Enable the force refresh or not. + /// - Returns: A `Self` value with changes applied. + public func forceRefresh(_ enabled: Bool = true) -> Self { + options.forceRefresh = enabled + return self + } + + /// Sets whether Kingfisher should try to retrieve the image from memory cache first. If not found, it ignores the + /// disk cache and starts a download task for the image source. + /// - Parameter enabled: Enable the memory-only cache searching or not. + /// - Returns: A `Self` value with changes applied. + /// + /// This is useful when you want to display a changeable image behind the same url at the same app session, while + /// avoiding download it for multiple times. + public func fromMemoryCacheOrRefresh(_ enabled: Bool = true) -> Self { + options.fromMemoryCacheOrRefresh = enabled + return self + } + + /// Sets whether the image should only be cached in memory but not in disk. + /// - Parameter enabled: Whether the image should be only cache in memory or not. + /// - Returns: A `Self` value with changes applied. + public func cacheMemoryOnly(_ enabled: Bool = true) -> Self { + options.cacheMemoryOnly = enabled + return self + } + + /// Sets whether Kingfisher should wait for caching operation to be completed before calling the + /// `onSuccess` or `onFailure` block. + /// - Parameter enabled: Whether Kingfisher should wait for caching operation. + /// - Returns: A `Self` value with changes applied. + public func waitForCache(_ enabled: Bool = true) -> Self { + options.waitForCache = enabled + return self + } + + /// Sets whether Kingfisher should only try to retrieve the image from cache, but not from network. + /// - Parameter enabled: Whether Kingfisher should only try to retrieve the image from cache. + /// - Returns: A `Self` value with changes applied. + /// + /// If the image is not in cache, the image retrieving will fail with the + /// `KingfisherError.cacheError` with `.imageNotExisting` as its reason. + public func onlyFromCache(_ enabled: Bool = true) -> Self { + options.onlyFromCache = enabled + return self + } + + /// Sets whether the image should be decoded in a background thread before using. + /// - Parameter enabled: Whether the image should be decoded in a background thread before using. + /// - Returns: A `Self` value with changes applied. + /// + /// Setting to `true` will decode the downloaded image data and do a off-screen rendering to extract pixel + /// information in background. This can speed up display, but will cost more time and memory to prepare the image + /// for using. + public func backgroundDecode(_ enabled: Bool = true) -> Self { + options.backgroundDecode = enabled + return self + } + + /// Sets the callback queue which is used as the target queue of dispatch callbacks when retrieving images from + /// cache. If not set, Kingfisher will use main queue for callbacks. + /// - Parameter queue: The target queue which the cache retrieving callback will be invoked on. + /// - Returns: A `Self` value with changes applied. + /// + /// - Note: + /// This option does not affect the callbacks for UI related extension methods or `KFImage` result handlers. + /// You will always get the callbacks called from main queue. + public func callbackQueue(_ queue: CallbackQueue) -> Self { + options.callbackQueue = queue + return self + } + + /// Sets the scale factor value when converting retrieved data to an image. + /// - Parameter factor: The scale factor value. + /// - Returns: A `Self` value with changes applied. + /// + /// Specify the image scale, instead of your screen scale. You may need to set the correct scale when you dealing + /// with 2x or 3x retina images. Otherwise, Kingfisher will convert the data to image object at `scale` 1.0. + /// + public func scaleFactor(_ factor: CGFloat) -> Self { + options.scaleFactor = factor + return self + } + + /// Sets whether the original image should be cached even when the original image has been processed by any other + /// `ImageProcessor`s. + /// - Parameter enabled: Whether the original image should be cached. + /// - Returns: A `Self` value with changes applied. + /// + /// If set and an `ImageProcessor` is used, Kingfisher will try to cache both the final result and original + /// image. Kingfisher will have a chance to use the original image when another processor is applied to the same + /// resource, instead of downloading it again. You can use `.originalCache` to specify a cache or the original + /// images if necessary. + /// + /// The original image will be only cached to disk storage. + /// + public func cacheOriginalImage(_ enabled: Bool = true) -> Self { + options.cacheOriginalImage = enabled + return self + } + + /// Sets writing options for an original image on a first write + /// - Parameter writingOptions: Options to control the writing of data to a disk storage. + /// - Returns: A `Self` value with changes applied. + /// If set, options will be passed the store operation for a new files. + /// + /// This is useful if you want to implement file enctyption on first write - eg [.completeFileProtection] + /// + public func diskStoreWriteOptions(_ writingOptions: Data.WritingOptions) -> Self { + options.diskStoreWriteOptions = writingOptions + return self + } + + /// Sets whether the disk storage loading should happen in the same calling queue. + /// - Parameter enabled: Whether the disk storage loading should happen in the same calling queue. + /// - Returns: A `Self` value with changes applied. + /// + /// By default, disk storage file loading + /// happens in its own queue with an asynchronous dispatch behavior. Although it provides better non-blocking disk + /// loading performance, it also causes a flickering when you reload an image from disk, if the image view already + /// has an image set. + /// + /// Set this options will stop that flickering by keeping all loading in the same queue (typically the UI queue + /// if you are using Kingfisher's extension methods to set an image), with a tradeoff of loading performance. + /// + public func loadDiskFileSynchronously(_ enabled: Bool = true) -> Self { + options.loadDiskFileSynchronously = enabled + return self + } + + /// Sets a queue on which the image processing should happen. + /// - Parameter queue: The queue on which the image processing should happen. + /// - Returns: A `Self` value with changes applied. + /// + /// By default, Kingfisher uses a pre-defined serial + /// queue to process images. Use this option to change this behavior. For example, specify a `.mainCurrentOrAsync` + /// to let the image be processed in main queue to prevent a possible flickering (but with a possibility of + /// blocking the UI, especially if the processor needs a lot of time to run). + public func processingQueue(_ queue: CallbackQueue?) -> Self { + options.processingQueue = queue + return self + } + + /// Sets the alternative sources that will be used when loading of the original input `Source` fails. + /// - Parameter sources: The alternative sources will be used. + /// - Returns: A `Self` value with changes applied. + /// + /// Values of the `sources` array will be used to start a new image loading task if the previous task + /// fails due to an error. The image source loading process will stop as soon as a source is loaded successfully. + /// If all `sources` are used but the loading is still failing, an `imageSettingError` with + /// `alternativeSourcesExhausted` as its reason will be given out in the `catch` block. + /// + /// This is useful if you want to implement a fallback solution for setting image. + /// + /// User cancellation will not trigger the alternative source loading. + public func alternativeSources(_ sources: [Source]?) -> Self { + options.alternativeSources = sources + return self + } + + /// Sets a retry strategy that will be used when something gets wrong during the image retrieving. + /// - Parameter strategy: The provided strategy to define how the retrying should happen. + /// - Returns: A `Self` value with changes applied. + public func retry(_ strategy: RetryStrategy?) -> Self { + options.retryStrategy = strategy + return self + } + + /// Sets a retry strategy with a max retry count and retrying interval. + /// - Parameters: + /// - maxCount: The maximum count before the retry stops. + /// - interval: The time interval between each retry attempt. + /// - Returns: A `Self` value with changes applied. + /// + /// This defines the simplest retry strategy, which retry a failing request for several times, with some certain + /// interval between each time. For example, `.retry(maxCount: 3, interval: .second(3))` means attempt for at most + /// three times, and wait for 3 seconds if a previous retry attempt fails, then start a new attempt. + public func retry(maxCount: Int, interval: DelayRetryStrategy.Interval = .seconds(3)) -> Self { + let strategy = DelayRetryStrategy(maxRetryCount: maxCount, retryInterval: interval) + options.retryStrategy = strategy + return self + } + + /// Sets the `Source` should be loaded when user enables Low Data Mode and the original source fails with an + /// `NSURLErrorNetworkUnavailableReason.constrained` error. + /// - Parameter source: The `Source` will be loaded under low data mode. + /// - Returns: A `Self` value with changes applied. + /// + /// When this option is set, the + /// `allowsConstrainedNetworkAccess` property of the request for the original source will be set to `false` and the + /// `Source` in associated value will be used to retrieve the image for low data mode. Usually, you can provide a + /// low-resolution version of your image or a local image provider to display a placeholder. + /// + /// If not set or the `source` is `nil`, the device Low Data Mode will be ignored and the original source will + /// be loaded following the system default behavior, in a normal way. + public func lowDataModeSource(_ source: Source?) -> Self { + options.lowDataModeSource = source + return self + } + + /// Sets whether the image setting for an image view should happen with transition even when retrieved from cache. + /// - Parameter enabled: Enable the force transition or not. + /// - Returns: A `Self` with changes applied. + public func forceTransition(_ enabled: Bool = true) -> Self { + options.forceTransition = enabled + return self + } + + /// Sets the image that will be used if an image retrieving task fails. + /// - Parameter image: The image that will be used when something goes wrong. + /// - Returns: A `Self` with changes applied. + /// + /// If set and an image retrieving error occurred Kingfisher will set provided image (or empty) + /// in place of requested one. It's useful when you don't want to show placeholder + /// during loading time but wants to use some default image when requests will be failed. + /// + public func onFailureImage(_ image: KFCrossPlatformImage?) -> Self { + options.onFailureImage = .some(image) + return self + } +} + +// MARK: - Request Modifier +extension KFOptionSetter { + /// Sets an `ImageDownloadRequestModifier` to change the image download request before it being sent. + /// - Parameter modifier: The modifier will be used to change the request before it being sent. + /// - Returns: A `Self` value with changes applied. + /// + /// This is the last chance you can modify the image download request. You can modify the request for some + /// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping. + /// + public func requestModifier(_ modifier: AsyncImageDownloadRequestModifier) -> Self { + options.requestModifier = modifier + return self + } + + /// Sets a block to change the image download request before it being sent. + /// - Parameter modifyBlock: The modifying block will be called to change the request before it being sent. + /// - Returns: A `Self` value with changes applied. + /// + /// This is the last chance you can modify the image download request. You can modify the request for some + /// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping. + /// + public func requestModifier(_ modifyBlock: @escaping (inout URLRequest) -> Void) -> Self { + options.requestModifier = AnyModifier { r -> URLRequest? in + var request = r + modifyBlock(&request) + return request + } + return self + } +} + +// MARK: - Redirect Handler +extension KFOptionSetter { + /// The `ImageDownloadRedirectHandler` argument will be used to change the request before redirection. + /// This is the possibility you can modify the image download request during redirect. You can modify the request for + /// some customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url + /// mapping. + /// The original redirection request will be sent without any modification by default. + /// - Parameter handler: The handler will be used for redirection. + /// - Returns: A `Self` value with changes applied. + public func redirectHandler(_ handler: ImageDownloadRedirectHandler) -> Self { + options.redirectHandler = handler + return self + } + + /// The `block` will be used to change the request before redirection. + /// This is the possibility you can modify the image download request during redirect. You can modify the request for + /// some customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url + /// mapping. + /// The original redirection request will be sent without any modification by default. + /// - Parameter block: The block will be used for redirection. + /// - Returns: A `Self` value with changes applied. + public func redirectHandler(_ block: @escaping (KF.RedirectPayload) -> Void) -> Self { + let redirectHandler = AnyRedirectHandler { (task, response, request, handler) in + let payload = KF.RedirectPayload( + task: task, response: response, newRequest: request, completionHandler: handler + ) + block(payload) + } + options.redirectHandler = redirectHandler + return self + } +} + +// MARK: - Processor +extension KFOptionSetter { + + /// Sets an image processor for the image task. It replaces the current image processor settings. + /// + /// - Parameter processor: The processor you want to use to process the image after it is downloaded. + /// - Returns: A `Self` value with changes applied. + /// + /// - Note: + /// To append a processor to current ones instead of replacing them all, use `appendProcessor(_:)`. + public func setProcessor(_ processor: ImageProcessor) -> Self { + options.processor = processor + return self + } + + /// Sets an array of image processors for the image task. It replaces the current image processor settings. + /// - Parameter processors: An array of processors. The processors inside this array will be concatenated one by one + /// to form a processor pipeline. + /// - Returns: A `Self` value with changes applied. + /// + /// - Note: + /// To append processors to current ones instead of replacing them all, concatenate them by `|>`, then use + /// `appendProcessor(_:)`. + public func setProcessors(_ processors: [ImageProcessor]) -> Self { + switch processors.count { + case 0: + options.processor = DefaultImageProcessor.default + case 1...: + options.processor = processors.dropFirst().reduce(processors[0]) { $0 |> $1 } + default: + assertionFailure("Never happen") + } + return self + } + + /// Appends a processor to the current set processors. + /// - Parameter processor: The processor which will be appended to current processor settings. + /// - Returns: A `Self` value with changes applied. + public func appendProcessor(_ processor: ImageProcessor) -> Self { + options.processor = options.processor |> processor + return self + } + + /// Appends a `RoundCornerImageProcessor` to current processors. + /// - Parameters: + /// - radius: The radius will be applied in processing. Specify a certain point value with `.point`, or a fraction + /// of the target image with `.widthFraction`. or `.heightFraction`. For example, given a square image + /// with width and height equals, `.widthFraction(0.5)` means use half of the length of size and makes + /// the final image a round one. + /// - targetSize: Target size of output image should be. If `nil`, the image will keep its original size after processing. + /// - corners: The target corners which will be applied rounding. + /// - backgroundColor: Background color of the output image. If `nil`, it will use a transparent background. + /// - Returns: A `Self` value with changes applied. + public func roundCorner( + radius: Radius, + targetSize: CGSize? = nil, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) -> Self + { + let processor = RoundCornerImageProcessor( + radius: radius, + targetSize: targetSize, + roundingCorners: corners, + backgroundColor: backgroundColor + ) + return appendProcessor(processor) + } + + /// Appends a `BlurImageProcessor` to current processors. + /// - Parameter radius: Blur radius for the simulated Gaussian blur. + /// - Returns: A `Self` value with changes applied. + public func blur(radius: CGFloat) -> Self { + appendProcessor( + BlurImageProcessor(blurRadius: radius) + ) + } + + /// Appends a `OverlayImageProcessor` to current processors. + /// - Parameters: + /// - color: Overlay color will be used to overlay the input image. + /// - fraction: Fraction will be used when overlay the color to image. + /// - Returns: A `Self` value with changes applied. + public func overlay(color: KFCrossPlatformColor, fraction: CGFloat = 0.5) -> Self { + appendProcessor( + OverlayImageProcessor(overlay: color, fraction: fraction) + ) + } + + /// Appends a `TintImageProcessor` to current processors. + /// - Parameter color: Tint color will be used to tint the input image. + /// - Returns: A `Self` value with changes applied. + public func tint(color: KFCrossPlatformColor) -> Self { + appendProcessor( + TintImageProcessor(tint: color) + ) + } + + /// Appends a `BlackWhiteProcessor` to current processors. + /// - Returns: A `Self` value with changes applied. + public func blackWhite() -> Self { + appendProcessor( + BlackWhiteProcessor() + ) + } + + /// Appends a `CroppingImageProcessor` to current processors. + /// - Parameters: + /// - size: Target size of output image should be. + /// - anchor: Anchor point from which the output size should be calculate. The anchor point is consisted by two + /// values between 0.0 and 1.0. It indicates a related point in current image. + /// See `CroppingImageProcessor.init(size:anchor:)` for more. + /// - Returns: A `Self` value with changes applied. + public func cropping(size: CGSize, anchor: CGPoint = .init(x: 0.5, y: 0.5)) -> Self { + appendProcessor( + CroppingImageProcessor(size: size, anchor: anchor) + ) + } + + /// Appends a `DownsamplingImageProcessor` to current processors. + /// + /// Compared to `ResizingImageProcessor`, the `DownsamplingImageProcessor` does not render the original images and + /// then resize it. Instead, it downsamples the input data directly to a thumbnail image. So it is a more efficient + /// than `ResizingImageProcessor`. Prefer to use `DownsamplingImageProcessor` as possible + /// as you can than the `ResizingImageProcessor`. + /// + /// Only CG-based images are supported. Animated images (like GIF) is not supported. + /// + /// - Parameter size: Target size of output image should be. It should be smaller than the size of input image. + /// If it is larger, the result image will be the same size of input data without downsampling. + /// - Returns: A `Self` value with changes applied. + public func downsampling(size: CGSize) -> Self { + let processor = DownsamplingImageProcessor(size: size) + if options.processor == DefaultImageProcessor.default { + return setProcessor(processor) + } else { + return appendProcessor(processor) + } + } + + + /// Appends a `ResizingImageProcessor` to current processors. + /// + /// If you need to resize a data represented image to a smaller size, use `DownsamplingImageProcessor` + /// instead, which is more efficient and uses less memory. + /// + /// - Parameters: + /// - referenceSize: The reference size for resizing operation in point. + /// - mode: Target content mode of output image should be. Default is `.none`. + /// - Returns: A `Self` value with changes applied. + public func resizing(referenceSize: CGSize, mode: ContentMode = .none) -> Self { + appendProcessor( + ResizingImageProcessor(referenceSize: referenceSize, mode: mode) + ) + } +} + +// MARK: - Cache Serializer +extension KFOptionSetter { + + /// Uses a given `CacheSerializer` to convert some data to an image object for retrieving from disk cache or vice + /// versa for storing to disk cache. + /// - Parameter cacheSerializer: The `CacheSerializer` which will be used. + /// - Returns: A `Self` value with changes applied. + public func serialize(by cacheSerializer: CacheSerializer) -> Self { + options.cacheSerializer = cacheSerializer + return self + } + + /// Uses a given format to serializer the image data to disk. It converts the image object to the give data format. + /// - Parameters: + /// - format: The desired data encoding format when store the image on disk. + /// - jpegCompressionQuality: If the format is `.JPEG`, it specify the compression quality when converting the + /// image to a JPEG data. Otherwise, it is ignored. + /// - Returns: A `Self` value with changes applied. + public func serialize(as format: ImageFormat, jpegCompressionQuality: CGFloat? = nil) -> Self { + let cacheSerializer: FormatIndicatedCacheSerializer + switch format { + case .JPEG: + cacheSerializer = .jpeg(compressionQuality: jpegCompressionQuality ?? 1.0) + case .PNG: + cacheSerializer = .png + case .GIF: + cacheSerializer = .gif + case .unknown: + cacheSerializer = .png + } + options.cacheSerializer = cacheSerializer + return self + } +} + +// MARK: - Image Modifier +extension KFOptionSetter { + + /// Sets an `ImageModifier` to the image task. Use this to modify the fetched image object properties if needed. + /// + /// If the image was fetched directly from the downloader, the modifier will run directly after the + /// `ImageProcessor`. If the image is being fetched from a cache, the modifier will run after the `CacheSerializer`. + /// - Parameter modifier: The `ImageModifier` which will be used to modify the image object. + /// - Returns: A `Self` value with changes applied. + public func imageModifier(_ modifier: ImageModifier?) -> Self { + options.imageModifier = modifier + return self + } + + /// Sets a block to modify the image object. Use this to modify the fetched image object properties if needed. + /// + /// If the image was fetched directly from the downloader, the modifier block will run directly after the + /// `ImageProcessor`. If the image is being fetched from a cache, the modifier will run after the `CacheSerializer`. + /// + /// - Parameter block: The block which is used to modify the image object. + /// - Returns: A `Self` value with changes applied. + public func imageModifier(_ block: @escaping (inout KFCrossPlatformImage) throws -> Void) -> Self { + let modifier = AnyImageModifier { image -> KFCrossPlatformImage in + var image = image + try block(&image) + return image + } + options.imageModifier = modifier + return self + } +} + + +// MARK: - Cache Expiration +extension KFOptionSetter { + + /// Sets the expiration setting for memory cache of this image task. + /// + /// By default, the underlying `MemoryStorage.Backend` uses the + /// expiration in its config for all items. If set, the `MemoryStorage.Backend` will use this value to overwrite + /// the config setting for this caching item. + /// + /// - Parameter expiration: The expiration setting used in cache storage. + /// - Returns: A `Self` value with changes applied. + public func memoryCacheExpiration(_ expiration: StorageExpiration?) -> Self { + options.memoryCacheExpiration = expiration + return self + } + + /// Sets the expiration extending setting for memory cache. The item expiration time will be incremented by this + /// value after access. + /// + /// By default, the underlying `MemoryStorage.Backend` uses the initial cache expiration as extending + /// value: .cacheTime. + /// + /// To disable extending option at all, sets `.none` to it. + /// + /// - Parameter extending: The expiration extending setting used in cache storage. + /// - Returns: A `Self` value with changes applied. + public func memoryCacheAccessExtending(_ extending: ExpirationExtending) -> Self { + options.memoryCacheAccessExtendingExpiration = extending + return self + } + + /// Sets the expiration setting for disk cache of this image task. + /// + /// By default, the underlying `DiskStorage.Backend` uses the expiration in its config for all items. If set, + /// the `DiskStorage.Backend` will use this value to overwrite the config setting for this caching item. + /// + /// - Parameter expiration: The expiration setting used in cache storage. + /// - Returns: A `Self` value with changes applied. + public func diskCacheExpiration(_ expiration: StorageExpiration?) -> Self { + options.diskCacheExpiration = expiration + return self + } + + /// Sets the expiration extending setting for disk cache. The item expiration time will be incremented by this + /// value after access. + /// + /// By default, the underlying `DiskStorage.Backend` uses the initial cache expiration as extending + /// value: .cacheTime. + /// + /// To disable extending option at all, sets `.none` to it. + /// + /// - Parameter extending: The expiration extending setting used in cache storage. + /// - Returns: A `Self` value with changes applied. + public func diskCacheAccessExtending(_ extending: ExpirationExtending) -> Self { + options.diskCacheAccessExtendingExpiration = extending + return self + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/Kingfisher.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/Kingfisher.swift new file mode 100644 index 0000000..f875e2a --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/Kingfisher.swift @@ -0,0 +1,106 @@ +// +// Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 16/9/14. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import ImageIO + +#if os(macOS) +import AppKit +public typealias KFCrossPlatformImage = NSImage +public typealias KFCrossPlatformView = NSView +public typealias KFCrossPlatformColor = NSColor +public typealias KFCrossPlatformImageView = NSImageView +public typealias KFCrossPlatformButton = NSButton +#else +import UIKit +public typealias KFCrossPlatformImage = UIImage +public typealias KFCrossPlatformColor = UIColor +#if !os(watchOS) +public typealias KFCrossPlatformImageView = UIImageView +public typealias KFCrossPlatformView = UIView +public typealias KFCrossPlatformButton = UIButton +#if canImport(TVUIKit) +import TVUIKit +#endif +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +import CarPlay +#endif +#else +import WatchKit +#endif +#endif + +/// Wrapper for Kingfisher compatible types. This type provides an extension point for +/// convenience methods in Kingfisher. +public struct KingfisherWrapper { + public let base: Base + public init(_ base: Base) { + self.base = base + } +} + +/// Represents an object type that is compatible with Kingfisher. You can use `kf` property to get a +/// value in the namespace of Kingfisher. +public protocol KingfisherCompatible: AnyObject { } + +/// Represents a value type that is compatible with Kingfisher. You can use `kf` property to get a +/// value in the namespace of Kingfisher. +public protocol KingfisherCompatibleValue {} + +extension KingfisherCompatible { + /// Gets a namespace holder for Kingfisher compatible types. + public var kf: KingfisherWrapper { + get { return KingfisherWrapper(self) } + set { } + } +} + +extension KingfisherCompatibleValue { + /// Gets a namespace holder for Kingfisher compatible types. + public var kf: KingfisherWrapper { + get { return KingfisherWrapper(self) } + set { } + } +} + +extension KFCrossPlatformImage: KingfisherCompatible { } +#if !os(watchOS) +extension KFCrossPlatformImageView: KingfisherCompatible { } +extension KFCrossPlatformButton: KingfisherCompatible { } +extension NSTextAttachment: KingfisherCompatible { } +#else +extension WKInterfaceImage: KingfisherCompatible { } +#endif + +#if os(tvOS) && canImport(TVUIKit) +@available(tvOS 12.0, *) +extension TVMonogramView: KingfisherCompatible { } +#endif + +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +@available(iOS 14.0, *) +extension CPListItem: KingfisherCompatible { } +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KingfisherError.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KingfisherError.swift new file mode 100644 index 0000000..83d20a1 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KingfisherError.swift @@ -0,0 +1,461 @@ +// +// KingfisherError.swift +// Kingfisher +// +// Created by onevcat on 2018/09/26. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +extension Never {} + +/// Represents all the errors which can happen in Kingfisher framework. +/// Kingfisher related methods always throw a `KingfisherError` or invoke the callback with `KingfisherError` +/// as its error type. To handle errors from Kingfisher, you switch over the error to get a reason catalog, +/// then switch over the reason to know error detail. +public enum KingfisherError: Error { + + // MARK: Error Reason Types + + /// Represents the error reason during networking request phase. + /// + /// - emptyRequest: The request is empty. Code 1001. + /// - invalidURL: The URL of request is invalid. Code 1002. + /// - taskCancelled: The downloading task is cancelled by user. Code 1003. + public enum RequestErrorReason { + + /// The request is empty. Code 1001. + case emptyRequest + + /// The URL of request is invalid. Code 1002. + /// - request: The request is tend to be sent but its URL is invalid. + case invalidURL(request: URLRequest) + + /// The downloading task is cancelled by user. Code 1003. + /// - task: The session data task which is cancelled. + /// - token: The cancel token which is used for cancelling the task. + case taskCancelled(task: SessionDataTask, token: SessionDataTask.CancelToken) + } + + /// Represents the error reason during networking response phase. + /// + /// - invalidURLResponse: The response is not a valid URL response. Code 2001. + /// - invalidHTTPStatusCode: The response contains an invalid HTTP status code. Code 2002. + /// - URLSessionError: An error happens in the system URL session. Code 2003. + /// - dataModifyingFailed: Data modifying fails on returning a valid data. Code 2004. + /// - noURLResponse: The task is done but no URL response found. Code 2005. + public enum ResponseErrorReason { + + /// The response is not a valid URL response. Code 2001. + /// - response: The received invalid URL response. + /// The response is expected to be an HTTP response, but it is not. + case invalidURLResponse(response: URLResponse) + + /// The response contains an invalid HTTP status code. Code 2002. + /// - Note: + /// By default, status code 200..<400 is recognized as valid. You can override + /// this behavior by conforming to the `ImageDownloaderDelegate`. + /// - response: The received response. + case invalidHTTPStatusCode(response: HTTPURLResponse) + + /// An error happens in the system URL session. Code 2003. + /// - error: The underlying URLSession error object. + case URLSessionError(error: Error) + + /// Data modifying fails on returning a valid data. Code 2004. + /// - task: The failed task. + case dataModifyingFailed(task: SessionDataTask) + + /// The task is done but no URL response found. Code 2005. + /// - task: The failed task. + case noURLResponse(task: SessionDataTask) + } + + /// Represents the error reason during Kingfisher caching system. + /// + /// - fileEnumeratorCreationFailed: Cannot create a file enumerator for a certain disk URL. Code 3001. + /// - invalidFileEnumeratorContent: Cannot get correct file contents from a file enumerator. Code 3002. + /// - invalidURLResource: The file at target URL exists, but its URL resource is unavailable. Code 3003. + /// - cannotLoadDataFromDisk: The file at target URL exists, but the data cannot be loaded from it. Code 3004. + /// - cannotCreateDirectory: Cannot create a folder at a given path. Code 3005. + /// - imageNotExisting: The requested image does not exist in cache. Code 3006. + /// - cannotConvertToData: Cannot convert an object to data for storing. Code 3007. + /// - cannotSerializeImage: Cannot serialize an image to data for storing. Code 3008. + /// - cannotCreateCacheFile: Cannot create the cache file at a certain fileURL under a key. Code 3009. + /// - cannotSetCacheFileAttribute: Cannot set file attributes to a cached file. Code 3010. + public enum CacheErrorReason { + + /// Cannot create a file enumerator for a certain disk URL. Code 3001. + /// - url: The target disk URL from which the file enumerator should be created. + case fileEnumeratorCreationFailed(url: URL) + + /// Cannot get correct file contents from a file enumerator. Code 3002. + /// - url: The target disk URL from which the content of a file enumerator should be got. + case invalidFileEnumeratorContent(url: URL) + + /// The file at target URL exists, but its URL resource is unavailable. Code 3003. + /// - error: The underlying error thrown by file manager. + /// - key: The key used to getting the resource from cache. + /// - url: The disk URL where the target cached file exists. + case invalidURLResource(error: Error, key: String, url: URL) + + /// The file at target URL exists, but the data cannot be loaded from it. Code 3004. + /// - url: The disk URL where the target cached file exists. + /// - error: The underlying error which describes why this error happens. + case cannotLoadDataFromDisk(url: URL, error: Error) + + /// Cannot create a folder at a given path. Code 3005. + /// - path: The disk path where the directory creating operation fails. + /// - error: The underlying error which describes why this error happens. + case cannotCreateDirectory(path: String, error: Error) + + /// The requested image does not exist in cache. Code 3006. + /// - key: Key of the requested image in cache. + case imageNotExisting(key: String) + + /// Cannot convert an object to data for storing. Code 3007. + /// - object: The object which needs be convert to data. + case cannotConvertToData(object: Any, error: Error) + + /// Cannot serialize an image to data for storing. Code 3008. + /// - image: The input image needs to be serialized to cache. + /// - original: The original image data, if exists. + /// - serializer: The `CacheSerializer` used for the image serializing. + case cannotSerializeImage(image: KFCrossPlatformImage?, original: Data?, serializer: CacheSerializer) + + /// Cannot create the cache file at a certain fileURL under a key. Code 3009. + /// - fileURL: The url where the cache file should be created. + /// - key: The cache key used for the cache. When caching a file through `KingfisherManager` and Kingfisher's + /// extension method, it is the resolved cache key based on your input `Source` and the image processors. + /// - data: The data to be cached. + /// - error: The underlying error originally thrown by Foundation when writing the `data` to the disk file at + /// `fileURL`. + case cannotCreateCacheFile(fileURL: URL, key: String, data: Data, error: Error) + + /// Cannot set file attributes to a cached file. Code 3010. + /// - filePath: The path of target cache file. + /// - attributes: The file attribute to be set to the target file. + /// - error: The underlying error originally thrown by Foundation when setting the `attributes` to the disk + /// file at `filePath`. + case cannotSetCacheFileAttribute(filePath: String, attributes: [FileAttributeKey : Any], error: Error) + + /// The disk storage of cache is not ready. Code 3011. + /// + /// This is usually due to extremely lack of space on disk storage, and + /// Kingfisher failed even when creating the cache folder. The disk storage will be in unusable state. Normally, + /// ask user to free some spaces and restart the app to make the disk storage work again. + /// - cacheURL: The intended URL which should be the storage folder. + case diskStorageIsNotReady(cacheURL: URL) + } + + + /// Represents the error reason during image processing phase. + /// + /// - processingFailed: Image processing fails. There is no valid output image from the processor. Code 4001. + public enum ProcessorErrorReason { + /// Image processing fails. There is no valid output image from the processor. Code 4001. + /// - processor: The `ImageProcessor` used to process the image or its data in `item`. + /// - item: The image or its data content. + case processingFailed(processor: ImageProcessor, item: ImageProcessItem) + } + + /// Represents the error reason during image setting in a view related class. + /// + /// - emptySource: The input resource is empty or `nil`. Code 5001. + /// - notCurrentSourceTask: The source task is finished, but it is not the one expected now. Code 5002. + /// - dataProviderError: An error happens during getting data from an `ImageDataProvider`. Code 5003. + public enum ImageSettingErrorReason { + + /// The input resource is empty or `nil`. Code 5001. + case emptySource + + /// The resource task is finished, but it is not the one expected now. This usually happens when you set another + /// resource on the view without cancelling the current on-going one. The previous setting task will fail with + /// this `.notCurrentSourceTask` error when a result got, regardless of it being successful or not for that task. + /// The result of this original task is contained in the associated value. + /// Code 5002. + /// - result: The `RetrieveImageResult` if the source task is finished without problem. `nil` if an error + /// happens. + /// - error: The `Error` if an issue happens during image setting task. `nil` if the task finishes without + /// problem. + /// - source: The original source value of the task. + case notCurrentSourceTask(result: RetrieveImageResult?, error: Error?, source: Source) + + /// An error happens during getting data from an `ImageDataProvider`. Code 5003. + case dataProviderError(provider: ImageDataProvider, error: Error) + + /// No more alternative `Source` can be used in current loading process. It means that the + /// `.alternativeSources` are used and Kingfisher tried to recovery from the original error, but still + /// fails for all the given alternative sources. The associated value holds all the errors encountered during + /// the load process, including the original source loading error and all the alternative sources errors. + /// Code 5004. + case alternativeSourcesExhausted([PropagationError]) + } + + // MARK: Member Cases + + /// Represents the error reason during networking request phase. + case requestError(reason: RequestErrorReason) + /// Represents the error reason during networking response phase. + case responseError(reason: ResponseErrorReason) + /// Represents the error reason during Kingfisher caching system. + case cacheError(reason: CacheErrorReason) + /// Represents the error reason during image processing phase. + case processorError(reason: ProcessorErrorReason) + /// Represents the error reason during image setting in a view related class. + case imageSettingError(reason: ImageSettingErrorReason) + + // MARK: Helper Properties & Methods + + /// Helper property to check whether this error is a `RequestErrorReason.taskCancelled` or not. + public var isTaskCancelled: Bool { + if case .requestError(reason: .taskCancelled) = self { + return true + } + return false + } + + /// Helper method to check whether this error is a `ResponseErrorReason.invalidHTTPStatusCode` and the + /// associated value is a given status code. + /// + /// - Parameter code: The given status code. + /// - Returns: If `self` is a `ResponseErrorReason.invalidHTTPStatusCode` error + /// and its status code equals to `code`, `true` is returned. Otherwise, `false`. + public func isInvalidResponseStatusCode(_ code: Int) -> Bool { + if case .responseError(reason: .invalidHTTPStatusCode(let response)) = self { + return response.statusCode == code + } + return false + } + + public var isInvalidResponseStatusCode: Bool { + if case .responseError(reason: .invalidHTTPStatusCode) = self { + return true + } + return false + } + + /// Helper property to check whether this error is a `ImageSettingErrorReason.notCurrentSourceTask` or not. + /// When a new image setting task starts while the old one is still running, the new task identifier will be + /// set and the old one is overwritten. A `.notCurrentSourceTask` error will be raised when the old task finishes + /// to let you know the setting process finishes with a certain result, but the image view or button is not set. + public var isNotCurrentTask: Bool { + if case .imageSettingError(reason: .notCurrentSourceTask(_, _, _)) = self { + return true + } + return false + } + + var isLowDataModeConstrained: Bool { + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *), + case .responseError(reason: .URLSessionError(let sessionError)) = self, + let urlError = sessionError as? URLError, + urlError.networkUnavailableReason == .constrained + { + return true + } + return false + } + +} + +// MARK: - LocalizedError Conforming +extension KingfisherError: LocalizedError { + + /// A localized message describing what error occurred. + public var errorDescription: String? { + switch self { + case .requestError(let reason): return reason.errorDescription + case .responseError(let reason): return reason.errorDescription + case .cacheError(let reason): return reason.errorDescription + case .processorError(let reason): return reason.errorDescription + case .imageSettingError(let reason): return reason.errorDescription + } + } +} + + +// MARK: - CustomNSError Conforming +extension KingfisherError: CustomNSError { + + /// The error domain of `KingfisherError`. All errors from Kingfisher is under this domain. + public static let domain = "com.onevcat.Kingfisher.Error" + + /// The error code within the given domain. + public var errorCode: Int { + switch self { + case .requestError(let reason): return reason.errorCode + case .responseError(let reason): return reason.errorCode + case .cacheError(let reason): return reason.errorCode + case .processorError(let reason): return reason.errorCode + case .imageSettingError(let reason): return reason.errorCode + } + } +} + +extension KingfisherError.RequestErrorReason { + var errorDescription: String? { + switch self { + case .emptyRequest: + return "The request is empty or `nil`." + case .invalidURL(let request): + return "The request contains an invalid or empty URL. Request: \(request)." + case .taskCancelled(let task, let token): + return "The session task was cancelled. Task: \(task), cancel token: \(token)." + } + } + + var errorCode: Int { + switch self { + case .emptyRequest: return 1001 + case .invalidURL: return 1002 + case .taskCancelled: return 1003 + } + } +} + +extension KingfisherError.ResponseErrorReason { + var errorDescription: String? { + switch self { + case .invalidURLResponse(let response): + return "The URL response is invalid: \(response)" + case .invalidHTTPStatusCode(let response): + return "The HTTP status code in response is invalid. Code: \(response.statusCode), response: \(response)." + case .URLSessionError(let error): + return "A URL session error happened. The underlying error: \(error)" + case .dataModifyingFailed(let task): + return "The data modifying delegate returned `nil` for the downloaded data. Task: \(task)." + case .noURLResponse(let task): + return "No URL response received. Task: \(task)," + } + } + + var errorCode: Int { + switch self { + case .invalidURLResponse: return 2001 + case .invalidHTTPStatusCode: return 2002 + case .URLSessionError: return 2003 + case .dataModifyingFailed: return 2004 + case .noURLResponse: return 2005 + } + } +} + +extension KingfisherError.CacheErrorReason { + var errorDescription: String? { + switch self { + case .fileEnumeratorCreationFailed(let url): + return "Cannot create file enumerator for URL: \(url)." + case .invalidFileEnumeratorContent(let url): + return "Cannot get contents from the file enumerator at URL: \(url)." + case .invalidURLResource(let error, let key, let url): + return "Cannot get URL resource values or data for the given URL: \(url). " + + "Cache key: \(key). Underlying error: \(error)" + case .cannotLoadDataFromDisk(let url, let error): + return "Cannot load data from disk at URL: \(url). Underlying error: \(error)" + case .cannotCreateDirectory(let path, let error): + return "Cannot create directory at given path: Path: \(path). Underlying error: \(error)" + case .imageNotExisting(let key): + return "The image is not in cache, but you requires it should only be " + + "from cache by enabling the `.onlyFromCache` option. Key: \(key)." + case .cannotConvertToData(let object, let error): + return "Cannot convert the input object to a `Data` object when storing it to disk cache. " + + "Object: \(object). Underlying error: \(error)" + case .cannotSerializeImage(let image, let originalData, let serializer): + return "Cannot serialize an image due to the cache serializer returning `nil`. " + + "Image: \(String(describing:image)), original data: \(String(describing: originalData)), " + + "serializer: \(serializer)." + case .cannotCreateCacheFile(let fileURL, let key, let data, let error): + return "Cannot create cache file at url: \(fileURL), key: \(key), data length: \(data.count). " + + "Underlying foundation error: \(error)." + case .cannotSetCacheFileAttribute(let filePath, let attributes, let error): + return "Cannot set file attribute for the cache file at path: \(filePath), attributes: \(attributes)." + + "Underlying foundation error: \(error)." + case .diskStorageIsNotReady(let cacheURL): + return "The disk storage is not ready to use yet at URL: '\(cacheURL)'. " + + "This is usually caused by extremely lack of disk space. Ask users to free up some space and restart the app." + } + } + + var errorCode: Int { + switch self { + case .fileEnumeratorCreationFailed: return 3001 + case .invalidFileEnumeratorContent: return 3002 + case .invalidURLResource: return 3003 + case .cannotLoadDataFromDisk: return 3004 + case .cannotCreateDirectory: return 3005 + case .imageNotExisting: return 3006 + case .cannotConvertToData: return 3007 + case .cannotSerializeImage: return 3008 + case .cannotCreateCacheFile: return 3009 + case .cannotSetCacheFileAttribute: return 3010 + case .diskStorageIsNotReady: return 3011 + } + } +} + +extension KingfisherError.ProcessorErrorReason { + var errorDescription: String? { + switch self { + case .processingFailed(let processor, let item): + return "Processing image failed. Processor: \(processor). Processing item: \(item)." + } + } + + var errorCode: Int { + switch self { + case .processingFailed: return 4001 + } + } +} + +extension KingfisherError.ImageSettingErrorReason { + var errorDescription: String? { + switch self { + case .emptySource: + return "The input resource is empty." + case .notCurrentSourceTask(let result, let error, let resource): + if let result = result { + return "Retrieving resource succeeded, but this source is " + + "not the one currently expected. Result: \(result). Resource: \(resource)." + } else if let error = error { + return "Retrieving resource failed, and this resource is " + + "not the one currently expected. Error: \(error). Resource: \(resource)." + } else { + return nil + } + case .dataProviderError(let provider, let error): + return "Image data provider fails to provide data. Provider: \(provider), error: \(error)" + case .alternativeSourcesExhausted(let errors): + return "Image setting from alternaive sources failed: \(errors)" + } + } + + var errorCode: Int { + switch self { + case .emptySource: return 5001 + case .notCurrentSourceTask: return 5002 + case .dataProviderError: return 5003 + case .alternativeSourcesExhausted: return 5004 + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KingfisherManager.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KingfisherManager.swift new file mode 100644 index 0000000..dd4c315 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KingfisherManager.swift @@ -0,0 +1,739 @@ +// +// KingfisherManager.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +import Foundation + +/// The downloading progress block type. +/// The parameter value is the `receivedSize` of current response. +/// The second parameter is the total expected data length from response's "Content-Length" header. +/// If the expected length is not available, this block will not be called. +public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> Void) + +/// Represents the result of a Kingfisher retrieving image task. +public struct RetrieveImageResult { + + /// Gets the image object of this result. + public let image: KFCrossPlatformImage + + /// Gets the cache source of the image. It indicates from which layer of cache this image is retrieved. + /// If the image is just downloaded from network, `.none` will be returned. + public let cacheType: CacheType + + /// The `Source` which this result is related to. This indicated where the `image` of `self` is referring. + public let source: Source + + /// The original `Source` from which the retrieve task begins. It can be different from the `source` property. + /// When an alternative source loading happened, the `source` will be the replacing loading target, while the + /// `originalSource` will be kept as the initial `source` which issued the image loading process. + public let originalSource: Source +} + +/// A struct that stores some related information of an `KingfisherError`. It provides some context information for +/// a pure error so you can identify the error easier. +public struct PropagationError { + + /// The `Source` to which current `error` is bound. + public let source: Source + + /// The actual error happens in framework. + public let error: KingfisherError +} + + +/// The downloading task updated block type. The parameter `newTask` is the updated new task of image setting process. +/// It is a `nil` if the image loading does not require an image downloading process. If an image downloading is issued, +/// this value will contain the actual `DownloadTask` for you to keep and cancel it later if you need. +public typealias DownloadTaskUpdatedBlock = ((_ newTask: DownloadTask?) -> Void) + +/// Main manager class of Kingfisher. It connects Kingfisher downloader and cache, +/// to provide a set of convenience methods to use Kingfisher for tasks. +/// You can use this class to retrieve an image via a specified URL from web or cache. +public class KingfisherManager { + + /// Represents a shared manager used across Kingfisher. + /// Use this instance for getting or storing images with Kingfisher. + public static let shared = KingfisherManager() + + // Mark: Public Properties + /// The `ImageCache` used by this manager. It is `ImageCache.default` by default. + /// If a cache is specified in `KingfisherManager.defaultOptions`, the value in `defaultOptions` will be + /// used instead. + public var cache: ImageCache + + /// The `ImageDownloader` used by this manager. It is `ImageDownloader.default` by default. + /// If a downloader is specified in `KingfisherManager.defaultOptions`, the value in `defaultOptions` will be + /// used instead. + public var downloader: ImageDownloader + + /// Default options used by the manager. This option will be used in + /// Kingfisher manager related methods, as well as all view extension methods. + /// You can also passing other options for each image task by sending an `options` parameter + /// to Kingfisher's APIs. The per image options will overwrite the default ones, + /// if the option exists in both. + public var defaultOptions = KingfisherOptionsInfo.empty + + // Use `defaultOptions` to overwrite the `downloader` and `cache`. + private var currentDefaultOptions: KingfisherOptionsInfo { + return [.downloader(downloader), .targetCache(cache)] + defaultOptions + } + + private let processingQueue: CallbackQueue + + private convenience init() { + self.init(downloader: .default, cache: .default) + } + + /// Creates an image setting manager with specified downloader and cache. + /// + /// - Parameters: + /// - downloader: The image downloader used to download images. + /// - cache: The image cache which stores memory and disk images. + public init(downloader: ImageDownloader, cache: ImageCache) { + self.downloader = downloader + self.cache = cache + + let processQueueName = "com.onevcat.Kingfisher.KingfisherManager.processQueue.\(UUID().uuidString)" + processingQueue = .dispatch(DispatchQueue(label: processQueueName)) + } + + // MARK: - Getting Images + + /// Gets an image from a given resource. + /// - Parameters: + /// - resource: The `Resource` object defines data information like key or URL. + /// - options: Options to use when creating the image. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. `progressBlock` is always called in + /// main queue. + /// - downloadTaskUpdated: Called when a new image downloading task is created for current image retrieving. This + /// usually happens when an alternative source is used to replace the original (failed) + /// task. You can update your reference of `DownloadTask` if you want to manually `cancel` + /// the new task. + /// - completionHandler: Called when the image retrieved and set finished. This completion handler will be invoked + /// from the `options.callbackQueue`. If not specified, the main queue will be used. + /// - Returns: A task represents the image downloading. If there is a download task starts for `.network` resource, + /// the started `DownloadTask` is returned. Otherwise, `nil` is returned. + /// + /// - Note: + /// This method will first check whether the requested `resource` is already in cache or not. If cached, + /// it returns `nil` and invoke the `completionHandler` after the cached image retrieved. Otherwise, it + /// will download the `resource`, store it in cache, then call `completionHandler`. + @discardableResult + public func retrieveImage( + with resource: Resource, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + return retrieveImage( + with: resource.convertToSource(), + options: options, + progressBlock: progressBlock, + downloadTaskUpdated: downloadTaskUpdated, + completionHandler: completionHandler + ) + } + + /// Gets an image from a given resource. + /// + /// - Parameters: + /// - source: The `Source` object defines data information from network or a data provider. + /// - options: Options to use when creating the image. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. `progressBlock` is always called in + /// main queue. + /// - downloadTaskUpdated: Called when a new image downloading task is created for current image retrieving. This + /// usually happens when an alternative source is used to replace the original (failed) + /// task. You can update your reference of `DownloadTask` if you want to manually `cancel` + /// the new task. + /// - completionHandler: Called when the image retrieved and set finished. This completion handler will be invoked + /// from the `options.callbackQueue`. If not specified, the main queue will be used. + /// - Returns: A task represents the image downloading. If there is a download task starts for `.network` resource, + /// the started `DownloadTask` is returned. Otherwise, `nil` is returned. + /// + /// - Note: + /// This method will first check whether the requested `source` is already in cache or not. If cached, + /// it returns `nil` and invoke the `completionHandler` after the cached image retrieved. Otherwise, it + /// will try to load the `source`, store it in cache, then call `completionHandler`. + /// + public func retrieveImage( + with source: Source, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + let options = currentDefaultOptions + (options ?? .empty) + let info = KingfisherParsedOptionsInfo(options) + return retrieveImage( + with: source, + options: info, + progressBlock: progressBlock, + downloadTaskUpdated: downloadTaskUpdated, + completionHandler: completionHandler) + } + + func retrieveImage( + with source: Source, + options: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + var info = options + if let block = progressBlock { + info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + return retrieveImage( + with: source, + options: info, + downloadTaskUpdated: downloadTaskUpdated, + completionHandler: completionHandler) + } + + func retrieveImage( + with source: Source, + options: KingfisherParsedOptionsInfo, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + let retrievingContext = RetrievingContext(options: options, originalSource: source) + var retryContext: RetryContext? + + func startNewRetrieveTask( + with source: Source, + downloadTaskUpdated: DownloadTaskUpdatedBlock? + ) { + let newTask = self.retrieveImage(with: source, context: retrievingContext) { result in + handler(currentSource: source, result: result) + } + downloadTaskUpdated?(newTask) + } + + func failCurrentSource(_ source: Source, with error: KingfisherError) { + // Skip alternative sources if the user cancelled it. + guard !error.isTaskCancelled else { + completionHandler?(.failure(error)) + return + } + // When low data mode constrained error, retry with the low data mode source instead of use alternative on fly. + guard !error.isLowDataModeConstrained else { + if let source = retrievingContext.options.lowDataModeSource { + retrievingContext.options.lowDataModeSource = nil + startNewRetrieveTask(with: source, downloadTaskUpdated: downloadTaskUpdated) + } else { + // This should not happen. + completionHandler?(.failure(error)) + } + return + } + if let nextSource = retrievingContext.popAlternativeSource() { + retrievingContext.appendError(error, to: source) + startNewRetrieveTask(with: nextSource, downloadTaskUpdated: downloadTaskUpdated) + } else { + // No other alternative source. Finish with error. + if retrievingContext.propagationErrors.isEmpty { + completionHandler?(.failure(error)) + } else { + retrievingContext.appendError(error, to: source) + let finalError = KingfisherError.imageSettingError( + reason: .alternativeSourcesExhausted(retrievingContext.propagationErrors) + ) + completionHandler?(.failure(finalError)) + } + } + } + + func handler(currentSource: Source, result: (Result)) -> Void { + switch result { + case .success: + completionHandler?(result) + case .failure(let error): + if let retryStrategy = options.retryStrategy { + let context = retryContext?.increaseRetryCount() ?? RetryContext(source: source, error: error) + retryContext = context + + retryStrategy.retry(context: context) { decision in + switch decision { + case .retry(let userInfo): + retryContext?.userInfo = userInfo + startNewRetrieveTask(with: source, downloadTaskUpdated: downloadTaskUpdated) + case .stop: + failCurrentSource(currentSource, with: error) + } + } + } else { + failCurrentSource(currentSource, with: error) + } + } + } + + return retrieveImage( + with: source, + context: retrievingContext) + { + result in + handler(currentSource: source, result: result) + } + + } + + private func retrieveImage( + with source: Source, + context: RetrievingContext, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + let options = context.options + if options.forceRefresh { + return loadAndCacheImage( + source: source, + context: context, + completionHandler: completionHandler)?.value + + } else { + let loadedFromCache = retrieveImageFromCache( + source: source, + context: context, + completionHandler: completionHandler) + + if loadedFromCache { + return nil + } + + if options.onlyFromCache { + let error = KingfisherError.cacheError(reason: .imageNotExisting(key: source.cacheKey)) + completionHandler?(.failure(error)) + return nil + } + + return loadAndCacheImage( + source: source, + context: context, + completionHandler: completionHandler)?.value + } + } + + func provideImage( + provider: ImageDataProvider, + options: KingfisherParsedOptionsInfo, + completionHandler: ((Result) -> Void)?) + { + guard let completionHandler = completionHandler else { return } + provider.data { result in + switch result { + case .success(let data): + (options.processingQueue ?? self.processingQueue).execute { + let processor = options.processor + let processingItem = ImageProcessItem.data(data) + guard let image = processor.process(item: processingItem, options: options) else { + options.callbackQueue.execute { + let error = KingfisherError.processorError( + reason: .processingFailed(processor: processor, item: processingItem)) + completionHandler(.failure(error)) + } + return + } + + options.callbackQueue.execute { + let result = ImageLoadingResult(image: image, url: nil, originalData: data) + completionHandler(.success(result)) + } + } + case .failure(let error): + options.callbackQueue.execute { + let error = KingfisherError.imageSettingError( + reason: .dataProviderError(provider: provider, error: error)) + completionHandler(.failure(error)) + } + + } + } + } + + private func cacheImage( + source: Source, + options: KingfisherParsedOptionsInfo, + context: RetrievingContext, + result: Result, + completionHandler: ((Result) -> Void)? + ) + { + switch result { + case .success(let value): + let needToCacheOriginalImage = options.cacheOriginalImage && + options.processor != DefaultImageProcessor.default + let coordinator = CacheCallbackCoordinator( + shouldWaitForCache: options.waitForCache, shouldCacheOriginal: needToCacheOriginalImage) + let result = RetrieveImageResult( + image: options.imageModifier?.modify(value.image) ?? value.image, + cacheType: .none, + source: source, + originalSource: context.originalSource + ) + // Add image to cache. + let targetCache = options.targetCache ?? self.cache + targetCache.store( + value.image, + original: value.originalData, + forKey: source.cacheKey, + options: options, + toDisk: !options.cacheMemoryOnly) + { + _ in + coordinator.apply(.cachingImage) { + completionHandler?(.success(result)) + } + } + + // Add original image to cache if necessary. + + if needToCacheOriginalImage { + let originalCache = options.originalCache ?? targetCache + originalCache.storeToDisk( + value.originalData, + forKey: source.cacheKey, + processorIdentifier: DefaultImageProcessor.default.identifier, + expiration: options.diskCacheExpiration) + { + _ in + coordinator.apply(.cachingOriginalImage) { + completionHandler?(.success(result)) + } + } + } + + coordinator.apply(.cacheInitiated) { + completionHandler?(.success(result)) + } + + case .failure(let error): + completionHandler?(.failure(error)) + } + } + + @discardableResult + func loadAndCacheImage( + source: Source, + context: RetrievingContext, + completionHandler: ((Result) -> Void)?) -> DownloadTask.WrappedTask? + { + let options = context.options + func _cacheImage(_ result: Result) { + cacheImage( + source: source, + options: options, + context: context, + result: result, + completionHandler: completionHandler + ) + } + + switch source { + case .network(let resource): + let downloader = options.downloader ?? self.downloader + let task = downloader.downloadImage( + with: resource.downloadURL, options: options, completionHandler: _cacheImage + ) + + + // The code below is neat, but it fails the Swift 5.2 compiler with a runtime crash when + // `BUILD_LIBRARY_FOR_DISTRIBUTION` is turned on. I believe it is a bug in the compiler. + // Let's fallback to a traditional style before it can be fixed in Swift. + // + // https://github.com/onevcat/Kingfisher/issues/1436 + // + // return task.map(DownloadTask.WrappedTask.download) + + if let task = task { + return .download(task) + } else { + return nil + } + + case .provider(let provider): + provideImage(provider: provider, options: options, completionHandler: _cacheImage) + return .dataProviding + } + } + + /// Retrieves image from memory or disk cache. + /// + /// - Parameters: + /// - source: The target source from which to get image. + /// - key: The key to use when caching the image. + /// - url: Image request URL. This is not used when retrieving image from cache. It is just used for + /// `RetrieveImageResult` callback compatibility. + /// - options: Options on how to get the image from image cache. + /// - completionHandler: Called when the image retrieving finishes, either with succeeded + /// `RetrieveImageResult` or an error. + /// - Returns: `true` if the requested image or the original image before being processed is existing in cache. + /// Otherwise, this method returns `false`. + /// + /// - Note: + /// The image retrieving could happen in either memory cache or disk cache. The `.processor` option in + /// `options` will be considered when searching in the cache. If no processed image is found, Kingfisher + /// will try to check whether an original version of that image is existing or not. If there is already an + /// original, Kingfisher retrieves it from cache and processes it. Then, the processed image will be store + /// back to cache for later use. + func retrieveImageFromCache( + source: Source, + context: RetrievingContext, + completionHandler: ((Result) -> Void)?) -> Bool + { + let options = context.options + // 1. Check whether the image was already in target cache. If so, just get it. + let targetCache = options.targetCache ?? cache + let key = source.cacheKey + let targetImageCached = targetCache.imageCachedType( + forKey: key, processorIdentifier: options.processor.identifier) + + let validCache = targetImageCached.cached && + (options.fromMemoryCacheOrRefresh == false || targetImageCached == .memory) + if validCache { + targetCache.retrieveImage(forKey: key, options: options) { result in + guard let completionHandler = completionHandler else { return } + options.callbackQueue.execute { + result.match( + onSuccess: { cacheResult in + let value: Result + if var image = cacheResult.image { + if image.kf.imageFrameCount != nil && image.kf.imageFrameCount != 1, let data = image.kf.animatedImageData { + // Always recreate animated image representation since it is possible to be loaded in different options. + // https://github.com/onevcat/Kingfisher/issues/1923 + image = KingfisherWrapper.animatedImage(data: data, options: options.imageCreatingOptions) ?? .init() + } + if let modifier = options.imageModifier { + image = modifier.modify(image) + } + value = result.map { + RetrieveImageResult( + image: image, + cacheType: $0.cacheType, + source: source, + originalSource: context.originalSource + ) + } + } else { + value = .failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))) + } + completionHandler(value) + }, + onFailure: { _ in + completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key)))) + } + ) + } + } + return true + } + + // 2. Check whether the original image exists. If so, get it, process it, save to storage and return. + let originalCache = options.originalCache ?? targetCache + // No need to store the same file in the same cache again. + if originalCache === targetCache && options.processor == DefaultImageProcessor.default { + return false + } + + // Check whether the unprocessed image existing or not. + let originalImageCacheType = originalCache.imageCachedType( + forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier) + let canAcceptDiskCache = !options.fromMemoryCacheOrRefresh + + let canUseOriginalImageCache = + (canAcceptDiskCache && originalImageCacheType.cached) || + (!canAcceptDiskCache && originalImageCacheType == .memory) + + if canUseOriginalImageCache { + // Now we are ready to get found the original image from cache. We need the unprocessed image, so remove + // any processor from options first. + var optionsWithoutProcessor = options + optionsWithoutProcessor.processor = DefaultImageProcessor.default + originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { result in + + result.match( + onSuccess: { cacheResult in + guard let image = cacheResult.image else { + assertionFailure("The image (under key: \(key) should be existing in the original cache.") + return + } + + let processor = options.processor + (options.processingQueue ?? self.processingQueue).execute { + let item = ImageProcessItem.image(image) + guard let processedImage = processor.process(item: item, options: options) else { + let error = KingfisherError.processorError( + reason: .processingFailed(processor: processor, item: item)) + options.callbackQueue.execute { completionHandler?(.failure(error)) } + return + } + + var cacheOptions = options + cacheOptions.callbackQueue = .untouch + + let coordinator = CacheCallbackCoordinator( + shouldWaitForCache: options.waitForCache, shouldCacheOriginal: false) + + let result = RetrieveImageResult( + image: options.imageModifier?.modify(processedImage) ?? processedImage, + cacheType: .none, + source: source, + originalSource: context.originalSource + ) + + targetCache.store( + processedImage, + forKey: key, + options: cacheOptions, + toDisk: !options.cacheMemoryOnly) + { + _ in + coordinator.apply(.cachingImage) { + options.callbackQueue.execute { completionHandler?(.success(result)) } + } + } + + coordinator.apply(.cacheInitiated) { + options.callbackQueue.execute { completionHandler?(.success(result)) } + } + } + }, + onFailure: { _ in + // This should not happen actually, since we already confirmed `originalImageCached` is `true`. + // Just in case... + options.callbackQueue.execute { + completionHandler?( + .failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))) + ) + } + } + ) + } + return true + } + + return false + } +} + +class RetrievingContext { + + var options: KingfisherParsedOptionsInfo + + let originalSource: Source + var propagationErrors: [PropagationError] = [] + + init(options: KingfisherParsedOptionsInfo, originalSource: Source) { + self.originalSource = originalSource + self.options = options + } + + func popAlternativeSource() -> Source? { + guard var alternativeSources = options.alternativeSources, !alternativeSources.isEmpty else { + return nil + } + let nextSource = alternativeSources.removeFirst() + options.alternativeSources = alternativeSources + return nextSource + } + + @discardableResult + func appendError(_ error: KingfisherError, to source: Source) -> [PropagationError] { + let item = PropagationError(source: source, error: error) + propagationErrors.append(item) + return propagationErrors + } +} + +class CacheCallbackCoordinator { + + enum State { + case idle + case imageCached + case originalImageCached + case done + } + + enum Action { + case cacheInitiated + case cachingImage + case cachingOriginalImage + } + + private let shouldWaitForCache: Bool + private let shouldCacheOriginal: Bool + private let stateQueue: DispatchQueue + private var threadSafeState: State = .idle + + private (set) var state: State { + set { stateQueue.sync { threadSafeState = newValue } } + get { stateQueue.sync { threadSafeState } } + } + + init(shouldWaitForCache: Bool, shouldCacheOriginal: Bool) { + self.shouldWaitForCache = shouldWaitForCache + self.shouldCacheOriginal = shouldCacheOriginal + let stateQueueName = "com.onevcat.Kingfisher.CacheCallbackCoordinator.stateQueue.\(UUID().uuidString)" + self.stateQueue = DispatchQueue(label: stateQueueName) + } + + func apply(_ action: Action, trigger: () -> Void) { + switch (state, action) { + case (.done, _): + break + + // From .idle + case (.idle, .cacheInitiated): + if !shouldWaitForCache { + state = .done + trigger() + } + case (.idle, .cachingImage): + if shouldCacheOriginal { + state = .imageCached + } else { + state = .done + trigger() + } + case (.idle, .cachingOriginalImage): + state = .originalImageCached + + // From .imageCached + case (.imageCached, .cachingOriginalImage): + state = .done + trigger() + + // From .originalImageCached + case (.originalImageCached, .cachingImage): + state = .done + trigger() + + default: + assertionFailure("This case should not happen in CacheCallbackCoordinator: \(state) - \(action)") + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift new file mode 100644 index 0000000..5f2aea6 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift @@ -0,0 +1,400 @@ +// +// KingfisherOptionsInfo.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/23. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + + +/// KingfisherOptionsInfo is a typealias for [KingfisherOptionsInfoItem]. +/// You can use the enum of option item with value to control some behaviors of Kingfisher. +public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem] + +extension Array where Element == KingfisherOptionsInfoItem { + static let empty: KingfisherOptionsInfo = [] +} + +/// Represents the available option items could be used in `KingfisherOptionsInfo`. +public enum KingfisherOptionsInfoItem { + + /// Kingfisher will use the associated `ImageCache` object when handling related operations, + /// including trying to retrieve the cached images and store the downloaded image to it. + case targetCache(ImageCache) + + /// The `ImageCache` for storing and retrieving original images. If `originalCache` is + /// contained in the options, it will be preferred for storing and retrieving original images. + /// If there is no `.originalCache` in the options, `.targetCache` will be used to store original images. + /// + /// When using KingfisherManager to download and store an image, if `cacheOriginalImage` is + /// applied in the option, the original image will be stored to this `originalCache`. At the + /// same time, if a requested final image (with processor applied) cannot be found in `targetCache`, + /// Kingfisher will try to search the original image to check whether it is already there. If found, + /// it will be used and applied with the given processor. It is an optimization for not downloading + /// the same image for multiple times. + case originalCache(ImageCache) + + /// Kingfisher will use the associated `ImageDownloader` object to download the requested images. + case downloader(ImageDownloader) + + /// Member for animation transition when using `UIImageView`. Kingfisher will use the `ImageTransition` of + /// this enum to animate the image in if it is downloaded from web. The transition will not happen when the + /// image is retrieved from either memory or disk cache by default. If you need to do the transition even when + /// the image being retrieved from cache, set `.forceRefresh` as well. + case transition(ImageTransition) + + /// Associated `Float` value will be set as the priority of image download task. The value for it should be + /// between 0.0~1.0. If this option not set, the default value (`URLSessionTask.defaultPriority`) will be used. + case downloadPriority(Float) + + /// If set, Kingfisher will ignore the cache and try to start a download task for the image source. + case forceRefresh + + /// If set, Kingfisher will try to retrieve the image from memory cache first. If the image is not in memory + /// cache, then it will ignore the disk cache but download the image again from network. This is useful when + /// you want to display a changeable image behind the same url at the same app session, while avoiding download + /// it for multiple times. + case fromMemoryCacheOrRefresh + + /// If set, setting the image to an image view will happen with transition even when retrieved from cache. + /// See `.transition` option for more. + case forceTransition + + /// If set, Kingfisher will only cache the value in memory but not in disk. + case cacheMemoryOnly + + /// If set, Kingfisher will wait for caching operation to be completed before calling the completion block. + case waitForCache + + /// If set, Kingfisher will only try to retrieve the image from cache, but not from network. If the image is not in + /// cache, the image retrieving will fail with the `KingfisherError.cacheError` with `.imageNotExisting` as its + /// reason. + case onlyFromCache + + /// Decode the image in background thread before using. It will decode the downloaded image data and do a off-screen + /// rendering to extract pixel information in background. This can speed up display, but will cost more time to + /// prepare the image for using. + case backgroundDecode + + /// The associated value will be used as the target queue of dispatch callbacks when retrieving images from + /// cache. If not set, Kingfisher will use `.mainCurrentOrAsync` for callbacks. + /// + /// - Note: + /// This option does not affect the callbacks for UI related extension methods. You will always get the + /// callbacks called from main queue. + case callbackQueue(CallbackQueue) + + /// The associated value will be used as the scale factor when converting retrieved data to an image. + /// Specify the image scale, instead of your screen scale. You may need to set the correct scale when you dealing + /// with 2x or 3x retina images. Otherwise, Kingfisher will convert the data to image object at `scale` 1.0. + case scaleFactor(CGFloat) + + /// Whether all the animated image data should be preloaded. Default is `false`, which means only following frames + /// will be loaded on need. If `true`, all the animated image data will be loaded and decoded into memory. + /// + /// This option is mainly used for back compatibility internally. You should not set it directly. Instead, + /// you should choose the image view class to control the GIF data loading. There are two classes in Kingfisher + /// support to display a GIF image. `AnimatedImageView` does not preload all data, it takes much less memory, but + /// uses more CPU when display. While a normal image view (`UIImageView` or `NSImageView`) loads all data at once, + /// which uses more memory but only decode image frames once. + case preloadAllAnimationData + + /// The `ImageDownloadRequestModifier` contained will be used to change the request before it being sent. + /// This is the last chance you can modify the image download request. You can modify the request for some + /// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping. + /// The original request will be sent without any modification by default. + case requestModifier(AsyncImageDownloadRequestModifier) + + /// The `ImageDownloadRedirectHandler` contained will be used to change the request before redirection. + /// This is the possibility you can modify the image download request during redirect. You can modify the request for + /// some customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url + /// mapping. + /// The original redirection request will be sent without any modification by default. + case redirectHandler(ImageDownloadRedirectHandler) + + /// Processor for processing when the downloading finishes, a processor will convert the downloaded data to an image + /// and/or apply some filter on it. If a cache is connected to the downloader (it happens when you are using + /// KingfisherManager or any of the view extension methods), the converted image will also be sent to cache as well. + /// If not set, the `DefaultImageProcessor.default` will be used. + case processor(ImageProcessor) + + /// Provides a `CacheSerializer` to convert some data to an image object for + /// retrieving from disk cache or vice versa for storing to disk cache. + /// If not set, the `DefaultCacheSerializer.default` will be used. + case cacheSerializer(CacheSerializer) + + /// An `ImageModifier` is for modifying an image as needed right before it is used. If the image was fetched + /// directly from the downloader, the modifier will run directly after the `ImageProcessor`. If the image is being + /// fetched from a cache, the modifier will run after the `CacheSerializer`. + /// + /// Use `ImageModifier` when you need to set properties that do not persist when caching the image on a concrete + /// type of `Image`, such as the `renderingMode` or the `alignmentInsets` of `UIImage`. + case imageModifier(ImageModifier) + + /// Keep the existing image of image view while setting another image to it. + /// By setting this option, the placeholder image parameter of image view extension method + /// will be ignored and the current image will be kept while loading or downloading the new image. + case keepCurrentImageWhileLoading + + /// If set, Kingfisher will only load the first frame from an animated image file as a single image. + /// Loading an animated images may take too much memory. It will be useful when you want to display a + /// static preview of the first frame from an animated image. + /// + /// This option will be ignored if the target image is not animated image data. + case onlyLoadFirstFrame + + /// If set and an `ImageProcessor` is used, Kingfisher will try to cache both the final result and original + /// image. Kingfisher will have a chance to use the original image when another processor is applied to the same + /// resource, instead of downloading it again. You can use `.originalCache` to specify a cache or the original + /// images if necessary. + /// + /// The original image will be only cached to disk storage. + case cacheOriginalImage + + /// If set and an image retrieving error occurred Kingfisher will set provided image (or empty) + /// in place of requested one. It's useful when you don't want to show placeholder + /// during loading time but wants to use some default image when requests will be failed. + case onFailureImage(KFCrossPlatformImage?) + + /// If set and used in `ImagePrefetcher`, the prefetching operation will load the images into memory storage + /// aggressively. By default this is not contained in the options, that means if the requested image is already + /// in disk cache, Kingfisher will not try to load it to memory. + case alsoPrefetchToMemory + + /// If set, the disk storage loading will happen in the same calling queue. By default, disk storage file loading + /// happens in its own queue with an asynchronous dispatch behavior. Although it provides better non-blocking disk + /// loading performance, it also causes a flickering when you reload an image from disk, if the image view already + /// has an image set. + /// + /// Set this options will stop that flickering by keeping all loading in the same queue (typically the UI queue + /// if you are using Kingfisher's extension methods to set an image), with a tradeoff of loading performance. + case loadDiskFileSynchronously + + /// Options to control the writing of data to disk storage + /// If set, options will be passed the store operation for a new files. + case diskStoreWriteOptions(Data.WritingOptions) + + /// The expiration setting for memory cache. By default, the underlying `MemoryStorage.Backend` uses the + /// expiration in its config for all items. If set, the `MemoryStorage.Backend` will use this associated + /// value to overwrite the config setting for this caching item. + case memoryCacheExpiration(StorageExpiration) + + /// The expiration extending setting for memory cache. The item expiration time will be incremented by this + /// value after access. + /// By default, the underlying `MemoryStorage.Backend` uses the initial cache expiration as extending + /// value: .cacheTime. + /// + /// To disable extending option at all add memoryCacheAccessExtendingExpiration(.none) to options. + case memoryCacheAccessExtendingExpiration(ExpirationExtending) + + /// The expiration setting for disk cache. By default, the underlying `DiskStorage.Backend` uses the + /// expiration in its config for all items. If set, the `DiskStorage.Backend` will use this associated + /// value to overwrite the config setting for this caching item. + case diskCacheExpiration(StorageExpiration) + + /// The expiration extending setting for disk cache. The item expiration time will be incremented by this value after access. + /// By default, the underlying `DiskStorage.Backend` uses the initial cache expiration as extending value: .cacheTime. + /// To disable extending option at all add diskCacheAccessExtendingExpiration(.none) to options. + case diskCacheAccessExtendingExpiration(ExpirationExtending) + + /// Decides on which queue the image processing should happen. By default, Kingfisher uses a pre-defined serial + /// queue to process images. Use this option to change this behavior. For example, specify a `.mainCurrentOrAsync` + /// to let the image be processed in main queue to prevent a possible flickering (but with a possibility of + /// blocking the UI, especially if the processor needs a lot of time to run). + case processingQueue(CallbackQueue) + + /// Enable progressive image loading, Kingfisher will use the associated `ImageProgressive` value to process the + /// progressive JPEG data and display it in a progressive way. + case progressiveJPEG(ImageProgressive) + + /// The alternative sources will be used when the original input `Source` fails. The `Source`s in the associated + /// array will be used to start a new image loading task if the previous task fails due to an error. The image + /// source loading process will stop as soon as a source is loaded successfully. If all `[Source]`s are used but + /// the loading is still failing, an `imageSettingError` with `alternativeSourcesExhausted` as its reason will be + /// thrown out. + /// + /// This option is useful if you want to implement a fallback solution for setting image. + /// + /// User cancellation will not trigger the alternative source loading. + case alternativeSources([Source]) + + /// Provide a retry strategy which will be used when something gets wrong during the image retrieving process from + /// `KingfisherManager`. You can define a strategy by create a type conforming to the `RetryStrategy` protocol. + /// + /// - Note: + /// + /// All extension methods of Kingfisher (`kf` extensions on `UIImageView` or `UIButton`) retrieve images through + /// `KingfisherManager`, so the retry strategy also applies when using them. However, this option does not apply + /// when pass to an `ImageDownloader` or `ImageCache`. + /// + case retryStrategy(RetryStrategy) + + /// The `Source` should be loaded when user enables Low Data Mode and the original source fails with an + /// `NSURLErrorNetworkUnavailableReason.constrained` error. When this option is set, the + /// `allowsConstrainedNetworkAccess` property of the request for the original source will be set to `false` and the + /// `Source` in associated value will be used to retrieve the image for low data mode. Usually, you can provide a + /// low-resolution version of your image or a local image provider to display a placeholder. + /// + /// If not set or the `source` is `nil`, the device Low Data Mode will be ignored and the original source will + /// be loaded following the system default behavior, in a normal way. + case lowDataMode(Source?) +} + +// Improve performance by parsing the input `KingfisherOptionsInfo` (self) first. +// So we can prevent the iterating over the options array again and again. +/// The parsed options info used across Kingfisher methods. Each property in this type corresponds a case member +/// in `KingfisherOptionsInfoItem`. When a `KingfisherOptionsInfo` sent to Kingfisher related methods, it will be +/// parsed and converted to a `KingfisherParsedOptionsInfo` first, and pass through the internal methods. +public struct KingfisherParsedOptionsInfo { + + public var targetCache: ImageCache? = nil + public var originalCache: ImageCache? = nil + public var downloader: ImageDownloader? = nil + public var transition: ImageTransition = .none + public var downloadPriority: Float = URLSessionTask.defaultPriority + public var forceRefresh = false + public var fromMemoryCacheOrRefresh = false + public var forceTransition = false + public var cacheMemoryOnly = false + public var waitForCache = false + public var onlyFromCache = false + public var backgroundDecode = false + public var preloadAllAnimationData = false + public var callbackQueue: CallbackQueue = .mainCurrentOrAsync + public var scaleFactor: CGFloat = 1.0 + public var requestModifier: AsyncImageDownloadRequestModifier? = nil + public var redirectHandler: ImageDownloadRedirectHandler? = nil + public var processor: ImageProcessor = DefaultImageProcessor.default + public var imageModifier: ImageModifier? = nil + public var cacheSerializer: CacheSerializer = DefaultCacheSerializer.default + public var keepCurrentImageWhileLoading = false + public var onlyLoadFirstFrame = false + public var cacheOriginalImage = false + public var onFailureImage: Optional = .none + public var alsoPrefetchToMemory = false + public var loadDiskFileSynchronously = false + public var diskStoreWriteOptions: Data.WritingOptions = [] + public var memoryCacheExpiration: StorageExpiration? = nil + public var memoryCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime + public var diskCacheExpiration: StorageExpiration? = nil + public var diskCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime + public var processingQueue: CallbackQueue? = nil + public var progressiveJPEG: ImageProgressive? = nil + public var alternativeSources: [Source]? = nil + public var retryStrategy: RetryStrategy? = nil + public var lowDataModeSource: Source? = nil + + var onDataReceived: [DataReceivingSideEffect]? = nil + + public init(_ info: KingfisherOptionsInfo?) { + guard let info = info else { return } + for option in info { + switch option { + case .targetCache(let value): targetCache = value + case .originalCache(let value): originalCache = value + case .downloader(let value): downloader = value + case .transition(let value): transition = value + case .downloadPriority(let value): downloadPriority = value + case .forceRefresh: forceRefresh = true + case .fromMemoryCacheOrRefresh: fromMemoryCacheOrRefresh = true + case .forceTransition: forceTransition = true + case .cacheMemoryOnly: cacheMemoryOnly = true + case .waitForCache: waitForCache = true + case .onlyFromCache: onlyFromCache = true + case .backgroundDecode: backgroundDecode = true + case .preloadAllAnimationData: preloadAllAnimationData = true + case .callbackQueue(let value): callbackQueue = value + case .scaleFactor(let value): scaleFactor = value + case .requestModifier(let value): requestModifier = value + case .redirectHandler(let value): redirectHandler = value + case .processor(let value): processor = value + case .imageModifier(let value): imageModifier = value + case .cacheSerializer(let value): cacheSerializer = value + case .keepCurrentImageWhileLoading: keepCurrentImageWhileLoading = true + case .onlyLoadFirstFrame: onlyLoadFirstFrame = true + case .cacheOriginalImage: cacheOriginalImage = true + case .onFailureImage(let value): onFailureImage = .some(value) + case .alsoPrefetchToMemory: alsoPrefetchToMemory = true + case .loadDiskFileSynchronously: loadDiskFileSynchronously = true + case .diskStoreWriteOptions(let options): diskStoreWriteOptions = options + case .memoryCacheExpiration(let expiration): memoryCacheExpiration = expiration + case .memoryCacheAccessExtendingExpiration(let expirationExtending): memoryCacheAccessExtendingExpiration = expirationExtending + case .diskCacheExpiration(let expiration): diskCacheExpiration = expiration + case .diskCacheAccessExtendingExpiration(let expirationExtending): diskCacheAccessExtendingExpiration = expirationExtending + case .processingQueue(let queue): processingQueue = queue + case .progressiveJPEG(let value): progressiveJPEG = value + case .alternativeSources(let sources): alternativeSources = sources + case .retryStrategy(let strategy): retryStrategy = strategy + case .lowDataMode(let source): lowDataModeSource = source + } + } + + if originalCache == nil { + originalCache = targetCache + } + } +} + +extension KingfisherParsedOptionsInfo { + var imageCreatingOptions: ImageCreatingOptions { + return ImageCreatingOptions( + scale: scaleFactor, + duration: 0.0, + preloadAll: preloadAllAnimationData, + onlyFirstFrame: onlyLoadFirstFrame) + } +} + +protocol DataReceivingSideEffect: AnyObject { + var onShouldApply: () -> Bool { get set } + func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) +} + +class ImageLoadingProgressSideEffect: DataReceivingSideEffect { + + var onShouldApply: () -> Bool = { return true } + + let block: DownloadProgressBlock + + init(_ block: @escaping DownloadProgressBlock) { + self.block = block + } + + func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) { + guard self.onShouldApply() else { return } + guard let expectedContentLength = task.task.response?.expectedContentLength, + expectedContentLength != -1 else + { + return + } + + let dataLength = Int64(task.mutableData.count) + DispatchQueue.main.async { + self.block(dataLength, expectedContentLength) + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/Filter.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/Filter.swift new file mode 100644 index 0000000..6e4b386 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/Filter.swift @@ -0,0 +1,146 @@ +// +// Filter.swift +// Kingfisher +// +// Created by Wei Wang on 2016/08/31. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if !os(watchOS) + +import CoreImage + +// Reuse the same CI Context for all CI drawing. +private let ciContext = CIContext(options: nil) + +/// Represents the type of transformer method, which will be used in to provide a `Filter`. +public typealias Transformer = (CIImage) -> CIImage? + +/// Represents a processor based on a `CIImage` `Filter`. +/// It requires a filter to create an `ImageProcessor`. +public protocol CIImageProcessor: ImageProcessor { + var filter: Filter { get } +} + +extension CIImageProcessor { + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.apply(filter) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// A wrapper struct for a `Transformer` of CIImage filters. A `Filter` +/// value could be used to create a `CIImage` processor. +public struct Filter { + + let transform: Transformer + + public init(transform: @escaping Transformer) { + self.transform = transform + } + + /// Tint filter which will apply a tint color to images. + public static var tint: (KFCrossPlatformColor) -> Filter = { + color in + Filter { + input in + + let colorFilter = CIFilter(name: "CIConstantColorGenerator")! + colorFilter.setValue(CIColor(color: color), forKey: kCIInputColorKey) + + let filter = CIFilter(name: "CISourceOverCompositing")! + + let colorImage = colorFilter.outputImage + filter.setValue(colorImage, forKey: kCIInputImageKey) + filter.setValue(input, forKey: kCIInputBackgroundImageKey) + + return filter.outputImage?.cropped(to: input.extent) + } + } + + /// Represents color control elements. It is a tuple of + /// `(brightness, contrast, saturation, inputEV)` + public typealias ColorElement = (CGFloat, CGFloat, CGFloat, CGFloat) + + /// Color control filter which will apply color control change to images. + public static var colorControl: (ColorElement) -> Filter = { arg -> Filter in + let (brightness, contrast, saturation, inputEV) = arg + return Filter { input in + let paramsColor = [kCIInputBrightnessKey: brightness, + kCIInputContrastKey: contrast, + kCIInputSaturationKey: saturation] + let blackAndWhite = input.applyingFilter("CIColorControls", parameters: paramsColor) + let paramsExposure = [kCIInputEVKey: inputEV] + return blackAndWhite.applyingFilter("CIExposureAdjust", parameters: paramsExposure) + } + } +} + +extension KingfisherWrapper where Base: KFCrossPlatformImage { + + /// Applies a `Filter` containing `CIImage` transformer to `self`. + /// + /// - Parameter filter: The filter used to transform `self`. + /// - Returns: A transformed image by input `Filter`. + /// + /// - Note: + /// Only CG-based images are supported. If any error happens + /// during transforming, `self` will be returned. + public func apply(_ filter: Filter) -> KFCrossPlatformImage { + + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Tint image only works for CG-based image.") + return base + } + + let inputImage = CIImage(cgImage: cgImage) + guard let outputImage = filter.transform(inputImage) else { + return base + } + + guard let result = ciContext.createCGImage(outputImage, from: outputImage.extent) else { + assertionFailure("[Kingfisher] Can not make an tint image within context.") + return base + } + + #if os(macOS) + return fixedForRetinaPixel(cgImage: result, to: size) + #else + return KFCrossPlatformImage(cgImage: result, scale: base.scale, orientation: base.imageOrientation) + #endif + } + +} + +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift new file mode 100644 index 0000000..8b2480f --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift @@ -0,0 +1,121 @@ +// +// AnimatedImage.swift +// Kingfisher +// +// Created by onevcat on 2018/09/26. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import ImageIO + +/// Represents a set of image creating options used in Kingfisher. +public struct ImageCreatingOptions { + + /// The target scale of image needs to be created. + public let scale: CGFloat + + /// The expected animation duration if an animated image being created. + public let duration: TimeInterval + + /// For an animated image, whether or not all frames should be loaded before displaying. + public let preloadAll: Bool + + /// For an animated image, whether or not only the first image should be + /// loaded as a static image. It is useful for preview purpose of an animated image. + public let onlyFirstFrame: Bool + + /// Creates an `ImageCreatingOptions` object. + /// + /// - Parameters: + /// - scale: The target scale of image needs to be created. Default is `1.0`. + /// - duration: The expected animation duration if an animated image being created. + /// A value less or equal to `0.0` means the animated image duration will + /// be determined by the frame data. Default is `0.0`. + /// - preloadAll: For an animated image, whether or not all frames should be loaded before displaying. + /// Default is `false`. + /// - onlyFirstFrame: For an animated image, whether or not only the first image should be + /// loaded as a static image. It is useful for preview purpose of an animated image. + /// Default is `false`. + public init( + scale: CGFloat = 1.0, + duration: TimeInterval = 0.0, + preloadAll: Bool = false, + onlyFirstFrame: Bool = false) + { + self.scale = scale + self.duration = duration + self.preloadAll = preloadAll + self.onlyFirstFrame = onlyFirstFrame + } +} + +/// Represents the decoding for a GIF image. This class extracts frames from an `imageSource`, then +/// hold the images for later use. +public class GIFAnimatedImage { + let images: [KFCrossPlatformImage] + let duration: TimeInterval + + init?(from imageSource: CGImageSource, for info: [String: Any], options: ImageCreatingOptions) { + let frameCount = CGImageSourceGetCount(imageSource) + var images = [KFCrossPlatformImage]() + var gifDuration = 0.0 + + for i in 0 ..< frameCount { + guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, info as CFDictionary) else { + return nil + } + + if frameCount == 1 { + gifDuration = .infinity + } else { + // Get current animated GIF frame duration + gifDuration += GIFAnimatedImage.getFrameDuration(from: imageSource, at: i) + } + images.append(KingfisherWrapper.image(cgImage: imageRef, scale: options.scale, refImage: nil)) + if options.onlyFirstFrame { break } + } + self.images = images + self.duration = gifDuration + } + + /// Calculates frame duration for a gif frame out of the kCGImagePropertyGIFDictionary dictionary. + public static func getFrameDuration(from gifInfo: [String: Any]?) -> TimeInterval { + let defaultFrameDuration = 0.1 + guard let gifInfo = gifInfo else { return defaultFrameDuration } + + let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber + let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber + let duration = unclampedDelayTime ?? delayTime + + guard let frameDuration = duration else { return defaultFrameDuration } + return frameDuration.doubleValue > 0.011 ? frameDuration.doubleValue : defaultFrameDuration + } + + /// Calculates frame duration at a specific index for a gif from an `imageSource`. + public static func getFrameDuration(from imageSource: CGImageSource, at index: Int) -> TimeInterval { + guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, index, nil) + as? [String: Any] else { return 0.0 } + + let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any] + return getFrameDuration(from: gifInfo) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/GraphicsContext.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/GraphicsContext.swift new file mode 100644 index 0000000..6d8443c --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/GraphicsContext.swift @@ -0,0 +1,88 @@ +// +// GraphicsContext.swift +// Kingfisher +// +// Created by taras on 19/04/2021. +// +// Copyright (c) 2021 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif +#if canImport(UIKit) +import UIKit +#endif + +enum GraphicsContext { + static func begin(size: CGSize, scale: CGFloat) { + #if os(macOS) + NSGraphicsContext.saveGraphicsState() + #else + UIGraphicsBeginImageContextWithOptions(size, false, scale) + #endif + } + + static func current(size: CGSize, scale: CGFloat, inverting: Bool, cgImage: CGImage?) -> CGContext? { + #if os(macOS) + guard let rep = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: Int(size.width), + pixelsHigh: Int(size.height), + bitsPerSample: cgImage?.bitsPerComponent ?? 8, + samplesPerPixel: 4, + hasAlpha: true, + isPlanar: false, + colorSpaceName: .calibratedRGB, + bytesPerRow: 0, + bitsPerPixel: 0) else + { + assertionFailure("[Kingfisher] Image representation cannot be created.") + return nil + } + rep.size = size + guard let context = NSGraphicsContext(bitmapImageRep: rep) else { + assertionFailure("[Kingfisher] Image context cannot be created.") + return nil + } + + NSGraphicsContext.current = context + return context.cgContext + #else + guard let context = UIGraphicsGetCurrentContext() else { + return nil + } + if inverting { // If drawing a CGImage, we need to make context flipped. + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: 0, y: -size.height) + } + return context + #endif + } + + static func end() { + #if os(macOS) + NSGraphicsContext.restoreGraphicsState() + #else + UIGraphicsEndImageContext() + #endif + } +} + diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/Image.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/Image.swift new file mode 100644 index 0000000..68373fc --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/Image.swift @@ -0,0 +1,377 @@ +// +// Image.swift +// Kingfisher +// +// Created by Wei Wang on 16/1/6. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +#if os(macOS) +import AppKit +private var imagesKey: Void? +private var durationKey: Void? +#else +import UIKit +import MobileCoreServices +private var imageSourceKey: Void? +#endif + +#if !os(watchOS) +import CoreImage +#endif + +import CoreGraphics +import ImageIO + +private var animatedImageDataKey: Void? +private var imageFrameCountKey: Void? + +// MARK: - Image Properties +extension KingfisherWrapper where Base: KFCrossPlatformImage { + private(set) var animatedImageData: Data? { + get { return getAssociatedObject(base, &animatedImageDataKey) } + set { setRetainedAssociatedObject(base, &animatedImageDataKey, newValue) } + } + + public var imageFrameCount: Int? { + get { return getAssociatedObject(base, &imageFrameCountKey) } + set { setRetainedAssociatedObject(base, &imageFrameCountKey, newValue) } + } + + #if os(macOS) + var cgImage: CGImage? { + return base.cgImage(forProposedRect: nil, context: nil, hints: nil) + } + + var scale: CGFloat { + return 1.0 + } + + private(set) var images: [KFCrossPlatformImage]? { + get { return getAssociatedObject(base, &imagesKey) } + set { setRetainedAssociatedObject(base, &imagesKey, newValue) } + } + + private(set) var duration: TimeInterval { + get { return getAssociatedObject(base, &durationKey) ?? 0.0 } + set { setRetainedAssociatedObject(base, &durationKey, newValue) } + } + + var size: CGSize { + return base.representations.reduce(.zero) { size, rep in + let width = max(size.width, CGFloat(rep.pixelsWide)) + let height = max(size.height, CGFloat(rep.pixelsHigh)) + return CGSize(width: width, height: height) + } + } + #else + var cgImage: CGImage? { return base.cgImage } + var scale: CGFloat { return base.scale } + var images: [KFCrossPlatformImage]? { return base.images } + var duration: TimeInterval { return base.duration } + var size: CGSize { return base.size } + + /// The image source reference of current image. + public private(set) var imageSource: CGImageSource? { + get { return getAssociatedObject(base, &imageSourceKey) } + set { setRetainedAssociatedObject(base, &imageSourceKey, newValue) } + } + #endif + + // Bitmap memory cost with bytes. + var cost: Int { + let pixel = Int(size.width * size.height * scale * scale) + guard let cgImage = cgImage else { + return pixel * 4 + } + let bytesPerPixel = cgImage.bitsPerPixel / 8 + guard let imageCount = images?.count else { + return pixel * bytesPerPixel + } + return pixel * bytesPerPixel * imageCount + } +} + +// MARK: - Image Conversion +extension KingfisherWrapper where Base: KFCrossPlatformImage { + #if os(macOS) + static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage { + return KFCrossPlatformImage(cgImage: cgImage, size: .zero) + } + + /// Normalize the image. This getter does nothing on macOS but return the image itself. + public var normalized: KFCrossPlatformImage { return base } + + #else + /// Creating an image from a give `CGImage` at scale and orientation for refImage. The method signature is for + /// compatibility of macOS version. + static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage { + return KFCrossPlatformImage(cgImage: cgImage, scale: scale, orientation: refImage?.imageOrientation ?? .up) + } + + /// Returns normalized image for current `base` image. + /// This method will try to redraw an image with orientation and scale considered. + public var normalized: KFCrossPlatformImage { + // prevent animated image (GIF) lose it's images + guard images == nil else { return base.copy() as! KFCrossPlatformImage } + // No need to do anything if already up + guard base.imageOrientation != .up else { return base.copy() as! KFCrossPlatformImage } + + return draw(to: size, inverting: true, refImage: KFCrossPlatformImage()) { + fixOrientation(in: $0) + return true + } + } + + func fixOrientation(in context: CGContext) { + + var transform = CGAffineTransform.identity + + let orientation = base.imageOrientation + + switch orientation { + case .down, .downMirrored: + transform = transform.translatedBy(x: size.width, y: size.height) + transform = transform.rotated(by: .pi) + case .left, .leftMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.rotated(by: .pi / 2.0) + case .right, .rightMirrored: + transform = transform.translatedBy(x: 0, y: size.height) + transform = transform.rotated(by: .pi / -2.0) + case .up, .upMirrored: + break + #if compiler(>=5) + @unknown default: + break + #endif + } + + //Flip image one more time if needed to, this is to prevent flipped image + switch orientation { + case .upMirrored, .downMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + case .leftMirrored, .rightMirrored: + transform = transform.translatedBy(x: size.height, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + case .up, .down, .left, .right: + break + #if compiler(>=5) + @unknown default: + break + #endif + } + + context.concatenate(transform) + switch orientation { + case .left, .leftMirrored, .right, .rightMirrored: + context.draw(cgImage!, in: CGRect(x: 0, y: 0, width: size.height, height: size.width)) + default: + context.draw(cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) + } + } + #endif +} + +// MARK: - Image Representation +extension KingfisherWrapper where Base: KFCrossPlatformImage { + /// Returns PNG representation of `base` image. + /// + /// - Returns: PNG data of image. + public func pngRepresentation() -> Data? { + #if os(macOS) + guard let cgImage = cgImage else { + return nil + } + let rep = NSBitmapImageRep(cgImage: cgImage) + return rep.representation(using: .png, properties: [:]) + #else + return base.pngData() + #endif + } + + /// Returns JPEG representation of `base` image. + /// + /// - Parameter compressionQuality: The compression quality when converting image to JPEG data. + /// - Returns: JPEG data of image. + public func jpegRepresentation(compressionQuality: CGFloat) -> Data? { + #if os(macOS) + guard let cgImage = cgImage else { + return nil + } + let rep = NSBitmapImageRep(cgImage: cgImage) + return rep.representation(using:.jpeg, properties: [.compressionFactor: compressionQuality]) + #else + return base.jpegData(compressionQuality: compressionQuality) + #endif + } + + /// Returns GIF representation of `base` image. + /// + /// - Returns: Original GIF data of image. + public func gifRepresentation() -> Data? { + return animatedImageData + } + + /// Returns a data representation for `base` image, with the `format` as the format indicator. + /// - Parameters: + /// - format: The format in which the output data should be. If `unknown`, the `base` image will be + /// converted in the PNG representation. + /// - compressionQuality: The compression quality when converting image to a lossy format data. + /// + /// - Returns: The output data representing. + public func data(format: ImageFormat, compressionQuality: CGFloat = 1.0) -> Data? { + return autoreleasepool { () -> Data? in + let data: Data? + switch format { + case .PNG: data = pngRepresentation() + case .JPEG: data = jpegRepresentation(compressionQuality: compressionQuality) + case .GIF: data = gifRepresentation() + case .unknown: data = normalized.kf.pngRepresentation() + } + + return data + } + } +} + +// MARK: - Creating Images +extension KingfisherWrapper where Base: KFCrossPlatformImage { + + /// Creates an animated image from a given data and options. Currently only GIF data is supported. + /// + /// - Parameters: + /// - data: The animated image data. + /// - options: Options to use when creating the animated image. + /// - Returns: An `Image` object represents the animated image. It is in form of an array of image frames with a + /// certain duration. `nil` if anything wrong when creating animated image. + public static func animatedImage(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? { + let info: [String: Any] = [ + kCGImageSourceShouldCache as String: true, + kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF + ] + + guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else { + return nil + } + + #if os(macOS) + guard let animatedImage = GIFAnimatedImage(from: imageSource, for: info, options: options) else { + return nil + } + var image: KFCrossPlatformImage? + if options.onlyFirstFrame { + image = animatedImage.images.first + } else { + image = KFCrossPlatformImage(data: data) + var kf = image?.kf + kf?.images = animatedImage.images + kf?.duration = animatedImage.duration + } + image?.kf.animatedImageData = data + image?.kf.imageFrameCount = Int(CGImageSourceGetCount(imageSource)) + return image + #else + + var image: KFCrossPlatformImage? + if options.preloadAll || options.onlyFirstFrame { + // Use `images` image if you want to preload all animated data + guard let animatedImage = GIFAnimatedImage(from: imageSource, for: info, options: options) else { + return nil + } + if options.onlyFirstFrame { + image = animatedImage.images.first + } else { + let duration = options.duration <= 0.0 ? animatedImage.duration : options.duration + image = .animatedImage(with: animatedImage.images, duration: duration) + } + image?.kf.animatedImageData = data + } else { + image = KFCrossPlatformImage(data: data, scale: options.scale) + var kf = image?.kf + kf?.imageSource = imageSource + kf?.animatedImageData = data + } + + image?.kf.imageFrameCount = Int(CGImageSourceGetCount(imageSource)) + return image + #endif + } + + /// Creates an image from a given data and options. `.JPEG`, `.PNG` or `.GIF` is supported. For other + /// image format, image initializer from system will be used. If no image object could be created from + /// the given `data`, `nil` will be returned. + /// + /// - Parameters: + /// - data: The image data representation. + /// - options: Options to use when creating the image. + /// - Returns: An `Image` object represents the image if created. If the `data` is invalid or not supported, `nil` + /// will be returned. + public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? { + var image: KFCrossPlatformImage? + switch data.kf.imageFormat { + case .JPEG: + image = KFCrossPlatformImage(data: data, scale: options.scale) + case .PNG: + image = KFCrossPlatformImage(data: data, scale: options.scale) + case .GIF: + image = KingfisherWrapper.animatedImage(data: data, options: options) + case .unknown: + image = KFCrossPlatformImage(data: data, scale: options.scale) + } + return image + } + + /// Creates a downsampled image from given data to a certain size and scale. + /// + /// - Parameters: + /// - data: The image data contains a JPEG or PNG image. + /// - pointSize: The target size in point to which the image should be downsampled. + /// - scale: The scale of result image. + /// - Returns: A downsampled `Image` object following the input conditions. + /// + /// - Note: + /// Different from image `resize` methods, downsampling will not render the original + /// input image in pixel format. It does downsampling from the image data, so it is much + /// more memory efficient and friendly. Choose to use downsampling as possible as you can. + /// + /// The input size should be smaller than the size of input image. If it is larger than the + /// original image size, the result image will be the same size of input without downsampling. + public static func downsampledImage(data: Data, to pointSize: CGSize, scale: CGFloat) -> KFCrossPlatformImage? { + let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary + guard let imageSource = CGImageSourceCreateWithData(data as CFData, imageSourceOptions) else { + return nil + } + + let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale + let downsampleOptions = [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary + guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else { + return nil + } + return KingfisherWrapper.image(cgImage: downsampledImage, scale: scale, refImage: nil) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageDrawing.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageDrawing.swift new file mode 100644 index 0000000..fce627c --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageDrawing.swift @@ -0,0 +1,632 @@ +// +// ImageDrawing.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Accelerate + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif +#if canImport(UIKit) +import UIKit +#endif + +// MARK: - Image Transforming +extension KingfisherWrapper where Base: KFCrossPlatformImage { + // MARK: Blend Mode + /// Create image from `base` image and apply blend mode. + /// + /// - parameter blendMode: The blend mode of creating image. + /// - parameter alpha: The alpha should be used for image. + /// - parameter backgroundColor: The background color for the output image. + /// + /// - returns: An image with blend mode applied. + /// + /// - Note: This method only works for CG-based image. + #if !os(macOS) + public func image(withBlendMode blendMode: CGBlendMode, + alpha: CGFloat = 1.0, + backgroundColor: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage + { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Blend mode image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: .zero, size: size) + return draw(to: rect.size, inverting: false) { _ in + if let backgroundColor = backgroundColor { + backgroundColor.setFill() + UIRectFill(rect) + } + + base.draw(in: rect, blendMode: blendMode, alpha: alpha) + return false + } + } + #endif + + #if os(macOS) + // MARK: Compositing + /// Creates image from `base` image and apply compositing operation. + /// + /// - Parameters: + /// - compositingOperation: The compositing operation of creating image. + /// - alpha: The alpha should be used for image. + /// - backgroundColor: The background color for the output image. + /// - Returns: An image with compositing operation applied. + /// + /// - Note: This method only works for CG-based image. For any non-CG-based image, `base` itself is returned. + public func image(withCompositingOperation compositingOperation: NSCompositingOperation, + alpha: CGFloat = 1.0, + backgroundColor: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage + { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Compositing Operation image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: .zero, size: size) + return draw(to: rect.size, inverting: false) { _ in + if let backgroundColor = backgroundColor { + backgroundColor.setFill() + rect.fill() + } + base.draw(in: rect, from: .zero, operation: compositingOperation, fraction: alpha) + return false + } + } + #endif + + // MARK: Round Corner + + /// Creates a round corner image from on `base` image. + /// + /// - Parameters: + /// - radius: The round corner radius of creating image. + /// - size: The target size of creating image. + /// - corners: The target corners which will be applied rounding. + /// - backgroundColor: The background color for the output image + /// - Returns: An image with round corner of `self`. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func image( + withRadius radius: Radius, + fit size: CGSize, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) -> KFCrossPlatformImage + { + + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Round corner image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) + return draw(to: size, inverting: false) { _ in + #if os(macOS) + if let backgroundColor = backgroundColor { + let rectPath = NSBezierPath(rect: rect) + backgroundColor.setFill() + rectPath.fill() + } + + let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners) + path.addClip() + base.draw(in: rect) + #else + guard let context = UIGraphicsGetCurrentContext() else { + assertionFailure("[Kingfisher] Failed to create CG context for image.") + return false + } + + if let backgroundColor = backgroundColor { + let rectPath = UIBezierPath(rect: rect) + backgroundColor.setFill() + rectPath.fill() + } + + let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners) + context.addPath(path.cgPath) + context.clip() + base.draw(in: rect) + #endif + return false + } + } + + /// Creates a round corner image from on `base` image. + /// + /// - Parameters: + /// - radius: The round corner radius of creating image. + /// - size: The target size of creating image. + /// - corners: The target corners which will be applied rounding. + /// - backgroundColor: The background color for the output image + /// - Returns: An image with round corner of `self`. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func image( + withRoundRadius radius: CGFloat, + fit size: CGSize, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) -> KFCrossPlatformImage + { + image(withRadius: .point(radius), fit: size, roundingCorners: corners, backgroundColor: backgroundColor) + } + + #if os(macOS) + func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> NSBezierPath { + let cornerRadius = radius.compute(with: rect.size) + let path = NSBezierPath(roundedRect: rect, byRoundingCorners: corners, radius: cornerRadius - offsetBase / 2) + path.windingRule = .evenOdd + return path + } + #else + func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> UIBezierPath { + let cornerRadius = radius.compute(with: rect.size) + return UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners.uiRectCorner, + cornerRadii: CGSize( + width: cornerRadius - offsetBase / 2, + height: cornerRadius - offsetBase / 2 + ) + ) + } + #endif + + #if os(iOS) || os(tvOS) + func resize(to size: CGSize, for contentMode: UIView.ContentMode) -> KFCrossPlatformImage { + switch contentMode { + case .scaleAspectFit: + return resize(to: size, for: .aspectFit) + case .scaleAspectFill: + return resize(to: size, for: .aspectFill) + default: + return resize(to: size) + } + } + #endif + + // MARK: Resizing + /// Resizes `base` image to an image with new size. + /// + /// - Parameter size: The target size in point. + /// - Returns: An image with new size. + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func resize(to size: CGSize) -> KFCrossPlatformImage { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Resize only works for CG-based image.") + return base + } + + let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) + return draw(to: size, inverting: false) { _ in + #if os(macOS) + base.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0) + #else + base.draw(in: rect) + #endif + return false + } + } + + /// Resizes `base` image to an image of new size, respecting the given content mode. + /// + /// - Parameters: + /// - targetSize: The target size in point. + /// - contentMode: Content mode of output image should be. + /// - Returns: An image with new size. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func resize(to targetSize: CGSize, for contentMode: ContentMode) -> KFCrossPlatformImage { + let newSize = size.kf.resize(to: targetSize, for: contentMode) + return resize(to: newSize) + } + + // MARK: Cropping + /// Crops `base` image to a new size with a given anchor. + /// + /// - Parameters: + /// - size: The target size. + /// - anchor: The anchor point from which the size should be calculated. + /// - Returns: An image with new size. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func crop(to size: CGSize, anchorOn anchor: CGPoint) -> KFCrossPlatformImage { + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Crop only works for CG-based image.") + return base + } + + let rect = self.size.kf.constrainedRect(for: size, anchor: anchor) + guard let image = cgImage.cropping(to: rect.scaled(scale)) else { + assertionFailure("[Kingfisher] Cropping image failed.") + return base + } + + return KingfisherWrapper.image(cgImage: image, scale: scale, refImage: base) + } + + // MARK: Blur + /// Creates an image with blur effect based on `base` image. + /// + /// - Parameter radius: The blur radius should be used when creating blur effect. + /// - Returns: An image with blur effect applied. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func blurred(withRadius radius: CGFloat) -> KFCrossPlatformImage { + + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Blur only works for CG-based image.") + return base + } + + // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement + // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5) + // if d is odd, use three box-blurs of size 'd', centered on the output pixel. + let s = max(radius, 2.0) + // We will do blur on a resized image (*0.5), so the blur radius could be half as well. + + // Fix the slow compiling time for Swift 3. + // See https://github.com/onevcat/Kingfisher/issues/611 + let pi2 = 2 * CGFloat.pi + let sqrtPi2 = sqrt(pi2) + var targetRadius = floor(s * 3.0 * sqrtPi2 / 4.0 + 0.5) + + if targetRadius.isEven { targetRadius += 1 } + + // Determine necessary iteration count by blur radius. + let iterations: Int + if radius < 0.5 { + iterations = 1 + } else if radius < 1.5 { + iterations = 2 + } else { + iterations = 3 + } + + let w = Int(size.width) + let h = Int(size.height) + + func createEffectBuffer(_ context: CGContext) -> vImage_Buffer { + let data = context.data + let width = vImagePixelCount(context.width) + let height = vImagePixelCount(context.height) + let rowBytes = context.bytesPerRow + + return vImage_Buffer(data: data, height: height, width: width, rowBytes: rowBytes) + } + GraphicsContext.begin(size: size, scale: scale) + guard let context = GraphicsContext.current(size: size, scale: scale, inverting: true, cgImage: cgImage) else { + assertionFailure("[Kingfisher] Failed to create CG context for blurring image.") + return base + } + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: w, height: h)) + GraphicsContext.end() + + var inBuffer = createEffectBuffer(context) + + GraphicsContext.begin(size: size, scale: scale) + guard let outContext = GraphicsContext.current(size: size, scale: scale, inverting: true, cgImage: cgImage) else { + assertionFailure("[Kingfisher] Failed to create CG context for blurring image.") + return base + } + defer { GraphicsContext.end() } + var outBuffer = createEffectBuffer(outContext) + + for _ in 0 ..< iterations { + let flag = vImage_Flags(kvImageEdgeExtend) + vImageBoxConvolve_ARGB8888( + &inBuffer, &outBuffer, nil, 0, 0, UInt32(targetRadius), UInt32(targetRadius), nil, flag) + // Next inBuffer should be the outButter of current iteration + (inBuffer, outBuffer) = (outBuffer, inBuffer) + } + + #if os(macOS) + let result = outContext.makeImage().flatMap { + fixedForRetinaPixel(cgImage: $0, to: size) + } + #else + let result = outContext.makeImage().flatMap { + KFCrossPlatformImage(cgImage: $0, scale: base.scale, orientation: base.imageOrientation) + } + #endif + guard let blurredImage = result else { + assertionFailure("[Kingfisher] Can not make an blurred image within this context.") + return base + } + + return blurredImage + } + + public func addingBorder(_ border: Border) -> KFCrossPlatformImage + { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Blend mode image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: .zero, size: size) + return draw(to: rect.size, inverting: false) { context in + + #if os(macOS) + base.draw(in: rect) + #else + base.draw(in: rect, blendMode: .normal, alpha: 1.0) + #endif + + + let strokeRect = rect.insetBy(dx: border.lineWidth / 2, dy: border.lineWidth / 2) + context.setStrokeColor(border.color.cgColor) + context.setAlpha(border.color.rgba.a) + + let line = pathForRoundCorner( + rect: strokeRect, + radius: border.radius, + corners: border.roundingCorners, + offsetBase: border.lineWidth + ) + line.lineCapStyle = .square + line.lineWidth = border.lineWidth + line.stroke() + + return false + } + } + + // MARK: Overlay + /// Creates an image from `base` image with a color overlay layer. + /// + /// - Parameters: + /// - color: The color should be use to overlay. + /// - fraction: Fraction of input color. From 0.0 to 1.0. 0.0 means solid color, + /// 1.0 means transparent overlay. + /// - Returns: An image with a color overlay applied. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func overlaying(with color: KFCrossPlatformColor, fraction: CGFloat) -> KFCrossPlatformImage { + + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Overlaying only works for CG-based image.") + return base + } + + let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) + return draw(to: rect.size, inverting: false) { context in + #if os(macOS) + base.draw(in: rect) + if fraction > 0 { + color.withAlphaComponent(1 - fraction).set() + rect.fill(using: .sourceAtop) + } + #else + color.set() + UIRectFill(rect) + base.draw(in: rect, blendMode: .destinationIn, alpha: 1.0) + + if fraction > 0 { + base.draw(in: rect, blendMode: .sourceAtop, alpha: fraction) + } + #endif + return false + } + } + + // MARK: Tint + /// Creates an image from `base` image with a color tint. + /// + /// - Parameter color: The color should be used to tint `base` + /// - Returns: An image with a color tint applied. + public func tinted(with color: KFCrossPlatformColor) -> KFCrossPlatformImage { + #if os(watchOS) + return base + #else + return apply(.tint(color)) + #endif + } + + // MARK: Color Control + + /// Create an image from `self` with color control. + /// + /// - Parameters: + /// - brightness: Brightness changing to image. + /// - contrast: Contrast changing to image. + /// - saturation: Saturation changing to image. + /// - inputEV: InputEV changing to image. + /// - Returns: An image with color control applied. + public func adjusted(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) -> KFCrossPlatformImage { + #if os(watchOS) + return base + #else + return apply(.colorControl((brightness, contrast, saturation, inputEV))) + #endif + } + + /// Return an image with given scale. + /// + /// - Parameter scale: Target scale factor the new image should have. + /// - Returns: The image with target scale. If the base image is already in the scale, `base` will be returned. + public func scaled(to scale: CGFloat) -> KFCrossPlatformImage { + guard scale != self.scale else { + return base + } + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Scaling only works for CG-based image.") + return base + } + return KingfisherWrapper.image(cgImage: cgImage, scale: scale, refImage: base) + } +} + +// MARK: - Decoding Image +extension KingfisherWrapper where Base: KFCrossPlatformImage { + + /// Returns the decoded image of the `base` image. It will draw the image in a plain context and return the data + /// from it. This could improve the drawing performance when an image is just created from data but not yet + /// displayed for the first time. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image or animated image, `base` itself is returned. + public var decoded: KFCrossPlatformImage { return decoded(scale: scale) } + + /// Returns decoded image of the `base` image at a given scale. It will draw the image in a plain context and + /// return the data from it. This could improve the drawing performance when an image is just created from + /// data but not yet displayed for the first time. + /// + /// - Parameter scale: The given scale of target image should be. + /// - Returns: The decoded image ready to be displayed. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image or animated image, `base` itself is returned. + public func decoded(scale: CGFloat) -> KFCrossPlatformImage { + // Prevent animated image (GIF) losing it's images + #if os(iOS) + if imageSource != nil { return base } + #else + if images != nil { return base } + #endif + + guard let imageRef = cgImage else { + assertionFailure("[Kingfisher] Decoding only works for CG-based image.") + return base + } + + let size = CGSize(width: CGFloat(imageRef.width) / scale, height: CGFloat(imageRef.height) / scale) + return draw(to: size, inverting: true, scale: scale) { context in + context.draw(imageRef, in: CGRect(origin: .zero, size: size)) + return true + } + } + + /// Returns decoded image of the `base` image at a given scale. It will draw the image in a plain context and + /// return the data from it. This could improve the drawing performance when an image is just created from + /// data but not yet displayed for the first time. + /// + /// - Parameter context: The context for drawing. + /// - Returns: The decoded image ready to be displayed. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image or animated image, `base` itself is returned. + public func decoded(on context: CGContext) -> KFCrossPlatformImage { + // Prevent animated image (GIF) losing it's images + #if os(iOS) + if imageSource != nil { return base } + #else + if images != nil { return base } + #endif + + guard let refImage = cgImage else { + assertionFailure("[Kingfisher] Decoding only works for CG-based image.") + return base + } + + let size = CGSize(width: CGFloat(refImage.width) / scale, height: CGFloat(refImage.height) / scale) + + context.draw(refImage, in: CGRect(origin: .zero, size: size)) + + guard let cgImage = context.makeImage() else { + return base + } + + return KingfisherWrapper.image(cgImage: cgImage, scale: scale, refImage: base) + } +} + +extension KingfisherWrapper where Base: KFCrossPlatformImage { + func draw( + to size: CGSize, + inverting: Bool, + scale: CGFloat? = nil, + refImage: KFCrossPlatformImage? = nil, + draw: (CGContext) -> Bool // Whether use the refImage (`true`) or ignore image orientation (`false`) + ) -> KFCrossPlatformImage + { + #if os(macOS) || os(watchOS) + let targetScale = scale ?? self.scale + GraphicsContext.begin(size: size, scale: targetScale) + guard let context = GraphicsContext.current(size: size, scale: targetScale, inverting: inverting, cgImage: cgImage) else { + assertionFailure("[Kingfisher] Failed to create CG context for blurring image.") + return base + } + defer { GraphicsContext.end() } + let useRefImage = draw(context) + guard let cgImage = context.makeImage() else { + return base + } + let ref = useRefImage ? (refImage ?? base) : nil + return KingfisherWrapper.image(cgImage: cgImage, scale: targetScale, refImage: ref) + #else + + let format = UIGraphicsImageRendererFormat.preferred() + format.scale = scale ?? self.scale + let renderer = UIGraphicsImageRenderer(size: size, format: format) + + var useRefImage: Bool = false + let image = renderer.image { rendererContext in + + let context = rendererContext.cgContext + if inverting { // If drawing a CGImage, we need to make context flipped. + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: 0, y: -size.height) + } + + useRefImage = draw(context) + } + if useRefImage { + guard let cgImage = image.cgImage else { + return base + } + let ref = refImage ?? base + return KingfisherWrapper.image(cgImage: cgImage, scale: format.scale, refImage: ref) + } else { + return image + } + #endif + } + + #if os(macOS) + func fixedForRetinaPixel(cgImage: CGImage, to size: CGSize) -> KFCrossPlatformImage { + + let image = KFCrossPlatformImage(cgImage: cgImage, size: base.size) + let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) + + return draw(to: self.size, inverting: false) { context in + image.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0) + return false + } + } + #endif +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageFormat.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageFormat.swift new file mode 100644 index 0000000..464a855 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageFormat.swift @@ -0,0 +1,130 @@ +// +// ImageFormat.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents image format. +/// +/// - unknown: The format cannot be recognized or not supported yet. +/// - PNG: PNG image format. +/// - JPEG: JPEG image format. +/// - GIF: GIF image format. +public enum ImageFormat { + /// The format cannot be recognized or not supported yet. + case unknown + /// PNG image format. + case PNG + /// JPEG image format. + case JPEG + /// GIF image format. + case GIF + + struct HeaderData { + static var PNG: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] + static var JPEG_SOI: [UInt8] = [0xFF, 0xD8] + static var JPEG_IF: [UInt8] = [0xFF] + static var GIF: [UInt8] = [0x47, 0x49, 0x46] + } + + /// https://en.wikipedia.org/wiki/JPEG + public enum JPEGMarker { + case SOF0 //baseline + case SOF2 //progressive + case DHT //Huffman Table + case DQT //Quantization Table + case DRI //Restart Interval + case SOS //Start Of Scan + case RSTn(UInt8) //Restart + case APPn //Application-specific + case COM //Comment + case EOI //End Of Image + + var bytes: [UInt8] { + switch self { + case .SOF0: return [0xFF, 0xC0] + case .SOF2: return [0xFF, 0xC2] + case .DHT: return [0xFF, 0xC4] + case .DQT: return [0xFF, 0xDB] + case .DRI: return [0xFF, 0xDD] + case .SOS: return [0xFF, 0xDA] + case .RSTn(let n): return [0xFF, 0xD0 + n] + case .APPn: return [0xFF, 0xE0] + case .COM: return [0xFF, 0xFE] + case .EOI: return [0xFF, 0xD9] + } + } + } +} + + +extension Data: KingfisherCompatibleValue {} + +// MARK: - Misc Helpers +extension KingfisherWrapper where Base == Data { + /// Gets the image format corresponding to the data. + public var imageFormat: ImageFormat { + guard base.count > 8 else { return .unknown } + + var buffer = [UInt8](repeating: 0, count: 8) + base.copyBytes(to: &buffer, count: 8) + + if buffer == ImageFormat.HeaderData.PNG { + return .PNG + + } else if buffer[0] == ImageFormat.HeaderData.JPEG_SOI[0], + buffer[1] == ImageFormat.HeaderData.JPEG_SOI[1], + buffer[2] == ImageFormat.HeaderData.JPEG_IF[0] + { + return .JPEG + + } else if buffer[0] == ImageFormat.HeaderData.GIF[0], + buffer[1] == ImageFormat.HeaderData.GIF[1], + buffer[2] == ImageFormat.HeaderData.GIF[2] + { + return .GIF + } + + return .unknown + } + + public func contains(jpeg marker: ImageFormat.JPEGMarker) -> Bool { + guard imageFormat == .JPEG else { + return false + } + + let bytes = [UInt8](base) + let markerBytes = marker.bytes + for (index, item) in bytes.enumerated() where bytes.count > index + 1 { + guard + item == markerBytes.first, + bytes[index + 1] == markerBytes[1] else { + continue + } + return true + } + return false + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageProcessor.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageProcessor.swift new file mode 100644 index 0000000..2123822 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageProcessor.swift @@ -0,0 +1,935 @@ +// +// ImageProcessor.swift +// Kingfisher +// +// Created by Wei Wang on 2016/08/26. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CoreGraphics + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif + +/// Represents an item which could be processed by an `ImageProcessor`. +/// +/// - image: Input image. The processor should provide a way to apply +/// processing on this `image` and return the result image. +/// - data: Input data. The processor should provide a way to apply +/// processing on this `data` and return the result image. +public enum ImageProcessItem { + + /// Input image. The processor should provide a way to apply + /// processing on this `image` and return the result image. + case image(KFCrossPlatformImage) + + /// Input data. The processor should provide a way to apply + /// processing on this `data` and return the result image. + case data(Data) +} + +/// An `ImageProcessor` would be used to convert some downloaded data to an image. +public protocol ImageProcessor { + /// Identifier of the processor. It will be used to identify the processor when + /// caching and retrieving an image. You might want to make sure that processors with + /// same properties/functionality have the same identifiers, so correct processed images + /// could be retrieved with proper key. + /// + /// - Note: Do not supply an empty string for a customized processor, which is already reserved by + /// the `DefaultImageProcessor`. It is recommended to use a reverse domain name notation string of + /// your own for the identifier. + var identifier: String { get } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: The parsed options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: The return value should be `nil` if processing failed while converting an input item to image. + /// If `nil` received by the processing caller, an error will be reported and the process flow stops. + /// If the processing flow is not critical for your flow, then when the input item is already an image + /// (`.image` case) and there is any errors in the processing, you could return the input image itself + /// to keep the processing pipeline continuing. + /// - Note: Most processor only supports CG-based images. watchOS is not supported for processors containing + /// a filter, the input image will be returned directly on watchOS. + func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? +} + +extension ImageProcessor { + + /// Appends an `ImageProcessor` to another. The identifier of the new `ImageProcessor` + /// will be "\(self.identifier)|>\(another.identifier)". + /// + /// - Parameter another: An `ImageProcessor` you want to append to `self`. + /// - Returns: The new `ImageProcessor` will process the image in the order + /// of the two processors concatenated. + public func append(another: ImageProcessor) -> ImageProcessor { + let newIdentifier = identifier.appending("|>\(another.identifier)") + return GeneralProcessor(identifier: newIdentifier) { + item, options in + if let image = self.process(item: item, options: options) { + return another.process(item: .image(image), options: options) + } else { + return nil + } + } + } +} + +func ==(left: ImageProcessor, right: ImageProcessor) -> Bool { + return left.identifier == right.identifier +} + +func !=(left: ImageProcessor, right: ImageProcessor) -> Bool { + return !(left == right) +} + +typealias ProcessorImp = ((ImageProcessItem, KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?) +struct GeneralProcessor: ImageProcessor { + let identifier: String + let p: ProcessorImp + func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return p(item, options) + } +} + +/// The default processor. It converts the input data to a valid image. +/// Images of .PNG, .JPEG and .GIF format are supported. +/// If an image item is given as `.image` case, `DefaultImageProcessor` will +/// do nothing on it and return the associated image. +public struct DefaultImageProcessor: ImageProcessor { + + /// A default `DefaultImageProcessor` could be used across. + public static let `default` = DefaultImageProcessor() + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier = "" + + /// Creates a `DefaultImageProcessor`. Use `DefaultImageProcessor.default` to get an instance, + /// if you do not have a good reason to create your own `DefaultImageProcessor`. + public init() {} + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + case .data(let data): + return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) + } + } +} + +/// Represents the rect corner setting when processing a round corner image. +public struct RectCorner: OptionSet { + + /// Raw value of the rect corner. + public let rawValue: Int + + /// Represents the top left corner. + public static let topLeft = RectCorner(rawValue: 1 << 0) + + /// Represents the top right corner. + public static let topRight = RectCorner(rawValue: 1 << 1) + + /// Represents the bottom left corner. + public static let bottomLeft = RectCorner(rawValue: 1 << 2) + + /// Represents the bottom right corner. + public static let bottomRight = RectCorner(rawValue: 1 << 3) + + /// Represents all corners. + public static let all: RectCorner = [.topLeft, .topRight, .bottomLeft, .bottomRight] + + /// Creates a `RectCorner` option set with a given value. + /// + /// - Parameter rawValue: The value represents a certain corner option. + public init(rawValue: Int) { + self.rawValue = rawValue + } + + var cornerIdentifier: String { + if self == .all { + return "" + } + return "_corner(\(rawValue))" + } +} + +#if !os(macOS) +/// Processor for adding an blend mode to images. Only CG-based images are supported. +public struct BlendImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Blend Mode will be used to blend the input image. + public let blendMode: CGBlendMode + + /// Alpha will be used when blend image. + public let alpha: CGFloat + + /// Background color of the output image. If `nil`, it will stay transparent. + public let backgroundColor: KFCrossPlatformColor? + + /// Creates a `BlendImageProcessor`. + /// + /// - Parameters: + /// - blendMode: Blend Mode will be used to blend the input image. + /// - alpha: Alpha will be used when blend image. From 0.0 to 1.0. 1.0 means solid image, + /// 0.0 means transparent image (not visible at all). Default is 1.0. + /// - backgroundColor: Background color to apply for the output image. Default is `nil`. + public init(blendMode: CGBlendMode, alpha: CGFloat = 1.0, backgroundColor: KFCrossPlatformColor? = nil) { + self.blendMode = blendMode + self.alpha = alpha + self.backgroundColor = backgroundColor + var identifier = "com.onevcat.Kingfisher.BlendImageProcessor(\(blendMode.rawValue),\(alpha))" + if let color = backgroundColor { + identifier.append("_\(color.rgbaDescription)") + } + self.identifier = identifier + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.image(withBlendMode: blendMode, alpha: alpha, backgroundColor: backgroundColor) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} +#endif + +#if os(macOS) +/// Processor for adding an compositing operation to images. Only CG-based images are supported in macOS. +public struct CompositingImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Compositing operation will be used to the input image. + public let compositingOperation: NSCompositingOperation + + /// Alpha will be used when compositing image. + public let alpha: CGFloat + + /// Background color of the output image. If `nil`, it will stay transparent. + public let backgroundColor: KFCrossPlatformColor? + + /// Creates a `CompositingImageProcessor` + /// + /// - Parameters: + /// - compositingOperation: Compositing operation will be used to the input image. + /// - alpha: Alpha will be used when compositing image. + /// From 0.0 to 1.0. 1.0 means solid image, 0.0 means transparent image. + /// Default is 1.0. + /// - backgroundColor: Background color to apply for the output image. Default is `nil`. + public init(compositingOperation: NSCompositingOperation, + alpha: CGFloat = 1.0, + backgroundColor: KFCrossPlatformColor? = nil) + { + self.compositingOperation = compositingOperation + self.alpha = alpha + self.backgroundColor = backgroundColor + var identifier = "com.onevcat.Kingfisher.CompositingImageProcessor(\(compositingOperation.rawValue),\(alpha))" + if let color = backgroundColor { + identifier.append("_\(color.rgbaDescription)") + } + self.identifier = identifier + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.image( + withCompositingOperation: compositingOperation, + alpha: alpha, + backgroundColor: backgroundColor) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} +#endif + +/// Represents a radius specified in a `RoundCornerImageProcessor`. +public enum Radius { + /// The radius should be calculated as a fraction of the image width. Typically the associated value should be + /// between 0 and 0.5, where 0 represents no radius and 0.5 represents using half of the image width. + case widthFraction(CGFloat) + /// The radius should be calculated as a fraction of the image height. Typically the associated value should be + /// between 0 and 0.5, where 0 represents no radius and 0.5 represents using half of the image height. + case heightFraction(CGFloat) + /// Use a fixed point value as the round corner radius. + case point(CGFloat) + + var radiusIdentifier: String { + switch self { + case .widthFraction(let f): + return "w_frac_\(f)" + case .heightFraction(let f): + return "h_frac_\(f)" + case .point(let p): + return p.description + } + } + + public func compute(with size: CGSize) -> CGFloat { + let cornerRadius: CGFloat + switch self { + case .point(let point): + cornerRadius = point + case .widthFraction(let widthFraction): + cornerRadius = size.width * widthFraction + case .heightFraction(let heightFraction): + cornerRadius = size.height * heightFraction + } + return cornerRadius + } +} + +/// Processor for making round corner images. Only CG-based images are supported in macOS, +/// if a non-CG image passed in, the processor will do nothing. +/// +/// - Note: The input image will be rendered with round corner pixels removed. If the image itself does not contain +/// alpha channel (for example, a JPEG image), the processed image will contain an alpha channel in memory in order +/// to show correctly. However, when cached to disk, Kingfisher respects the original image format by default. That +/// means the alpha channel will be removed for these images. When you load the processed image from cache again, you +/// will lose transparent corner. +/// +/// You could use `FormatIndicatedCacheSerializer.png` to force Kingfisher to serialize the image to PNG format in this +/// case. +/// +public struct RoundCornerImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// The radius will be applied in processing. Specify a certain point value with `.point`, or a fraction of the + /// target image with `.widthFraction`. or `.heightFraction`. For example, given a square image with width and + /// height equals, `.widthFraction(0.5)` means use half of the length of size and makes the final image a round one. + public let radius: Radius + + /// The target corners which will be applied rounding. + public let roundingCorners: RectCorner + + /// Target size of output image should be. If `nil`, the image will keep its original size after processing. + public let targetSize: CGSize? + + /// Background color of the output image. If `nil`, it will use a transparent background. + public let backgroundColor: KFCrossPlatformColor? + + /// Creates a `RoundCornerImageProcessor`. + /// + /// - Parameters: + /// - cornerRadius: Corner radius in point will be applied in processing. + /// - targetSize: Target size of output image should be. If `nil`, + /// the image will keep its original size after processing. + /// Default is `nil`. + /// - corners: The target corners which will be applied rounding. Default is `.all`. + /// - backgroundColor: Background color to apply for the output image. Default is `nil`. + /// + /// - Note: + /// + /// This initializer accepts a concrete point value for `cornerRadius`. If you do not know the image size, but still + /// want to apply a full round-corner (making the final image a round one), or specify the corner radius as a + /// fraction of one dimension of the target image, use the `Radius` version instead. + /// + public init( + cornerRadius: CGFloat, + targetSize: CGSize? = nil, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) + { + let radius = Radius.point(cornerRadius) + self.init(radius: radius, targetSize: targetSize, roundingCorners: corners, backgroundColor: backgroundColor) + } + + /// Creates a `RoundCornerImageProcessor`. + /// + /// - Parameters: + /// - radius: The radius will be applied in processing. + /// - targetSize: Target size of output image should be. If `nil`, + /// the image will keep its original size after processing. + /// Default is `nil`. + /// - corners: The target corners which will be applied rounding. Default is `.all`. + /// - backgroundColor: Background color to apply for the output image. Default is `nil`. + public init( + radius: Radius, + targetSize: CGSize? = nil, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) + { + self.radius = radius + self.targetSize = targetSize + self.roundingCorners = corners + self.backgroundColor = backgroundColor + + self.identifier = { + var identifier = "" + + if let size = targetSize { + identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" + + "(\(radius.radiusIdentifier)_\(size)\(corners.cornerIdentifier))" + } else { + identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" + + "(\(radius.radiusIdentifier)\(corners.cornerIdentifier))" + } + if let backgroundColor = backgroundColor { + identifier += "_\(backgroundColor)" + } + + return identifier + }() + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + let size = targetSize ?? image.kf.size + return image.kf.scaled(to: options.scaleFactor) + .kf.image( + withRadius: radius, + fit: size, + roundingCorners: roundingCorners, + backgroundColor: backgroundColor) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +public struct Border { + public var color: KFCrossPlatformColor + public var lineWidth: CGFloat + + /// The radius will be applied in processing. Specify a certain point value with `.point`, or a fraction of the + /// target image with `.widthFraction`. or `.heightFraction`. For example, given a square image with width and + /// height equals, `.widthFraction(0.5)` means use half of the length of size and makes the final image a round one. + public var radius: Radius + + /// The target corners which will be applied rounding. + public var roundingCorners: RectCorner + + public init( + color: KFCrossPlatformColor = .black, + lineWidth: CGFloat = 4, + radius: Radius = .point(0), + roundingCorners: RectCorner = .all + ) { + self.color = color + self.lineWidth = lineWidth + self.radius = radius + self.roundingCorners = roundingCorners + } + + var identifier: String { + "\(color.rgbaDescription)_\(lineWidth)_\(radius.radiusIdentifier)_\(roundingCorners.cornerIdentifier)" + } +} + +public struct BorderImageProcessor: ImageProcessor { + public var identifier: String { "com.onevcat.Kingfisher.RoundCornerImageProcessor(\(border)" } + public let border: Border + + public init(border: Border) { + self.border = border + } + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.addingBorder(border) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Represents how a size adjusts itself to fit a target size. +/// +/// - none: Not scale the content. +/// - aspectFit: Scales the content to fit the size of the view by maintaining the aspect ratio. +/// - aspectFill: Scales the content to fill the size of the view. +public enum ContentMode { + /// Not scale the content. + case none + /// Scales the content to fit the size of the view by maintaining the aspect ratio. + case aspectFit + /// Scales the content to fill the size of the view. + case aspectFill +} + +/// Processor for resizing images. +/// If you need to resize a data represented image to a smaller size, use `DownsamplingImageProcessor` +/// instead, which is more efficient and uses less memory. +public struct ResizingImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// The reference size for resizing operation in point. + public let referenceSize: CGSize + + /// Target content mode of output image should be. + /// Default is `.none`. + public let targetContentMode: ContentMode + + /// Creates a `ResizingImageProcessor`. + /// + /// - Parameters: + /// - referenceSize: The reference size for resizing operation in point. + /// - mode: Target content mode of output image should be. + /// + /// - Note: + /// The instance of `ResizingImageProcessor` will follow its `mode` property + /// and try to resizing the input images to fit or fill the `referenceSize`. + /// That means if you are using a `mode` besides of `.none`, you may get an + /// image with its size not be the same as the `referenceSize`. + /// + /// **Example**: With input image size: {100, 200}, + /// `referenceSize`: {100, 100}, `mode`: `.aspectFit`, + /// you will get an output image with size of {50, 100}, which "fit"s + /// the `referenceSize`. + /// + /// If you need an output image exactly to be a specified size, append or use + /// a `CroppingImageProcessor`. + public init(referenceSize: CGSize, mode: ContentMode = .none) { + self.referenceSize = referenceSize + self.targetContentMode = mode + + if mode == .none { + self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(referenceSize))" + } else { + self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(referenceSize), \(mode))" + } + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.resize(to: referenceSize, for: targetContentMode) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for adding blur effect to images. `Accelerate.framework` is used underhood for +/// a better performance. A simulated Gaussian blur with specified blur radius will be applied. +public struct BlurImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Blur radius for the simulated Gaussian blur. + public let blurRadius: CGFloat + + /// Creates a `BlurImageProcessor` + /// + /// - parameter blurRadius: Blur radius for the simulated Gaussian blur. + public init(blurRadius: CGFloat) { + self.blurRadius = blurRadius + self.identifier = "com.onevcat.Kingfisher.BlurImageProcessor(\(blurRadius))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + let radius = blurRadius * options.scaleFactor + return image.kf.scaled(to: options.scaleFactor) + .kf.blurred(withRadius: radius) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for adding an overlay to images. Only CG-based images are supported in macOS. +public struct OverlayImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Overlay color will be used to overlay the input image. + public let overlay: KFCrossPlatformColor + + /// Fraction will be used when overlay the color to image. + public let fraction: CGFloat + + /// Creates an `OverlayImageProcessor` + /// + /// - parameter overlay: Overlay color will be used to overlay the input image. + /// - parameter fraction: Fraction will be used when overlay the color to image. + /// From 0.0 to 1.0. 0.0 means solid color, 1.0 means transparent overlay. + public init(overlay: KFCrossPlatformColor, fraction: CGFloat = 0.5) { + self.overlay = overlay + self.fraction = fraction + self.identifier = "com.onevcat.Kingfisher.OverlayImageProcessor(\(overlay.rgbaDescription)_\(fraction))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.overlaying(with: overlay, fraction: fraction) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for tint images with color. Only CG-based images are supported. +public struct TintImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Tint color will be used to tint the input image. + public let tint: KFCrossPlatformColor + + /// Creates a `TintImageProcessor` + /// + /// - parameter tint: Tint color will be used to tint the input image. + public init(tint: KFCrossPlatformColor) { + self.tint = tint + self.identifier = "com.onevcat.Kingfisher.TintImageProcessor(\(tint.rgbaDescription))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.tinted(with: tint) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for applying some color control to images. Only CG-based images are supported. +/// watchOS is not supported. +public struct ColorControlsProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Brightness changing to image. + public let brightness: CGFloat + + /// Contrast changing to image. + public let contrast: CGFloat + + /// Saturation changing to image. + public let saturation: CGFloat + + /// InputEV changing to image. + public let inputEV: CGFloat + + /// Creates a `ColorControlsProcessor` + /// + /// - Parameters: + /// - brightness: Brightness changing to image. + /// - contrast: Contrast changing to image. + /// - saturation: Saturation changing to image. + /// - inputEV: InputEV changing to image. + public init(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) { + self.brightness = brightness + self.contrast = contrast + self.saturation = saturation + self.inputEV = inputEV + self.identifier = "com.onevcat.Kingfisher.ColorControlsProcessor(\(brightness)_\(contrast)_\(saturation)_\(inputEV))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.adjusted(brightness: brightness, contrast: contrast, saturation: saturation, inputEV: inputEV) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for applying black and white effect to images. Only CG-based images are supported. +/// watchOS is not supported. +public struct BlackWhiteProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier = "com.onevcat.Kingfisher.BlackWhiteProcessor" + + /// Creates a `BlackWhiteProcessor` + public init() {} + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return ColorControlsProcessor(brightness: 0.0, contrast: 1.0, saturation: 0.0, inputEV: 0.7) + .process(item: item, options: options) + } +} + +/// Processor for cropping an image. Only CG-based images are supported. +/// watchOS is not supported. +public struct CroppingImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Target size of output image should be. + public let size: CGSize + + /// Anchor point from which the output size should be calculate. + /// The anchor point is consisted by two values between 0.0 and 1.0. + /// It indicates a related point in current image. + /// See `CroppingImageProcessor.init(size:anchor:)` for more. + public let anchor: CGPoint + + /// Creates a `CroppingImageProcessor`. + /// + /// - Parameters: + /// - size: Target size of output image should be. + /// - anchor: The anchor point from which the size should be calculated. + /// Default is `CGPoint(x: 0.5, y: 0.5)`, which means the center of input image. + /// - Note: + /// The anchor point is consisted by two values between 0.0 and 1.0. + /// It indicates a related point in current image, eg: (0.0, 0.0) for top-left + /// corner, (0.5, 0.5) for center and (1.0, 1.0) for bottom-right corner. + /// The `size` property of `CroppingImageProcessor` will be used along with + /// `anchor` to calculate a target rectangle in the size of image. + /// + /// The target size will be automatically calculated with a reasonable behavior. + /// For example, when you have an image size of `CGSize(width: 100, height: 100)`, + /// and a target size of `CGSize(width: 20, height: 20)`: + /// - with a (0.0, 0.0) anchor (top-left), the crop rect will be `{0, 0, 20, 20}`; + /// - with a (0.5, 0.5) anchor (center), it will be `{40, 40, 20, 20}` + /// - while with a (1.0, 1.0) anchor (bottom-right), it will be `{80, 80, 20, 20}` + public init(size: CGSize, anchor: CGPoint = CGPoint(x: 0.5, y: 0.5)) { + self.size = size + self.anchor = anchor + self.identifier = "com.onevcat.Kingfisher.CroppingImageProcessor(\(size)_\(anchor))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.crop(to: size, anchorOn: anchor) + case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for downsampling an image. Compared to `ResizingImageProcessor`, this processor +/// does not render the images to resize. Instead, it downsamples the input data directly to an +/// image. It is a more efficient than `ResizingImageProcessor`. Prefer to use `DownsamplingImageProcessor` as possible +/// as you can than the `ResizingImageProcessor`. +/// +/// Only CG-based images are supported. Animated images (like GIF) is not supported. +public struct DownsamplingImageProcessor: ImageProcessor { + + /// Target size of output image should be. It should be smaller than the size of + /// input image. If it is larger, the result image will be the same size of input + /// data without downsampling. + public let size: CGSize + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Creates a `DownsamplingImageProcessor`. + /// + /// - Parameter size: The target size of the downsample operation. + public init(size: CGSize) { + self.size = size + self.identifier = "com.onevcat.Kingfisher.DownsamplingImageProcessor(\(size))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + guard let data = image.kf.data(format: .unknown) else { + return nil + } + return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor) + case .data(let data): + return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor) + } + } +} + +infix operator |>: AdditionPrecedence +public func |>(left: ImageProcessor, right: ImageProcessor) -> ImageProcessor { + return left.append(another: right) +} + +extension KFCrossPlatformColor { + + var rgba: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + + #if os(macOS) + (usingColorSpace(.extendedSRGB) ?? self).getRed(&r, green: &g, blue: &b, alpha: &a) + #else + getRed(&r, green: &g, blue: &b, alpha: &a) + #endif + + return (r, g, b, a) + } + + var rgbaDescription: String { + let components = self.rgba + return String(format: "(%.2f,%.2f,%.2f,%.2f)", components.r, components.g, components.b, components.a) + } + + @available(*, deprecated, message: "`hex` is not safe for colors in extended space. Do not use this.") + var hex: String { + + let (r, g, b, a) = rgba + + let rInt = Int(r * 255) << 24 + let gInt = Int(g * 255) << 16 + let bInt = Int(b * 255) << 8 + let aInt = Int(a * 255) + + let rgba = rInt | gInt | bInt | aInt + + return String(format:"#%08x", rgba) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageProgressive.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageProgressive.swift new file mode 100644 index 0000000..0e4f3cb --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageProgressive.swift @@ -0,0 +1,328 @@ +// +// ImageProgressive.swift +// Kingfisher +// +// Created by lixiang on 2019/5/10. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CoreGraphics + +private let sharedProcessingQueue: CallbackQueue = + .dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process")) + +public struct ImageProgressive { + + /// A default `ImageProgressive` could be used across. It blurs the progressive loading with the fastest + /// scan enabled and scan interval as 0. + public static let `default` = ImageProgressive( + isBlur: true, + isFastestScan: true, + scanInterval: 0 + ) + + /// Whether to enable blur effect processing + let isBlur: Bool + /// Whether to enable the fastest scan + let isFastestScan: Bool + /// Minimum time interval for each scan + let scanInterval: TimeInterval + + public init(isBlur: Bool, + isFastestScan: Bool, + scanInterval: TimeInterval + ) + { + self.isBlur = isBlur + self.isFastestScan = isFastestScan + self.scanInterval = scanInterval + } +} + +protocol ImageSettable: AnyObject { + var image: KFCrossPlatformImage? { get set } +} + +final class ImageProgressiveProvider: DataReceivingSideEffect { + + var onShouldApply: () -> Bool = { return true } + + func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) { + + DispatchQueue.main.async { + guard self.onShouldApply() else { return } + self.update(data: task.mutableData, with: task.callbacks) + } + } + + private let option: ImageProgressive + private let refresh: (KFCrossPlatformImage) -> Void + + private let decoder: ImageProgressiveDecoder + private let queue = ImageProgressiveSerialQueue() + + init?(_ options: KingfisherParsedOptionsInfo, + refresh: @escaping (KFCrossPlatformImage) -> Void) { + guard let option = options.progressiveJPEG else { return nil } + + self.option = option + self.refresh = refresh + self.decoder = ImageProgressiveDecoder( + option, + processingQueue: options.processingQueue ?? sharedProcessingQueue, + creatingOptions: options.imageCreatingOptions + ) + } + + func update(data: Data, with callbacks: [SessionDataTask.TaskCallback]) { + guard !data.isEmpty else { return } + + queue.add(minimum: option.scanInterval) { completion in + + func decode(_ data: Data) { + self.decoder.decode(data, with: callbacks) { image in + defer { completion() } + guard self.onShouldApply() else { return } + guard let image = image else { return } + self.refresh(image) + } + } + + let semaphore = DispatchSemaphore(value: 0) + var onShouldApply: Bool = false + + CallbackQueue.mainAsync.execute { + onShouldApply = self.onShouldApply() + semaphore.signal() + } + semaphore.wait() + guard onShouldApply else { + self.queue.clean() + completion() + return + } + + if self.option.isFastestScan { + decode(self.decoder.scanning(data) ?? Data()) + } else { + self.decoder.scanning(data).forEach { decode($0) } + } + } + } +} + +private final class ImageProgressiveDecoder { + + private let option: ImageProgressive + private let processingQueue: CallbackQueue + private let creatingOptions: ImageCreatingOptions + private(set) var scannedCount = 0 + private(set) var scannedIndex = -1 + + init(_ option: ImageProgressive, + processingQueue: CallbackQueue, + creatingOptions: ImageCreatingOptions) { + self.option = option + self.processingQueue = processingQueue + self.creatingOptions = creatingOptions + } + + func scanning(_ data: Data) -> [Data] { + guard data.kf.contains(jpeg: .SOF2) else { + return [] + } + guard scannedIndex + 1 < data.count else { + return [] + } + + var datas: [Data] = [] + var index = scannedIndex + 1 + var count = scannedCount + + while index < data.count - 1 { + scannedIndex = index + // 0xFF, 0xDA - Start Of Scan + let SOS = ImageFormat.JPEGMarker.SOS.bytes + if data[index] == SOS[0], data[index + 1] == SOS[1] { + if count > 0 { + datas.append(data[0 ..< index]) + } + count += 1 + } + index += 1 + } + + // Found more scans this the previous time + guard count > scannedCount else { return [] } + scannedCount = count + + // `> 1` checks that we've received a first scan (SOS) and then received + // and also received a second scan (SOS). This way we know that we have + // at least one full scan available. + guard count > 1 else { return [] } + return datas + } + + func scanning(_ data: Data) -> Data? { + guard data.kf.contains(jpeg: .SOF2) else { + return nil + } + guard scannedIndex + 1 < data.count else { + return nil + } + + var index = scannedIndex + 1 + var count = scannedCount + var lastSOSIndex = 0 + + while index < data.count - 1 { + scannedIndex = index + // 0xFF, 0xDA - Start Of Scan + let SOS = ImageFormat.JPEGMarker.SOS.bytes + if data[index] == SOS[0], data[index + 1] == SOS[1] { + lastSOSIndex = index + count += 1 + } + index += 1 + } + + // Found more scans this the previous time + guard count > scannedCount else { return nil } + scannedCount = count + + // `> 1` checks that we've received a first scan (SOS) and then received + // and also received a second scan (SOS). This way we know that we have + // at least one full scan available. + guard count > 1 && lastSOSIndex > 0 else { return nil } + return data[0 ..< lastSOSIndex] + } + + func decode(_ data: Data, + with callbacks: [SessionDataTask.TaskCallback], + completion: @escaping (KFCrossPlatformImage?) -> Void) { + guard data.kf.contains(jpeg: .SOF2) else { + CallbackQueue.mainCurrentOrAsync.execute { completion(nil) } + return + } + + func processing(_ data: Data) { + let processor = ImageDataProcessor( + data: data, + callbacks: callbacks, + processingQueue: processingQueue + ) + processor.onImageProcessed.delegate(on: self) { (self, result) in + guard let image = try? result.0.get() else { + CallbackQueue.mainCurrentOrAsync.execute { completion(nil) } + return + } + + CallbackQueue.mainCurrentOrAsync.execute { completion(image) } + } + processor.process() + } + + // Blur partial images. + let count = scannedCount + + if option.isBlur, count < 6 { + processingQueue.execute { + // Progressively reduce blur as we load more scans. + let image = KingfisherWrapper.image( + data: data, + options: self.creatingOptions + ) + let radius = max(2, 14 - count * 4) + let temp = image?.kf.blurred(withRadius: CGFloat(radius)) + processing(temp?.kf.data(format: .JPEG) ?? data) + } + + } else { + processing(data) + } + } +} + +private final class ImageProgressiveSerialQueue { + typealias ClosureCallback = ((@escaping () -> Void)) -> Void + + private let queue: DispatchQueue + private var items: [DispatchWorkItem] = [] + private var notify: (() -> Void)? + private var lastTime: TimeInterval? + var count: Int { return items.count } + + init() { + self.queue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageProgressive.SerialQueue") + } + + func add(minimum interval: TimeInterval, closure: @escaping ClosureCallback) { + let completion = { [weak self] in + guard let self = self else { return } + + self.queue.async { [weak self] in + guard let self = self else { return } + guard !self.items.isEmpty else { return } + + self.items.removeFirst() + + if let next = self.items.first { + self.queue.asyncAfter( + deadline: .now() + interval, + execute: next + ) + + } else { + self.lastTime = Date().timeIntervalSince1970 + self.notify?() + self.notify = nil + } + } + } + + queue.async { [weak self] in + guard let self = self else { return } + + let item = DispatchWorkItem { + closure(completion) + } + if self.items.isEmpty { + let difference = Date().timeIntervalSince1970 - (self.lastTime ?? 0) + let delay = difference < interval ? interval - difference : 0 + self.queue.asyncAfter(deadline: .now() + delay, execute: item) + } + self.items.append(item) + } + } + + func notify(_ closure: @escaping () -> Void) { + self.notify = closure + } + + func clean() { + queue.async { [weak self] in + guard let self = self else { return } + self.items.forEach { $0.cancel() } + self.items.removeAll() + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageTransition.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageTransition.swift new file mode 100644 index 0000000..4d042df --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageTransition.swift @@ -0,0 +1,118 @@ +// +// ImageTransition.swift +// Kingfisher +// +// Created by Wei Wang on 15/9/18. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +#if os(iOS) || os(tvOS) +import UIKit + +/// Transition effect which will be used when an image downloaded and set by `UIImageView` +/// extension API in Kingfisher. You can assign an enum value with transition duration as +/// an item in `KingfisherOptionsInfo` to enable the animation transition. +/// +/// Apple's UIViewAnimationOptions is used under the hood. +/// For custom transition, you should specified your own transition options, animations and +/// completion handler as well. +/// +/// - none: No animation transition. +/// - fade: Fade in the loaded image in a given duration. +/// - flipFromLeft: Flip from left transition. +/// - flipFromRight: Flip from right transition. +/// - flipFromTop: Flip from top transition. +/// - flipFromBottom: Flip from bottom transition. +/// - custom: Custom transition. +public enum ImageTransition { + /// No animation transition. + case none + /// Fade in the loaded image in a given duration. + case fade(TimeInterval) + /// Flip from left transition. + case flipFromLeft(TimeInterval) + /// Flip from right transition. + case flipFromRight(TimeInterval) + /// Flip from top transition. + case flipFromTop(TimeInterval) + /// Flip from bottom transition. + case flipFromBottom(TimeInterval) + /// Custom transition defined by a general animation block. + /// - duration: The time duration of this custom transition. + /// - options: `UIView.AnimationOptions` should be used in the transition. + /// - animations: The animation block will be applied when setting image. + /// - completion: A block called when the transition animation finishes. + case custom(duration: TimeInterval, + options: UIView.AnimationOptions, + animations: ((UIImageView, UIImage) -> Void)?, + completion: ((Bool) -> Void)?) + + var duration: TimeInterval { + switch self { + case .none: return 0 + case .fade(let duration): return duration + + case .flipFromLeft(let duration): return duration + case .flipFromRight(let duration): return duration + case .flipFromTop(let duration): return duration + case .flipFromBottom(let duration): return duration + + case .custom(let duration, _, _, _): return duration + } + } + + var animationOptions: UIView.AnimationOptions { + switch self { + case .none: return [] + case .fade: return .transitionCrossDissolve + + case .flipFromLeft: return .transitionFlipFromLeft + case .flipFromRight: return .transitionFlipFromRight + case .flipFromTop: return .transitionFlipFromTop + case .flipFromBottom: return .transitionFlipFromBottom + + case .custom(_, let options, _, _): return options + } + } + + var animations: ((UIImageView, UIImage) -> Void)? { + switch self { + case .custom(_, _, let animations, _): return animations + default: return { $0.image = $1 } + } + } + + var completion: ((Bool) -> Void)? { + switch self { + case .custom(_, _, _, let completion): return completion + default: return nil + } + } +} +#else +// Just a placeholder for compiling on macOS. +public enum ImageTransition { + case none + /// This is a placeholder on macOS now. It is for SwiftUI (KFImage) to identify the fade option only. + case fade(TimeInterval) +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/Placeholder.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/Placeholder.swift new file mode 100644 index 0000000..94d9e3a --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/Placeholder.swift @@ -0,0 +1,82 @@ +// +// Placeholder.swift +// Kingfisher +// +// Created by Tieme van Veen on 28/08/2017. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if !os(watchOS) + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif + +#if canImport(UIKit) +import UIKit +#endif + +/// Represents a placeholder type which could be set while loading as well as +/// loading finished without getting an image. +public protocol Placeholder { + + /// How the placeholder should be added to a given image view. + func add(to imageView: KFCrossPlatformImageView) + + /// How the placeholder should be removed from a given image view. + func remove(from imageView: KFCrossPlatformImageView) +} + +/// Default implementation of an image placeholder. The image will be set or +/// reset directly for `image` property of the image view. +extension KFCrossPlatformImage: Placeholder { + /// How the placeholder should be added to a given image view. + public func add(to imageView: KFCrossPlatformImageView) { imageView.image = self } + + /// How the placeholder should be removed from a given image view. + public func remove(from imageView: KFCrossPlatformImageView) { imageView.image = nil } +} + +/// Default implementation of an arbitrary view as placeholder. The view will be +/// added as a subview when adding and be removed from its super view when removing. +/// +/// To use your customize View type as placeholder, simply let it conforming to +/// `Placeholder` by `extension MyView: Placeholder {}`. +extension Placeholder where Self: KFCrossPlatformView { + + /// How the placeholder should be added to a given image view. + public func add(to imageView: KFCrossPlatformImageView) { + imageView.addSubview(self) + translatesAutoresizingMaskIntoConstraints = false + + centerXAnchor.constraint(equalTo: imageView.centerXAnchor).isActive = true + centerYAnchor.constraint(equalTo: imageView.centerYAnchor).isActive = true + heightAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true + widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true + } + + /// How the placeholder should be removed from a given image view. + public func remove(from imageView: KFCrossPlatformImageView) { + removeFromSuperview() + } +} + +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift new file mode 100644 index 0000000..d3b3ea1 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift @@ -0,0 +1,94 @@ +// +// AuthenticationChallengeResponsable.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +@available(*, deprecated, message: "Typo. Use `AuthenticationChallengeResponsible` instead", renamed: "AuthenticationChallengeResponsible") +public typealias AuthenticationChallengeResponsable = AuthenticationChallengeResponsible + +/// Protocol indicates that an authentication challenge could be handled. +public protocol AuthenticationChallengeResponsible: AnyObject { + + /// Called when a session level authentication challenge is received. + /// This method provide a chance to handle and response to the authentication + /// challenge before downloading could start. + /// + /// - Parameters: + /// - downloader: The downloader which receives this challenge. + /// - challenge: An object that contains the request for authentication. + /// - completionHandler: A handler that your delegate method must call. + /// + /// - Note: This method is a forward from `URLSessionDelegate.urlSession(:didReceiveChallenge:completionHandler:)`. + /// Please refer to the document of it in `URLSessionDelegate`. + func downloader( + _ downloader: ImageDownloader, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + + /// Called when a task level authentication challenge is received. + /// This method provide a chance to handle and response to the authentication + /// challenge before downloading could start. + /// + /// - Parameters: + /// - downloader: The downloader which receives this challenge. + /// - task: The task whose request requires authentication. + /// - challenge: An object that contains the request for authentication. + /// - completionHandler: A handler that your delegate method must call. + func downloader( + _ downloader: ImageDownloader, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) +} + +extension AuthenticationChallengeResponsible { + + public func downloader( + _ downloader: ImageDownloader, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) { + let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!) + completionHandler(.useCredential, credential) + return + } + } + + completionHandler(.performDefaultHandling, nil) + } + + public func downloader( + _ downloader: ImageDownloader, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + completionHandler(.performDefaultHandling, nil) + } + +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift new file mode 100644 index 0000000..b368972 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift @@ -0,0 +1,74 @@ +// +// ImageDataProcessor.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +private let sharedProcessingQueue: CallbackQueue = + .dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process")) + +// Handles image processing work on an own process queue. +class ImageDataProcessor { + let data: Data + let callbacks: [SessionDataTask.TaskCallback] + let queue: CallbackQueue + + // Note: We have an optimization choice there, to reduce queue dispatch by checking callback + // queue settings in each option... + let onImageProcessed = Delegate<(Result, SessionDataTask.TaskCallback), Void>() + + init(data: Data, callbacks: [SessionDataTask.TaskCallback], processingQueue: CallbackQueue?) { + self.data = data + self.callbacks = callbacks + self.queue = processingQueue ?? sharedProcessingQueue + } + + func process() { + queue.execute(doProcess) + } + + private func doProcess() { + var processedImages = [String: KFCrossPlatformImage]() + for callback in callbacks { + let processor = callback.options.processor + var image = processedImages[processor.identifier] + if image == nil { + image = processor.process(item: .data(data), options: callback.options) + processedImages[processor.identifier] = image + } + + let result: Result + if let image = image { + let finalImage = callback.options.backgroundDecode ? image.kf.decoded : image + result = .success(finalImage) + } else { + let error = KingfisherError.processorError( + reason: .processingFailed(processor: processor, item: .data(data))) + result = .failure(error) + } + onImageProcessed.call((result, callback)) + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift new file mode 100644 index 0000000..d50ed9a --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift @@ -0,0 +1,488 @@ +// +// ImageDownloader.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +typealias DownloadResult = Result + +/// Represents a success result of an image downloading progress. +public struct ImageLoadingResult { + + /// The downloaded image. + public let image: KFCrossPlatformImage + + /// Original URL of the image request. + public let url: URL? + + /// The raw data received from downloader. + public let originalData: Data +} + +/// Represents a task of an image downloading process. +public struct DownloadTask { + + /// The `SessionDataTask` object bounded to this download task. Multiple `DownloadTask`s could refer + /// to a same `sessionTask`. This is an optimization in Kingfisher to prevent multiple downloading task + /// for the same URL resource at the same time. + /// + /// When you `cancel` a `DownloadTask`, this `SessionDataTask` and its cancel token will be pass through. + /// You can use them to identify the cancelled task. + public let sessionTask: SessionDataTask + + /// The cancel token which is used to cancel the task. This is only for identify the task when it is cancelled. + /// To cancel a `DownloadTask`, use `cancel` instead. + public let cancelToken: SessionDataTask.CancelToken + + /// Cancel this task if it is running. It will do nothing if this task is not running. + /// + /// - Note: + /// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is being + /// downloading. However, even when internally no new session task created, a `DownloadTask` will be still created + /// and returned when you call related methods, but it will share the session downloading task with a previous task. + /// In this case, if multiple `DownloadTask`s share a single session download task, cancelling a `DownloadTask` + /// does not affect other `DownloadTask`s. + /// + /// If you need to cancel all `DownloadTask`s of a url, use `ImageDownloader.cancel(url:)`. If you need to cancel + /// all downloading tasks of an `ImageDownloader`, use `ImageDownloader.cancelAll()`. + public func cancel() { + sessionTask.cancel(token: cancelToken) + } +} + +extension DownloadTask { + enum WrappedTask { + case download(DownloadTask) + case dataProviding + + func cancel() { + switch self { + case .download(let task): task.cancel() + case .dataProviding: break + } + } + + var value: DownloadTask? { + switch self { + case .download(let task): return task + case .dataProviding: return nil + } + } + } +} + +/// Represents a downloading manager for requesting the image with a URL from server. +open class ImageDownloader { + + // MARK: Singleton + /// The default downloader. + public static let `default` = ImageDownloader(name: "default") + + // MARK: Public Properties + /// The duration before the downloading is timeout. Default is 15 seconds. + open var downloadTimeout: TimeInterval = 15.0 + + /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this + /// set will be ignored. You can use this set to specify the self-signed site. It only will be used if you don't + /// specify the `authenticationChallengeResponder`. + /// + /// If `authenticationChallengeResponder` is set, this property will be ignored and the implementation of + /// `authenticationChallengeResponder` will be used instead. + open var trustedHosts: Set? + + /// Use this to set supply a configuration for the downloader. By default, + /// NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used. + /// + /// You could change the configuration before a downloading task starts. + /// A configuration without persistent storage for caches is requested for downloader working correctly. + open var sessionConfiguration = URLSessionConfiguration.ephemeral { + didSet { + session.invalidateAndCancel() + session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) + } + } + open var sessionDelegate: SessionDelegate { + didSet { + session.invalidateAndCancel() + session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) + setupSessionHandler() + } + } + + /// Whether the download requests should use pipeline or not. Default is false. + open var requestsUsePipelining = false + + /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more. + open weak var delegate: ImageDownloaderDelegate? + + /// A responder for authentication challenge. + /// Downloader will forward the received authentication challenge for the downloading session to this responder. + open weak var authenticationChallengeResponder: AuthenticationChallengeResponsible? + + private let name: String + private var session: URLSession + + // MARK: Initializers + + /// Creates a downloader with name. + /// + /// - Parameter name: The name for the downloader. It should not be empty. + public init(name: String) { + if name.isEmpty { + fatalError("[Kingfisher] You should specify a name for the downloader. " + + "A downloader with empty name is not permitted.") + } + + self.name = name + + sessionDelegate = SessionDelegate() + session = URLSession( + configuration: sessionConfiguration, + delegate: sessionDelegate, + delegateQueue: nil) + + authenticationChallengeResponder = self + setupSessionHandler() + } + + deinit { session.invalidateAndCancel() } + + private func setupSessionHandler() { + sessionDelegate.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in + self.authenticationChallengeResponder?.downloader(self, didReceive: invoke.1, completionHandler: invoke.2) + } + sessionDelegate.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in + self.authenticationChallengeResponder?.downloader( + self, task: invoke.1, didReceive: invoke.2, completionHandler: invoke.3) + } + sessionDelegate.onValidStatusCode.delegate(on: self) { (self, code) in + return (self.delegate ?? self).isValidStatusCode(code, for: self) + } + sessionDelegate.onDownloadingFinished.delegate(on: self) { (self, value) in + let (url, result) = value + do { + let value = try result.get() + self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: value, error: nil) + } catch { + self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: nil, error: error) + } + } + sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in + return (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, with: task) + } + } + + // Wraps `completionHandler` to `onCompleted` respectively. + private func createCompletionCallBack(_ completionHandler: ((DownloadResult) -> Void)?) -> Delegate? { + return completionHandler.map { block -> Delegate in + + let delegate = Delegate, Void>() + delegate.delegate(on: self) { (self, callback) in + block(callback) + } + return delegate + } + } + + private func createTaskCallback( + _ completionHandler: ((DownloadResult) -> Void)?, + options: KingfisherParsedOptionsInfo + ) -> SessionDataTask.TaskCallback + { + return SessionDataTask.TaskCallback( + onCompleted: createCompletionCallBack(completionHandler), + options: options + ) + } + + private func createDownloadContext( + with url: URL, + options: KingfisherParsedOptionsInfo, + done: @escaping ((Result) -> Void) + ) + { + func checkRequestAndDone(r: URLRequest) { + + // There is a possibility that request modifier changed the url to `nil` or empty. + // In this case, throw an error. + guard let url = r.url, !url.absoluteString.isEmpty else { + done(.failure(KingfisherError.requestError(reason: .invalidURL(request: r)))) + return + } + + done(.success(DownloadingContext(url: url, request: r, options: options))) + } + + // Creates default request. + var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout) + request.httpShouldUsePipelining = requestsUsePipelining + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) , options.lowDataModeSource != nil { + request.allowsConstrainedNetworkAccess = false + } + + if let requestModifier = options.requestModifier { + // Modifies request before sending. + requestModifier.modified(for: request) { result in + guard let finalRequest = result else { + done(.failure(KingfisherError.requestError(reason: .emptyRequest))) + return + } + checkRequestAndDone(r: finalRequest) + } + } else { + checkRequestAndDone(r: request) + } + } + + private func addDownloadTask( + context: DownloadingContext, + callback: SessionDataTask.TaskCallback + ) -> DownloadTask + { + // Ready to start download. Add it to session task manager (`sessionHandler`) + let downloadTask: DownloadTask + if let existingTask = sessionDelegate.task(for: context.url) { + downloadTask = sessionDelegate.append(existingTask, url: context.url, callback: callback) + } else { + let sessionDataTask = session.dataTask(with: context.request) + sessionDataTask.priority = context.options.downloadPriority + downloadTask = sessionDelegate.add(sessionDataTask, url: context.url, callback: callback) + } + return downloadTask + } + + + private func reportWillDownloadImage(url: URL, request: URLRequest) { + delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request) + } + + private func reportDidDownloadImageData(result: Result<(Data, URLResponse?), KingfisherError>, url: URL) { + var response: URLResponse? + var err: Error? + do { + response = try result.get().1 + } catch { + err = error + } + self.delegate?.imageDownloader( + self, + didFinishDownloadingImageForURL: url, + with: response, + error: err + ) + } + + private func reportDidProcessImage( + result: Result, url: URL, response: URLResponse? + ) + { + if let image = try? result.get() { + self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response) + } + + } + + private func startDownloadTask( + context: DownloadingContext, + callback: SessionDataTask.TaskCallback + ) -> DownloadTask + { + + let downloadTask = addDownloadTask(context: context, callback: callback) + + let sessionTask = downloadTask.sessionTask + guard !sessionTask.started else { + return downloadTask + } + + sessionTask.onTaskDone.delegate(on: self) { (self, done) in + // Underlying downloading finishes. + // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback] + let (result, callbacks) = done + + // Before processing the downloaded data. + self.reportDidDownloadImageData(result: result, url: context.url) + + switch result { + // Download finished. Now process the data to an image. + case .success(let (data, response)): + let processor = ImageDataProcessor( + data: data, callbacks: callbacks, processingQueue: context.options.processingQueue + ) + processor.onImageProcessed.delegate(on: self) { (self, done) in + // `onImageProcessed` will be called for `callbacks.count` times, with each + // `SessionDataTask.TaskCallback` as the input parameter. + // result: Result, callback: SessionDataTask.TaskCallback + let (result, callback) = done + + self.reportDidProcessImage(result: result, url: context.url, response: response) + + let imageResult = result.map { ImageLoadingResult(image: $0, url: context.url, originalData: data) } + let queue = callback.options.callbackQueue + queue.execute { callback.onCompleted?.call(imageResult) } + } + processor.process() + + case .failure(let error): + callbacks.forEach { callback in + let queue = callback.options.callbackQueue + queue.execute { callback.onCompleted?.call(.failure(error)) } + } + } + } + + reportWillDownloadImage(url: context.url, request: context.request) + sessionTask.resume() + return downloadTask + } + + // MARK: Downloading Task + /// Downloads an image with a URL and option. Invoked internally by Kingfisher. Subclasses must invoke super. + /// + /// - Parameters: + /// - url: Target URL. + /// - options: The options could control download behavior. See `KingfisherOptionsInfo`. + /// - completionHandler: Called when the download progress finishes. This block will be called in the queue + /// defined in `.callbackQueue` in `options` parameter. + /// - Returns: A downloading task. You could call `cancel` on it to stop the download task. + @discardableResult + open func downloadImage( + with url: URL, + options: KingfisherParsedOptionsInfo, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var downloadTask: DownloadTask? + createDownloadContext(with: url, options: options) { result in + switch result { + case .success(let context): + // `downloadTask` will be set if the downloading started immediately. This is the case when no request + // modifier or a sync modifier (`ImageDownloadRequestModifier`) is used. Otherwise, when an + // `AsyncImageDownloadRequestModifier` is used the returned `downloadTask` of this method will be `nil` + // and the actual "delayed" task is given in `AsyncImageDownloadRequestModifier.onDownloadTaskStarted` + // callback. + downloadTask = self.startDownloadTask( + context: context, + callback: self.createTaskCallback(completionHandler, options: options) + ) + if let modifier = options.requestModifier { + modifier.onDownloadTaskStarted?(downloadTask) + } + case .failure(let error): + options.callbackQueue.execute { + completionHandler?(.failure(error)) + } + } + } + + return downloadTask + } + + /// Downloads an image with a URL and option. + /// + /// - Parameters: + /// - url: Target URL. + /// - options: The options could control download behavior. See `KingfisherOptionsInfo`. + /// - progressBlock: Called when the download progress updated. This block will be always be called in main queue. + /// - completionHandler: Called when the download progress finishes. This block will be called in the queue + /// defined in `.callbackQueue` in `options` parameter. + /// - Returns: A downloading task. You could call `cancel` on it to stop the download task. + @discardableResult + open func downloadImage( + with url: URL, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var info = KingfisherParsedOptionsInfo(options) + if let block = progressBlock { + info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + return downloadImage( + with: url, + options: info, + completionHandler: completionHandler) + } + + /// Downloads an image with a URL and option. + /// + /// - Parameters: + /// - url: Target URL. + /// - options: The options could control download behavior. See `KingfisherOptionsInfo`. + /// - completionHandler: Called when the download progress finishes. This block will be called in the queue + /// defined in `.callbackQueue` in `options` parameter. + /// - Returns: A downloading task. You could call `cancel` on it to stop the download task. + @discardableResult + open func downloadImage( + with url: URL, + options: KingfisherOptionsInfo? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + downloadImage( + with: url, + options: KingfisherParsedOptionsInfo(options), + completionHandler: completionHandler + ) + } +} + +// MARK: Cancelling Task +extension ImageDownloader { + + /// Cancel all downloading tasks for this `ImageDownloader`. It will trigger the completion handlers + /// for all not-yet-finished downloading tasks. + /// + /// If you need to only cancel a certain task, call `cancel()` on the `DownloadTask` + /// returned by the downloading methods. If you need to cancel all `DownloadTask`s of a certain url, + /// use `ImageDownloader.cancel(url:)`. + public func cancelAll() { + sessionDelegate.cancelAll() + } + + /// Cancel all downloading tasks for a given URL. It will trigger the completion handlers for + /// all not-yet-finished downloading tasks for the URL. + /// + /// - Parameter url: The URL which you want to cancel downloading. + public func cancel(url: URL) { + sessionDelegate.cancel(url: url) + } +} + +// Use the default implementation from extension of `AuthenticationChallengeResponsible`. +extension ImageDownloader: AuthenticationChallengeResponsible {} + +// Use the default implementation from extension of `ImageDownloaderDelegate`. +extension ImageDownloader: ImageDownloaderDelegate {} + +extension ImageDownloader { + struct DownloadingContext { + let url: URL + let request: URLRequest + let options: KingfisherParsedOptionsInfo + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift new file mode 100644 index 0000000..7f98640 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift @@ -0,0 +1,154 @@ +// +// ImageDownloaderDelegate.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Protocol of `ImageDownloader`. This protocol provides a set of methods which are related to image downloader +/// working stages and rules. +public protocol ImageDownloaderDelegate: AnyObject { + + /// Called when the `ImageDownloader` object will start downloading an image from a specified URL. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object which is used for the downloading operation. + /// - url: URL of the starting request. + /// - request: The request object for the download process. + /// + func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?) + + /// Called when the `ImageDownloader` completes a downloading request with success or failure. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object which is used for the downloading operation. + /// - url: URL of the original request URL. + /// - response: The response object of the downloading process. + /// - error: The error in case of failure. + /// + func imageDownloader( + _ downloader: ImageDownloader, + didFinishDownloadingImageForURL url: URL, + with response: URLResponse?, + error: Error?) + + /// Called when the `ImageDownloader` object successfully downloaded image data from specified URL. This is + /// your last chance to verify or modify the downloaded data before Kingfisher tries to perform addition + /// processing on the image data. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object which is used for the downloading operation. + /// - data: The original downloaded data. + /// - dataTask: The data task contains request and response information of the download. + /// - Note: + /// This can be used to pre-process raw image data before creation of `Image` instance (i.e. + /// decrypting or verification). If `nil` returned, the processing is interrupted and a `KingfisherError` with + /// `ResponseErrorReason.dataModifyingFailed` will be raised. You could use this fact to stop the image + /// processing flow if you find the data is corrupted or malformed. + /// + /// If this method is implemented, `imageDownloader(_:didDownload:for:)` will not be called anymore. + func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with dataTask: SessionDataTask) -> Data? + + /// Called when the `ImageDownloader` object successfully downloaded image data from specified URL. This is + /// your last chance to verify or modify the downloaded data before Kingfisher tries to perform addition + /// processing on the image data. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object which is used for the downloading operation. + /// - data: The original downloaded data. + /// - url: The URL of the original request URL. + /// - Returns: The data from which Kingfisher should use to create an image. You need to provide valid data + /// which content is one of the supported image file format. Kingfisher will perform process on this + /// data and try to convert it to an image object. + /// - Note: + /// This can be used to pre-process raw image data before creation of `Image` instance (i.e. + /// decrypting or verification). If `nil` returned, the processing is interrupted and a `KingfisherError` with + /// `ResponseErrorReason.dataModifyingFailed` will be raised. You could use this fact to stop the image + /// processing flow if you find the data is corrupted or malformed. + /// + /// If `imageDownloader(_:didDownload:with:)` is implemented, this method will not be called anymore. + func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? + + /// Called when the `ImageDownloader` object successfully downloads and processes an image from specified URL. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object which is used for the downloading operation. + /// - image: The downloaded and processed image. + /// - url: URL of the original request URL. + /// - response: The original response object of the downloading process. + /// + func imageDownloader( + _ downloader: ImageDownloader, + didDownload image: KFCrossPlatformImage, + for url: URL, + with response: URLResponse?) + + /// Checks if a received HTTP status code is valid or not. + /// By default, a status code in range 200..<400 is considered as valid. + /// If an invalid code is received, the downloader will raise an `KingfisherError` with + /// `ResponseErrorReason.invalidHTTPStatusCode` as its reason. + /// + /// - Parameters: + /// - code: The received HTTP status code. + /// - downloader: The `ImageDownloader` object asks for validate status code. + /// - Returns: Returns a value to indicate whether this HTTP status code is valid or not. + /// - Note: If the default 200 to 400 valid code does not suit your need, + /// you can implement this method to change that behavior. + func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool +} + +// Default implementation for `ImageDownloaderDelegate`. +extension ImageDownloaderDelegate { + public func imageDownloader( + _ downloader: ImageDownloader, + willDownloadImageForURL url: URL, + with request: URLRequest?) {} + + public func imageDownloader( + _ downloader: ImageDownloader, + didFinishDownloadingImageForURL url: URL, + with response: URLResponse?, + error: Error?) {} + + public func imageDownloader( + _ downloader: ImageDownloader, + didDownload image: KFCrossPlatformImage, + for url: URL, + with response: URLResponse?) {} + + public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool { + return (200..<400).contains(code) + } + + public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with task: SessionDataTask) -> Data? { + guard let url = task.originalURL else { + return data + } + return imageDownloader(downloader, didDownload: data, for: url) + } + + public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? { + return data + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageModifier.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageModifier.swift new file mode 100644 index 0000000..0acd0e8 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageModifier.swift @@ -0,0 +1,116 @@ +// +// ImageModifier.swift +// Kingfisher +// +// Created by Ethan Gill on 2017/11/28. +// +// Copyright (c) 2019 Ethan Gill +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// An `ImageModifier` can be used to change properties on an image between cache serialization and the actual use of +/// the image. The `modify(_:)` method will be called after the image retrieved from its source and before it returned +/// to the caller. This modified image is expected to be only used for rendering purpose, any changes applied by the +/// `ImageModifier` will not be serialized or cached. +public protocol ImageModifier { + /// Modify an input `Image`. + /// + /// - parameter image: Image which will be modified by `self` + /// + /// - returns: The modified image. + /// + /// - Note: The return value will be unmodified if modifying is not possible on + /// the current platform. + /// - Note: Most modifiers support UIImage or NSImage, but not CGImage. + func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage +} + +/// A wrapper for creating an `ImageModifier` easier. +/// This type conforms to `ImageModifier` and wraps an image modify block. +/// If the `block` throws an error, the original image will be used. +public struct AnyImageModifier: ImageModifier { + + /// A block which modifies images, or returns the original image + /// if modification cannot be performed with an error. + let block: (KFCrossPlatformImage) throws -> KFCrossPlatformImage + + /// Creates an `AnyImageModifier` with a given `modify` block. + public init(modify: @escaping (KFCrossPlatformImage) throws -> KFCrossPlatformImage) { + block = modify + } + + /// Modify an input `Image`. See `ImageModifier` protocol for more. + public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { + return (try? block(image)) ?? image + } +} + +#if os(iOS) || os(tvOS) || os(watchOS) +import UIKit + +/// Modifier for setting the rendering mode of images. +public struct RenderingModeImageModifier: ImageModifier { + + /// The rendering mode to apply to the image. + public let renderingMode: UIImage.RenderingMode + + /// Creates a `RenderingModeImageModifier`. + /// + /// - Parameter renderingMode: The rendering mode to apply to the image. Default is `.automatic`. + public init(renderingMode: UIImage.RenderingMode = .automatic) { + self.renderingMode = renderingMode + } + + /// Modify an input `Image`. See `ImageModifier` protocol for more. + public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { + return image.withRenderingMode(renderingMode) + } +} + +/// Modifier for setting the `flipsForRightToLeftLayoutDirection` property of images. +public struct FlipsForRightToLeftLayoutDirectionImageModifier: ImageModifier { + + /// Creates a `FlipsForRightToLeftLayoutDirectionImageModifier`. + public init() {} + + /// Modify an input `Image`. See `ImageModifier` protocol for more. + public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { + return image.imageFlippedForRightToLeftLayoutDirection() + } +} + +/// Modifier for setting the `alignmentRectInsets` property of images. +public struct AlignmentRectInsetsImageModifier: ImageModifier { + + /// The alignment insets to apply to the image + public let alignmentInsets: UIEdgeInsets + + /// Creates an `AlignmentRectInsetsImageModifier`. + public init(alignmentInsets: UIEdgeInsets) { + self.alignmentInsets = alignmentInsets + } + + /// Modify an input `Image`. See `ImageModifier` protocol for more. + public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { + return image.withAlignmentRectInsets(alignmentInsets) + } +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift new file mode 100644 index 0000000..3fce14c --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift @@ -0,0 +1,442 @@ +// +// ImagePrefetcher.swift +// Kingfisher +// +// Created by Claire Knight on 24/02/2016 +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// Progress update block of prefetcher when initialized with a list of resources. +/// +/// - `skippedResources`: An array of resources that are already cached before the prefetching starting. +/// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while +/// downloading, encountered an error when downloading or the download not being started at all. +/// - `completedResources`: An array of resources that are downloaded and cached successfully. +public typealias PrefetcherProgressBlock = + ((_ skippedResources: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> Void) + +/// Progress update block of prefetcher when initialized with a list of resources. +/// +/// - `skippedSources`: An array of sources that are already cached before the prefetching starting. +/// - `failedSources`: An array of sources that fail to be fetched. +/// - `completedResources`: An array of sources that are fetched and cached successfully. +public typealias PrefetcherSourceProgressBlock = + ((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void) + +/// Completion block of prefetcher when initialized with a list of sources. +/// +/// - `skippedResources`: An array of resources that are already cached before the prefetching starting. +/// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while +/// downloading, encountered an error when downloading or the download not being started at all. +/// - `completedResources`: An array of resources that are downloaded and cached successfully. +public typealias PrefetcherCompletionHandler = + ((_ skippedResources: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> Void) + +/// Completion block of prefetcher when initialized with a list of sources. +/// +/// - `skippedSources`: An array of sources that are already cached before the prefetching starting. +/// - `failedSources`: An array of sources that fail to be fetched. +/// - `completedSources`: An array of sources that are fetched and cached successfully. +public typealias PrefetcherSourceCompletionHandler = + ((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void) + +/// `ImagePrefetcher` represents a downloading manager for requesting many images via URLs, then caching them. +/// This is useful when you know a list of image resources and want to download them before showing. It also works with +/// some Cocoa prefetching mechanism like table view or collection view `prefetchDataSource`, to start image downloading +/// and caching before they display on screen. +public class ImagePrefetcher: CustomStringConvertible { + + public var description: String { + return "\(Unmanaged.passUnretained(self).toOpaque())" + } + + /// The maximum concurrent downloads to use when prefetching images. Default is 5. + public var maxConcurrentDownloads = 5 + + private let prefetchSources: [Source] + private let optionsInfo: KingfisherParsedOptionsInfo + + private var progressBlock: PrefetcherProgressBlock? + private var completionHandler: PrefetcherCompletionHandler? + + private var progressSourceBlock: PrefetcherSourceProgressBlock? + private var completionSourceHandler: PrefetcherSourceCompletionHandler? + + private var tasks = [String: DownloadTask.WrappedTask]() + + private var pendingSources: ArraySlice + private var skippedSources = [Source]() + private var completedSources = [Source]() + private var failedSources = [Source]() + + private var stopped = false + + // A manager used for prefetching. We will use the helper methods in manager. + private let manager: KingfisherManager + + private let pretchQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.pretchQueue") + private static let requestingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.requestingQueue") + + private var finished: Bool { + let totalFinished: Int = failedSources.count + skippedSources.count + completedSources.count + return totalFinished == prefetchSources.count && tasks.isEmpty + } + + /// Creates an image prefetcher with an array of URLs. + /// + /// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable. + /// After you get a valid `ImagePrefetcher` object, you call `start()` on it to begin the prefetching process. + /// The images which are already cached will be skipped without downloading again. + /// + /// - Parameters: + /// - urls: The URLs which should be prefetched. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called every time an resource is downloaded, skipped or cancelled. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init( + urls: [URL], + options: KingfisherOptionsInfo? = nil, + progressBlock: PrefetcherProgressBlock? = nil, + completionHandler: PrefetcherCompletionHandler? = nil) + { + let resources: [Resource] = urls.map { $0 } + self.init( + resources: resources, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + /// Creates an image prefetcher with an array of URLs. + /// + /// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable. + /// After you get a valid `ImagePrefetcher` object, you call `start()` on it to begin the prefetching process. + /// The images which are already cached will be skipped without downloading again. + /// + /// - Parameters: + /// - urls: The URLs which should be prefetched. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init( + urls: [URL], + options: KingfisherOptionsInfo? = nil, + completionHandler: PrefetcherCompletionHandler? = nil) + { + let resources: [Resource] = urls.map { $0 } + self.init( + resources: resources, + options: options, + progressBlock: nil, + completionHandler: completionHandler) + } + + /// Creates an image prefetcher with an array of resources. + /// + /// - Parameters: + /// - resources: The resources which should be prefetched. See `Resource` type for more. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called every time an resource is downloaded, skipped or cancelled. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init( + resources: [Resource], + options: KingfisherOptionsInfo? = nil, + progressBlock: PrefetcherProgressBlock? = nil, + completionHandler: PrefetcherCompletionHandler? = nil) + { + self.init(sources: resources.map { $0.convertToSource() }, options: options) + self.progressBlock = progressBlock + self.completionHandler = completionHandler + } + + /// Creates an image prefetcher with an array of resources. + /// + /// - Parameters: + /// - resources: The resources which should be prefetched. See `Resource` type for more. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init( + resources: [Resource], + options: KingfisherOptionsInfo? = nil, + completionHandler: PrefetcherCompletionHandler? = nil) + { + self.init(sources: resources.map { $0.convertToSource() }, options: options) + self.completionHandler = completionHandler + } + + /// Creates an image prefetcher with an array of sources. + /// + /// - Parameters: + /// - sources: The sources which should be prefetched. See `Source` type for more. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called every time an source fetching successes, fails, is skipped. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init(sources: [Source], + options: KingfisherOptionsInfo? = nil, + progressBlock: PrefetcherSourceProgressBlock? = nil, + completionHandler: PrefetcherSourceCompletionHandler? = nil) + { + self.init(sources: sources, options: options) + self.progressSourceBlock = progressBlock + self.completionSourceHandler = completionHandler + } + + /// Creates an image prefetcher with an array of sources. + /// + /// - Parameters: + /// - sources: The sources which should be prefetched. See `Source` type for more. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init(sources: [Source], + options: KingfisherOptionsInfo? = nil, + completionHandler: PrefetcherSourceCompletionHandler? = nil) + { + self.init(sources: sources, options: options) + self.completionSourceHandler = completionHandler + } + + init(sources: [Source], options: KingfisherOptionsInfo?) { + var options = KingfisherParsedOptionsInfo(options) + prefetchSources = sources + pendingSources = ArraySlice(sources) + + // We want all callbacks from our prefetch queue, so we should ignore the callback queue in options. + // Add our own callback dispatch queue to make sure all internal callbacks are + // coming back in our expected queue. + options.callbackQueue = .dispatch(pretchQueue) + optionsInfo = options + + let cache = optionsInfo.targetCache ?? .default + let downloader = optionsInfo.downloader ?? .default + manager = KingfisherManager(downloader: downloader, cache: cache) + } + + /// Starts to download the resources and cache them. This can be useful for background downloading + /// of assets that are required for later use in an app. This code will not try and update any UI + /// with the results of the process. + public func start() { + pretchQueue.async { + guard !self.stopped else { + assertionFailure("You can not restart the same prefetcher. Try to create a new prefetcher.") + self.handleComplete() + return + } + + guard self.maxConcurrentDownloads > 0 else { + assertionFailure("There should be concurrent downloads value should be at least 1.") + self.handleComplete() + return + } + + // Empty case. + guard self.prefetchSources.count > 0 else { + self.handleComplete() + return + } + + let initialConcurrentDownloads = min(self.prefetchSources.count, self.maxConcurrentDownloads) + for _ in 0 ..< initialConcurrentDownloads { + if let resource = self.pendingSources.popFirst() { + self.startPrefetching(resource) + } + } + } + } + + /// Stops current downloading progress, and cancel any future prefetching activity that might be occuring. + public func stop() { + pretchQueue.async { + if self.finished { return } + self.stopped = true + self.tasks.values.forEach { $0.cancel() } + } + } + + private func downloadAndCache(_ source: Source) { + + let downloadTaskCompletionHandler: ((Result) -> Void) = { result in + self.tasks.removeValue(forKey: source.cacheKey) + do { + let _ = try result.get() + self.completedSources.append(source) + } catch { + self.failedSources.append(source) + } + + self.reportProgress() + if self.stopped { + if self.tasks.isEmpty { + self.failedSources.append(contentsOf: self.pendingSources) + self.handleComplete() + } + } else { + self.reportCompletionOrStartNext() + } + } + + var downloadTask: DownloadTask.WrappedTask? + ImagePrefetcher.requestingQueue.sync { + let context = RetrievingContext( + options: optionsInfo, originalSource: source + ) + downloadTask = manager.loadAndCacheImage( + source: source, + context: context, + completionHandler: downloadTaskCompletionHandler) + } + + if let downloadTask = downloadTask { + tasks[source.cacheKey] = downloadTask + } + } + + private func append(cached source: Source) { + skippedSources.append(source) + + reportProgress() + reportCompletionOrStartNext() + } + + private func startPrefetching(_ source: Source) + { + if optionsInfo.forceRefresh { + downloadAndCache(source) + return + } + + let cacheType = manager.cache.imageCachedType( + forKey: source.cacheKey, + processorIdentifier: optionsInfo.processor.identifier) + switch cacheType { + case .memory: + append(cached: source) + case .disk: + if optionsInfo.alsoPrefetchToMemory { + let context = RetrievingContext(options: optionsInfo, originalSource: source) + _ = manager.retrieveImageFromCache( + source: source, + context: context) + { + _ in + self.append(cached: source) + } + } else { + append(cached: source) + } + case .none: + downloadAndCache(source) + } + } + + private func reportProgress() { + + if progressBlock == nil && progressSourceBlock == nil { + return + } + + let skipped = self.skippedSources + let failed = self.failedSources + let completed = self.completedSources + CallbackQueue.mainCurrentOrAsync.execute { + self.progressSourceBlock?(skipped, failed, completed) + self.progressBlock?( + skipped.compactMap { $0.asResource }, + failed.compactMap { $0.asResource }, + completed.compactMap { $0.asResource } + ) + } + } + + private func reportCompletionOrStartNext() { + if let resource = self.pendingSources.popFirst() { + // Loose call stack for huge ammount of sources. + pretchQueue.async { self.startPrefetching(resource) } + } else { + guard allFinished else { return } + self.handleComplete() + } + } + + var allFinished: Bool { + return skippedSources.count + failedSources.count + completedSources.count == prefetchSources.count + } + + private func handleComplete() { + + if completionHandler == nil && completionSourceHandler == nil { + return + } + + // The completion handler should be called on the main thread + CallbackQueue.mainCurrentOrAsync.execute { + self.completionSourceHandler?(self.skippedSources, self.failedSources, self.completedSources) + self.completionHandler?( + self.skippedSources.compactMap { $0.asResource }, + self.failedSources.compactMap { $0.asResource }, + self.completedSources.compactMap { $0.asResource } + ) + self.completionHandler = nil + self.progressBlock = nil + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift new file mode 100644 index 0000000..0d13cbe --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift @@ -0,0 +1,76 @@ +// +// RedirectHandler.swift +// Kingfisher +// +// Created by Roman Maidanovych on 2018/12/10. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents and wraps a method for modifying request during an image download request redirection. +public protocol ImageDownloadRedirectHandler { + + /// The `ImageDownloadRedirectHandler` contained will be used to change the request before redirection. + /// This is the posibility you can modify the image download request during redirection. You can modify the + /// request for some customizing purpose, such as adding auth token to the header, do basic HTTP auth or + /// something like url mapping. + /// + /// Usually, you pass an `ImageDownloadRedirectHandler` as the associated value of + /// `KingfisherOptionsInfoItem.redirectHandler` and use it as the `options` parameter in related methods. + /// + /// If you do nothing with the input `request` and return it as is, a downloading process will redirect with it. + /// + /// - Parameters: + /// - task: The current `SessionDataTask` which triggers this redirect. + /// - response: The response received during redirection. + /// - newRequest: The request for redirection which can be modified. + /// - completionHandler: A closure for being called with modified request. + func handleHTTPRedirection( + for task: SessionDataTask, + response: HTTPURLResponse, + newRequest: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) +} + +/// A wrapper for creating an `ImageDownloadRedirectHandler` easier. +/// This type conforms to `ImageDownloadRedirectHandler` and wraps a redirect request modify block. +public struct AnyRedirectHandler: ImageDownloadRedirectHandler { + + let block: (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void + + public func handleHTTPRedirection( + for task: SessionDataTask, + response: HTTPURLResponse, + newRequest: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) + { + block(task, response, newRequest, completionHandler) + } + + /// Creates a value of `ImageDownloadRedirectHandler` which runs `modify` block. + /// + /// - Parameter modify: The request modifying block runs when a request modifying task comes. + /// + public init(handle: @escaping (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void) { + block = handle + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/RequestModifier.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/RequestModifier.swift new file mode 100644 index 0000000..a1d972d --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/RequestModifier.swift @@ -0,0 +1,108 @@ +// +// RequestModifier.swift +// Kingfisher +// +// Created by Wei Wang on 2016/09/05. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents and wraps a method for modifying request before an image download request starts in an asynchronous way. +public protocol AsyncImageDownloadRequestModifier { + + /// This method will be called just before the `request` being sent. + /// This is the last chance you can modify the image download request. You can modify the request for some + /// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping. + /// When you have done with the modification, call the `reportModified` block with the modified request and the data + /// download will happen with this request. + /// + /// Usually, you pass an `AsyncImageDownloadRequestModifier` as the associated value of + /// `KingfisherOptionsInfoItem.requestModifier` and use it as the `options` parameter in related methods. + /// + /// If you do nothing with the input `request` and return it as is, a downloading process will start with it. + /// + /// - Parameters: + /// - request: The input request contains necessary information like `url`. This request is generated + /// according to your resource url as a GET request. + /// - reportModified: The callback block you need to call after the asynchronous modifying done. + /// + func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void) + + /// A block will be called when the download task started. + /// + /// If an `AsyncImageDownloadRequestModifier` and the asynchronous modification happens before the download, the + /// related download method will not return a valid `DownloadTask` value. Instead, you can get one from this method. + var onDownloadTaskStarted: ((DownloadTask?) -> Void)? { get } +} + +/// Represents and wraps a method for modifying request before an image download request starts. +public protocol ImageDownloadRequestModifier: AsyncImageDownloadRequestModifier { + + /// This method will be called just before the `request` being sent. + /// This is the last chance you can modify the image download request. You can modify the request for some + /// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping. + /// + /// Usually, you pass an `ImageDownloadRequestModifier` as the associated value of + /// `KingfisherOptionsInfoItem.requestModifier` and use it as the `options` parameter in related methods. + /// + /// If you do nothing with the input `request` and return it as is, a downloading process will start with it. + /// + /// - Parameter request: The input request contains necessary information like `url`. This request is generated + /// according to your resource url as a GET request. + /// - Returns: A modified version of request, which you wish to use for downloading an image. If `nil` returned, + /// a `KingfisherError.requestError` with `.emptyRequest` as its reason will occur. + /// + func modified(for request: URLRequest) -> URLRequest? +} + +extension ImageDownloadRequestModifier { + public func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void) { + let request = modified(for: request) + reportModified(request) + } + + /// This is `nil` for a sync `ImageDownloadRequestModifier` by default. You can get the `DownloadTask` from the + /// return value of downloader method. + public var onDownloadTaskStarted: ((DownloadTask?) -> Void)? { return nil } +} + +/// A wrapper for creating an `ImageDownloadRequestModifier` easier. +/// This type conforms to `ImageDownloadRequestModifier` and wraps an image modify block. +public struct AnyModifier: ImageDownloadRequestModifier { + + let block: (URLRequest) -> URLRequest? + + /// For `ImageDownloadRequestModifier` conformation. + public func modified(for request: URLRequest) -> URLRequest? { + return block(request) + } + + /// Creates a value of `ImageDownloadRequestModifier` which runs `modify` block. + /// + /// - Parameter modify: The request modifying block runs when a request modifying task comes. + /// The return `URLRequest?` value of this block will be used as the image download request. + /// If `nil` returned, a `KingfisherError.requestError` with `.emptyRequest` as its + /// reason will occur. + public init(modify: @escaping (URLRequest) -> URLRequest?) { + block = modify + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift new file mode 100644 index 0000000..1ab5a2e --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift @@ -0,0 +1,153 @@ +// +// RetryStrategy.swift +// Kingfisher +// +// Created by onevcat on 2020/05/04. +// +// Copyright (c) 2020 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a retry context which could be used to determine the current retry status. +public class RetryContext { + + /// The source from which the target image should be retrieved. + public let source: Source + + /// The last error which caused current retry behavior. + public let error: KingfisherError + + /// The retried count before current retry happens. This value is `0` if the current retry is for the first time. + public var retriedCount: Int + + /// A user set value for passing any other information during the retry. If you choose to use `RetryDecision.retry` + /// as the retry decision for `RetryStrategy.retry(context:retryHandler:)`, the associated value of + /// `RetryDecision.retry` will be delivered to you in the next retry. + public internal(set) var userInfo: Any? = nil + + init(source: Source, error: KingfisherError) { + self.source = source + self.error = error + self.retriedCount = 0 + } + + @discardableResult + func increaseRetryCount() -> RetryContext { + retriedCount += 1 + return self + } +} + +/// Represents decision of behavior on the current retry. +public enum RetryDecision { + /// A retry should happen. The associated `userInfo` will be pass to the next retry in the `RetryContext` parameter. + case retry(userInfo: Any?) + /// There should be no more retry attempt. The image retrieving process will fail with an error. + case stop +} + +/// Defines a retry strategy can be applied to a `.retryStrategy` option. +public protocol RetryStrategy { + + /// Kingfisher calls this method if an error happens during the image retrieving process from a `KingfisherManager`. + /// You implement this method to provide necessary logic based on the `context` parameter. Then you need to call + /// `retryHandler` to pass the retry decision back to Kingfisher. + /// + /// - Parameters: + /// - context: The retry context containing information of current retry attempt. + /// - retryHandler: A block you need to call with a decision of whether the retry should happen or not. + func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void) +} + +/// A retry strategy that guides Kingfisher to retry when a `.responseError` happens, with a specified max retry count +/// and a certain interval mechanism. +public struct DelayRetryStrategy: RetryStrategy { + + /// Represents the interval mechanism which used in a `DelayRetryStrategy`. + public enum Interval { + /// The next retry attempt should happen in fixed seconds. For example, if the associated value is 3, the + /// attempts happens after 3 seconds after the previous decision is made. + case seconds(TimeInterval) + /// The next retry attempt should happen in an accumulated duration. For example, if the associated value is 3, + /// the attempts happens with interval of 3, 6, 9, 12, ... seconds. + case accumulated(TimeInterval) + /// Uses a block to determine the next interval. The current retry count is given as a parameter. + case custom(block: (_ retriedCount: Int) -> TimeInterval) + + func timeInterval(for retriedCount: Int) -> TimeInterval { + let retryAfter: TimeInterval + switch self { + case .seconds(let interval): + retryAfter = interval + case .accumulated(let interval): + retryAfter = Double(retriedCount + 1) * interval + case .custom(let block): + retryAfter = block(retriedCount) + } + return retryAfter + } + } + + /// The max retry count defined for the retry strategy + public let maxRetryCount: Int + + /// The retry interval mechanism defined for the retry strategy. + public let retryInterval: Interval + + /// Creates a delay retry strategy. + /// - Parameters: + /// - maxRetryCount: The max retry count. + /// - retryInterval: The retry interval mechanism. By default, `.seconds(3)` is used to provide a constant retry + /// interval. + public init(maxRetryCount: Int, retryInterval: Interval = .seconds(3)) { + self.maxRetryCount = maxRetryCount + self.retryInterval = retryInterval + } + + public func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void) { + // Retry count exceeded. + guard context.retriedCount < maxRetryCount else { + retryHandler(.stop) + return + } + + // User cancel the task. No retry. + guard !context.error.isTaskCancelled else { + retryHandler(.stop) + return + } + + // Only retry for a response error. + guard case KingfisherError.responseError = context.error else { + retryHandler(.stop) + return + } + + let interval = retryInterval.timeInterval(for: context.retriedCount) + if interval == 0 { + retryHandler(.retry(userInfo: nil)) + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + interval) { + retryHandler(.retry(userInfo: nil)) + } + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift new file mode 100644 index 0000000..8932bd5 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift @@ -0,0 +1,127 @@ +// +// SessionDataTask.swift +// Kingfisher +// +// Created by Wei Wang on 2018/11/1. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a session data task in `ImageDownloader`. It consists of an underlying `URLSessionDataTask` and +/// an array of `TaskCallback`. Multiple `TaskCallback`s could be added for a single downloading data task. +public class SessionDataTask { + + /// Represents the type of token which used for cancelling a task. + public typealias CancelToken = Int + + struct TaskCallback { + let onCompleted: Delegate, Void>? + let options: KingfisherParsedOptionsInfo + } + + /// Downloaded raw data of current task. + public private(set) var mutableData: Data + + // This is a copy of `task.originalRequest?.url`. It is for getting a race-safe behavior for a pitfall on iOS 13. + // Ref: https://github.com/onevcat/Kingfisher/issues/1511 + public let originalURL: URL? + + /// The underlying download task. It is only for debugging purpose when you encountered an error. You should not + /// modify the content of this task or start it yourself. + public let task: URLSessionDataTask + private var callbacksStore = [CancelToken: TaskCallback]() + + var callbacks: [SessionDataTask.TaskCallback] { + lock.lock() + defer { lock.unlock() } + return Array(callbacksStore.values) + } + + private var currentToken = 0 + private let lock = NSLock() + + let onTaskDone = Delegate<(Result<(Data, URLResponse?), KingfisherError>, [TaskCallback]), Void>() + let onCallbackCancelled = Delegate<(CancelToken, TaskCallback), Void>() + + var started = false + var containsCallbacks: Bool { + // We should be able to use `task.state != .running` to check it. + // However, in some rare cases, cancelling the task does not change + // task state to `.cancelling` immediately, but still in `.running`. + // So we need to check callbacks count to for sure that it is safe to remove the + // task in delegate. + return !callbacks.isEmpty + } + + init(task: URLSessionDataTask) { + self.task = task + self.originalURL = task.originalRequest?.url + mutableData = Data() + } + + func addCallback(_ callback: TaskCallback) -> CancelToken { + lock.lock() + defer { lock.unlock() } + callbacksStore[currentToken] = callback + defer { currentToken += 1 } + return currentToken + } + + func removeCallback(_ token: CancelToken) -> TaskCallback? { + lock.lock() + defer { lock.unlock() } + if let callback = callbacksStore[token] { + callbacksStore[token] = nil + return callback + } + return nil + } + + func removeAllCallbacks() -> Void { + lock.lock() + defer { lock.unlock() } + callbacksStore.removeAll() + } + + func resume() { + guard !started else { return } + started = true + task.resume() + } + + func cancel(token: CancelToken) { + guard let callback = removeCallback(token) else { + return + } + onCallbackCancelled.call((token, callback)) + } + + func forceCancel() { + for token in callbacksStore.keys { + cancel(token: token) + } + } + + func didReceiveData(_ data: Data) { + mutableData.append(data) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift new file mode 100644 index 0000000..bebe680 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift @@ -0,0 +1,263 @@ +// +// SessionDelegate.swift +// Kingfisher +// +// Created by Wei Wang on 2018/11/1. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +// Represents the delegate object of downloader session. It also behave like a task manager for downloading. +@objc(KFSessionDelegate) // Fix for ObjC header name conflicting. https://github.com/onevcat/Kingfisher/issues/1530 +open class SessionDelegate: NSObject { + + typealias SessionChallengeFunc = ( + URLSession, + URLAuthenticationChallenge, + (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) + + typealias SessionTaskChallengeFunc = ( + URLSession, + URLSessionTask, + URLAuthenticationChallenge, + (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) + + private var tasks: [URL: SessionDataTask] = [:] + private let lock = NSLock() + + let onValidStatusCode = Delegate() + let onDownloadingFinished = Delegate<(URL, Result), Void>() + let onDidDownloadData = Delegate() + + let onReceiveSessionChallenge = Delegate() + let onReceiveSessionTaskChallenge = Delegate() + + func add( + _ dataTask: URLSessionDataTask, + url: URL, + callback: SessionDataTask.TaskCallback) -> DownloadTask + { + lock.lock() + defer { lock.unlock() } + + // Create a new task if necessary. + let task = SessionDataTask(task: dataTask) + task.onCallbackCancelled.delegate(on: self) { [weak task] (self, value) in + guard let task = task else { return } + + let (token, callback) = value + + let error = KingfisherError.requestError(reason: .taskCancelled(task: task, token: token)) + task.onTaskDone.call((.failure(error), [callback])) + // No other callbacks waiting, we can clear the task now. + if !task.containsCallbacks { + let dataTask = task.task + + self.cancelTask(dataTask) + self.remove(task) + } + } + let token = task.addCallback(callback) + tasks[url] = task + return DownloadTask(sessionTask: task, cancelToken: token) + } + + private func cancelTask(_ dataTask: URLSessionDataTask) { + lock.lock() + defer { lock.unlock() } + dataTask.cancel() + } + + func append( + _ task: SessionDataTask, + url: URL, + callback: SessionDataTask.TaskCallback) -> DownloadTask + { + let token = task.addCallback(callback) + return DownloadTask(sessionTask: task, cancelToken: token) + } + + private func remove(_ task: SessionDataTask) { + lock.lock() + defer { lock.unlock() } + + guard let url = task.originalURL else { + return + } + task.removeAllCallbacks() + tasks[url] = nil + } + + private func task(for task: URLSessionTask) -> SessionDataTask? { + lock.lock() + defer { lock.unlock() } + + guard let url = task.originalRequest?.url else { + return nil + } + guard let sessionTask = tasks[url] else { + return nil + } + guard sessionTask.task.taskIdentifier == task.taskIdentifier else { + return nil + } + return sessionTask + } + + func task(for url: URL) -> SessionDataTask? { + lock.lock() + defer { lock.unlock() } + return tasks[url] + } + + func cancelAll() { + lock.lock() + let taskValues = tasks.values + lock.unlock() + for task in taskValues { + task.forceCancel() + } + } + + func cancel(url: URL) { + lock.lock() + let task = tasks[url] + lock.unlock() + task?.forceCancel() + } +} + +extension SessionDelegate: URLSessionDataDelegate { + + open func urlSession( + _ session: URLSession, + dataTask: URLSessionDataTask, + didReceive response: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) + { + guard let httpResponse = response as? HTTPURLResponse else { + let error = KingfisherError.responseError(reason: .invalidURLResponse(response: response)) + onCompleted(task: dataTask, result: .failure(error)) + completionHandler(.cancel) + return + } + + let httpStatusCode = httpResponse.statusCode + guard onValidStatusCode.call(httpStatusCode) == true else { + let error = KingfisherError.responseError(reason: .invalidHTTPStatusCode(response: httpResponse)) + onCompleted(task: dataTask, result: .failure(error)) + completionHandler(.cancel) + return + } + completionHandler(.allow) + } + + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + guard let task = self.task(for: dataTask) else { + return + } + + task.didReceiveData(data) + + task.callbacks.forEach { callback in + callback.options.onDataReceived?.forEach { sideEffect in + sideEffect.onDataReceived(session, task: task, data: data) + } + } + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + guard let sessionTask = self.task(for: task) else { return } + + if let url = sessionTask.originalURL { + let result: Result + if let error = error { + result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error))) + } else if let response = task.response { + result = .success(response) + } else { + result = .failure(KingfisherError.responseError(reason: .noURLResponse(task: sessionTask))) + } + onDownloadingFinished.call((url, result)) + } + + let result: Result<(Data, URLResponse?), KingfisherError> + if let error = error { + result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error))) + } else { + if let data = onDidDownloadData.call(sessionTask) { + result = .success((data, task.response)) + } else { + result = .failure(KingfisherError.responseError(reason: .dataModifyingFailed(task: sessionTask))) + } + } + onCompleted(task: task, result: result) + } + + open func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + onReceiveSessionChallenge.call((session, challenge, completionHandler)) + } + + open func urlSession( + _ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + onReceiveSessionTaskChallenge.call((session, task, challenge, completionHandler)) + } + + open func urlSession( + _ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) + { + guard let sessionDataTask = self.task(for: task), + let redirectHandler = Array(sessionDataTask.callbacks).last?.options.redirectHandler else + { + completionHandler(request) + return + } + + redirectHandler.handleHTTPRedirection( + for: sessionDataTask, + response: response, + newRequest: request, + completionHandler: completionHandler) + } + + private func onCompleted(task: URLSessionTask, result: Result<(Data, URLResponse?), KingfisherError>) { + guard let sessionTask = self.task(for: task) else { + return + } + sessionTask.onTaskDone.call((result, sessionTask.callbacks)) + remove(sessionTask) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift new file mode 100644 index 0000000..b0eec2d --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift @@ -0,0 +1,130 @@ +// +// ImageBinder.swift +// Kingfisher +// +// Created by onevcat on 2019/06/27. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage { + + /// Represents a binder for `KFImage`. It takes responsibility as an `ObjectBinding` and performs + /// image downloading and progress reporting based on `KingfisherManager`. + class ImageBinder: ObservableObject { + + init() {} + + var downloadTask: DownloadTask? + private var loading = false + + var loadingOrSucceeded: Bool { + return loading || loadedImage != nil + } + + // Do not use @Published due to https://github.com/onevcat/Kingfisher/issues/1717. Revert to @Published once + // we can drop iOS 12. + var loaded = false { willSet { objectWillChange.send() } } + var loadedImage: KFCrossPlatformImage? = nil { willSet { objectWillChange.send() } } + var progress: Progress = .init() { willSet { objectWillChange.send() } } + + func start(context: Context) { + guard let source = context.source else { + CallbackQueue.mainCurrentOrAsync.execute { + context.onFailureDelegate.call(KingfisherError.imageSettingError(reason: .emptySource)) + if let image = context.options.onFailureImage { + self.loadedImage = image + } + self.loading = false + self.loaded = true + } + return + } + + loading = true + + progress = .init() + downloadTask = KingfisherManager.shared + .retrieveImage( + with: source, + options: context.options, + progressBlock: { size, total in + self.updateProgress(downloaded: size, total: total) + context.onProgressDelegate.call((size, total)) + }, + completionHandler: { [weak self] result in + + guard let self = self else { return } + + CallbackQueue.mainCurrentOrAsync.execute { + self.downloadTask = nil + self.loading = false + } + + switch result { + case .success(let value): + CallbackQueue.mainCurrentOrAsync.execute { + if let fadeDuration = context.fadeTransitionDuration(cacheType: value.cacheType) { + let animation = Animation.linear(duration: fadeDuration) + withAnimation(animation) { self.loaded = true } + } else { + self.loaded = true + } + self.loadedImage = value.image + } + + CallbackQueue.mainAsync.execute { + context.onSuccessDelegate.call(value) + } + case .failure(let error): + CallbackQueue.mainCurrentOrAsync.execute { + if let image = context.options.onFailureImage { + self.loadedImage = image + } + self.loaded = true + } + + CallbackQueue.mainAsync.execute { + context.onFailureDelegate.call(error) + } + } + }) + } + + private func updateProgress(downloaded: Int64, total: Int64) { + progress.totalUnitCount = total + progress.completedUnitCount = downloaded + objectWillChange.send() + } + + /// Cancels the download task if it is in progress. + func cancel() { + downloadTask?.cancel() + downloadTask = nil + loading = false + } + } +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift new file mode 100644 index 0000000..7275f0e --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift @@ -0,0 +1,99 @@ +// +// ImageContext.swift +// Kingfisher +// +// Created by onevcat on 2021/05/08. +// +// Copyright (c) 2021 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage { + public class Context { + let source: Source? + var options = KingfisherParsedOptionsInfo( + KingfisherManager.shared.defaultOptions + [.loadDiskFileSynchronously] + ) + + var configurations: [(HoldingView) -> HoldingView] = [] + var renderConfigurations: [(HoldingView.RenderingView) -> Void] = [] + + var cancelOnDisappear: Bool = false + var placeholder: ((Progress) -> AnyView)? = nil + + let onFailureDelegate = Delegate() + let onSuccessDelegate = Delegate() + let onProgressDelegate = Delegate<(Int64, Int64), Void>() + + init(source: Source?) { + self.source = source + } + + func shouldApplyFade(cacheType: CacheType) -> Bool { + options.forceTransition || cacheType == .none + } + + func fadeTransitionDuration(cacheType: CacheType) -> TimeInterval? { + shouldApplyFade(cacheType: cacheType) + ? options.transition.fadeDuration + : nil + } + } +} + +extension ImageTransition { + // Only for fade effect in SwiftUI. + fileprivate var fadeDuration: TimeInterval? { + switch self { + case .fade(let duration): + return duration + default: + return nil + } + } +} + + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage.Context: Hashable { + public static func == (lhs: KFImage.Context, rhs: KFImage.Context) -> Bool { + lhs.source == rhs.source && + lhs.options.processor.identifier == rhs.options.processor.identifier + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(source) + hasher.combine(options.processor.identifier) + } +} + +#if canImport(UIKit) && !os(watchOS) +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFAnimatedImage { + public typealias Context = KFImage.Context + typealias ImageBinder = KFImage.ImageBinder +} +#endif + +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift new file mode 100644 index 0000000..ad25eb2 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift @@ -0,0 +1,96 @@ +// +// KFAnimatedImage.swift +// Kingfisher +// +// Created by wangxingbin on 2021/4/29. +// +// Copyright (c) 2021 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(SwiftUI) && canImport(Combine) && canImport(UIKit) && !os(watchOS) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public struct KFAnimatedImage: KFImageProtocol { + public typealias HoldingView = KFAnimatedImageViewRepresenter + public var context: Context + public init(context: KFImage.Context) { + self.context = context + } + + /// Configures current rendering view with a `block`. This block will be applied when the under-hood + /// `AnimatedImageView` is created in `UIViewRepresentable.makeUIView(context:)` + /// + /// - Parameter block: The block applies to the animated image view. + /// - Returns: A `KFAnimatedImage` view that being configured by the `block`. + public func configure(_ block: @escaping (HoldingView.RenderingView) -> Void) -> Self { + context.renderConfigurations.append(block) + return self + } +} + +/// A wrapped `UIViewRepresentable` of `AnimatedImageView` +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public struct KFAnimatedImageViewRepresenter: UIViewRepresentable, KFImageHoldingView { + public typealias RenderingView = AnimatedImageView + public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> KFAnimatedImageViewRepresenter { + KFAnimatedImageViewRepresenter(image: image, context: context) + } + + var image: KFCrossPlatformImage? + let context: KFImage.Context + + public func makeUIView(context: Context) -> AnimatedImageView { + let view = AnimatedImageView() + + self.context.renderConfigurations.forEach { $0(view) } + + view.image = image + + // Allow SwiftUI scale (fit/fill) working fine. + view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + view.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + return view + } + + public func updateUIView(_ uiView: AnimatedImageView, context: Context) { + uiView.image = image + } +} + +#if DEBUG +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +struct KFAnimatedImage_Previews: PreviewProvider { + static var previews: some View { + Group { + KFAnimatedImage(source: .network(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF/1.gif")!)) + .onSuccess { r in + print(r) + } + .placeholder { + ProgressView() + } + .padding() + } + } +} +#endif +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift new file mode 100644 index 0000000..67f1c19 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift @@ -0,0 +1,106 @@ +// +// KFImage.swift +// Kingfisher +// +// Created by onevcat on 2019/06/26. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public struct KFImage: KFImageProtocol { + public var context: Context + public init(context: Context) { + self.context = context + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension Image: KFImageHoldingView { + public typealias RenderingView = Image + public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> Image { + Image(crossPlatformImage: image) + } +} + +// MARK: - Image compatibility. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage { + + public func resizable( + capInsets: EdgeInsets = EdgeInsets(), + resizingMode: Image.ResizingMode = .stretch) -> KFImage + { + configure { $0.resizable(capInsets: capInsets, resizingMode: resizingMode) } + } + + public func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> KFImage { + configure { $0.renderingMode(renderingMode) } + } + + public func interpolation(_ interpolation: Image.Interpolation) -> KFImage { + configure { $0.interpolation(interpolation) } + } + + public func antialiased(_ isAntialiased: Bool) -> KFImage { + configure { $0.antialiased(isAntialiased) } + } + + /// Starts the loading process of `self` immediately. + /// + /// By default, a `KFImage` will not load its source until the `onAppear` is called. This is a lazily loading + /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a + /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once + /// could help avoiding the flickering, with some performance trade-off. + /// + /// - Deprecated: This is not necessary anymore since `@StateObject` is used for holding the image data. + /// It does nothing now and please just remove it. + /// + /// - Returns: The `Self` value with changes applied. + @available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.") + public func loadImmediately(_ start: Bool = true) -> KFImage { + return self + } +} + +#if DEBUG +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +struct KFImage_Previews: PreviewProvider { + static var previews: some View { + Group { + KFImage.url(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png")!) + .onSuccess { r in + print(r) + } + .placeholder { p in + ProgressView(p) + } + .resizable() + .aspectRatio(contentMode: .fit) + .padding() + } + } +} +#endif +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift new file mode 100644 index 0000000..e553b2a --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift @@ -0,0 +1,138 @@ +// +// KFImageOptions.swift +// Kingfisher +// +// Created by onevcat on 2020/12/20. +// +// Copyright (c) 2020 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +// MARK: - KFImage creating. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + + /// Creates a `KFImage` for a given `Source`. + /// - Parameters: + /// - source: The `Source` object defines data information from network or a data provider. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func source( + _ source: Source? + ) -> Self + { + Self.init(source: source) + } + + /// Creates a `KFImage` for a given `Resource`. + /// - Parameters: + /// - source: The `Resource` object defines data information like key or URL. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func resource( + _ resource: Resource? + ) -> Self + { + source(resource?.convertToSource()) + } + + /// Creates a `KFImage` for a given `URL`. + /// - Parameters: + /// - url: The URL where the image should be downloaded. + /// - cacheKey: The key used to store the downloaded image in cache. + /// If `nil`, the `absoluteString` of `url` is used as the cache key. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func url( + _ url: URL?, cacheKey: String? = nil + ) -> Self + { + source(url?.convertToSource(overrideCacheKey: cacheKey)) + } + + /// Creates a `KFImage` for a given `ImageDataProvider`. + /// - Parameters: + /// - provider: The `ImageDataProvider` object contains information about the data. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func dataProvider( + _ provider: ImageDataProvider? + ) -> Self + { + source(provider?.convertToSource()) + } + + /// Creates a builder for some given raw data and a cache key. + /// - Parameters: + /// - data: The data object from which the image should be created. + /// - cacheKey: The key used to store the downloaded image in cache. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func data( + _ data: Data?, cacheKey: String + ) -> Self + { + if let data = data { + return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey)) + } else { + return dataProvider(nil) + } + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + /// Sets a placeholder `View` which shows when loading the image, with a progress parameter as input. + /// - Parameter content: A view that describes the placeholder. + /// - Returns: A `KFImage` view that contains `content` as its placeholder. + public func placeholder(@ViewBuilder _ content: @escaping (Progress) -> P) -> Self { + context.placeholder = { progress in + return AnyView(content(progress)) + } + return self + } + + /// Sets a placeholder `View` which shows when loading the image. + /// - Parameter content: A view that describes the placeholder. + /// - Returns: A `KFImage` view that contains `content` as its placeholder. + public func placeholder(@ViewBuilder _ content: @escaping () -> P) -> Self { + placeholder { _ in content() } + } + + /// Sets cancelling the download task bound to `self` when the view disappearing. + /// - Parameter flag: Whether cancel the task or not. + /// - Returns: A `KFImage` view that cancels downloading task when disappears. + public func cancelOnDisappear(_ flag: Bool) -> Self { + context.cancelOnDisappear = flag + return self + } + + /// Sets a fade transition for the image task. + /// - Parameter duration: The duration of the fade transition. + /// - Returns: A `KFImage` with changes applied. + /// + /// Kingfisher will use the fade transition to animate the image in if it is downloaded from web. + /// The transition will not happen when the + /// image is retrieved from either memory or disk cache by default. If you need to do the transition even when + /// the image being retrieved from cache, also call `forceRefresh()` on the returned `KFImage`. + public func fade(duration: TimeInterval) -> Self { + context.options.transition = .fade(duration) + return self + } +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift new file mode 100644 index 0000000..4eca452 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift @@ -0,0 +1,93 @@ +// +// KFImageProtocol.swift +// Kingfisher +// +// Created by onevcat on 2021/05/08. +// +// Copyright (c) 2021 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public protocol KFImageProtocol: View, KFOptionSetter { + associatedtype HoldingView: KFImageHoldingView + var context: KFImage.Context { get set } + init(context: KFImage.Context) +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + public var body: some View { + ZStack { + KFImageRenderer( + context: context + ).id(context) + } + } + + /// Creates a Kingfisher compatible image view to load image from the given `Source`. + /// - Parameters: + /// - source: The image `Source` defining where to load the target image. + public init(source: Source?) { + let context = KFImage.Context(source: source) + self.init(context: context) + } + + /// Creates a Kingfisher compatible image view to load image from the given `URL`. + /// - Parameters: + /// - source: The image `Source` defining where to load the target image. + public init(_ url: URL?) { + self.init(source: url?.convertToSource()) + } + + /// Configures current image with a `block`. This block will be lazily applied when creating the final `Image`. + /// - Parameter block: The block applies to loaded image. + /// - Returns: A `KFImage` view that configures internal `Image` with `block`. + public func configure(_ block: @escaping (HoldingView) -> HoldingView) -> Self { + context.configurations.append(block) + return self + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public protocol KFImageHoldingView: View { + associatedtype RenderingView + static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> Self +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + public var options: KingfisherParsedOptionsInfo { + get { context.options } + nonmutating set { context.options = newValue } + } + + public var onFailureDelegate: Delegate { context.onFailureDelegate } + public var onSuccessDelegate: Delegate { context.onSuccessDelegate } + public var onProgressDelegate: Delegate<(Int64, Int64), Void> { context.onProgressDelegate } + + public var delegateObserver: AnyObject { context } +} + + +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift new file mode 100644 index 0000000..90e6846 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift @@ -0,0 +1,105 @@ +// +// KFImageRenderer.swift +// Kingfisher +// +// Created by onevcat on 2021/05/08. +// +// Copyright (c) 2021 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +/// A Kingfisher compatible SwiftUI `View` to load an image from a `Source`. +/// Declaring a `KFImage` in a `View`'s body to trigger loading from the given `Source`. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +struct KFImageRenderer : View where HoldingView: KFImageHoldingView { + + @StateObject var binder: KFImage.ImageBinder = .init() + let context: KFImage.Context + + var body: some View { + ZStack { + context.configurations + .reduce(HoldingView.created(from: binder.loadedImage, context: context)) { + current, config in config(current) + } + .opacity(binder.loaded ? 1.0 : 0.0) + if binder.loadedImage == nil { + Group { + if let placeholder = context.placeholder, let view = placeholder(binder.progress) { + view + } else { + Color.clear + } + } + .onAppear { [weak binder = self.binder] in + guard let binder = binder else { + return + } + if !binder.loadingOrSucceeded { + binder.start(context: context) + } + } + .onDisappear { [weak binder = self.binder] in + guard let binder = binder else { + return + } + if context.cancelOnDisappear { + binder.cancel() + } + } + } + } + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension Image { + // Creates an Image with either UIImage or NSImage. + init(crossPlatformImage: KFCrossPlatformImage?) { + #if canImport(UIKit) + self.init(uiImage: crossPlatformImage ?? KFCrossPlatformImage()) + #elseif canImport(AppKit) + self.init(nsImage: crossPlatformImage ?? KFCrossPlatformImage()) + #endif + } +} + +#if canImport(UIKit) +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension UIImage.Orientation { + func toSwiftUI() -> Image.Orientation { + switch self { + case .down: return .down + case .up: return .up + case .left: return .left + case .right: return .right + case .upMirrored: return .upMirrored + case .downMirrored: return .downMirrored + case .leftMirrored: return .leftMirrored + case .rightMirrored: return .rightMirrored + @unknown default: return .up + } + } +} +#endif +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Box.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Box.swift new file mode 100644 index 0000000..0303a6e --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Box.swift @@ -0,0 +1,34 @@ +// +// Box.swift +// Kingfisher +// +// Created by Wei Wang on 2018/3/17. +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +class Box { + var value: T + + init(_ value: T) { + self.value = value + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift new file mode 100644 index 0000000..822af28 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift @@ -0,0 +1,83 @@ +// +// CallbackQueue.swift +// Kingfisher +// +// Created by onevcat on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +public typealias ExecutionQueue = CallbackQueue + +/// Represents callback queue behaviors when an calling of closure be dispatched. +/// +/// - asyncMain: Dispatch the calling to `DispatchQueue.main` with an `async` behavior. +/// - currentMainOrAsync: Dispatch the calling to `DispatchQueue.main` with an `async` behavior if current queue is not +/// `.main`. Otherwise, call the closure immediately in current main queue. +/// - untouch: Do not change the calling queue for closure. +/// - dispatch: Dispatches to a specified `DispatchQueue`. +public enum CallbackQueue { + /// Dispatch the calling to `DispatchQueue.main` with an `async` behavior. + case mainAsync + /// Dispatch the calling to `DispatchQueue.main` with an `async` behavior if current queue is not + /// `.main`. Otherwise, call the closure immediately in current main queue. + case mainCurrentOrAsync + /// Do not change the calling queue for closure. + case untouch + /// Dispatches to a specified `DispatchQueue`. + case dispatch(DispatchQueue) + + public func execute(_ block: @escaping () -> Void) { + switch self { + case .mainAsync: + DispatchQueue.main.async { block() } + case .mainCurrentOrAsync: + DispatchQueue.main.safeAsync { block() } + case .untouch: + block() + case .dispatch(let queue): + queue.async { block() } + } + } + + var queue: DispatchQueue { + switch self { + case .mainAsync: return .main + case .mainCurrentOrAsync: return .main + case .untouch: return OperationQueue.current?.underlyingQueue ?? .main + case .dispatch(let queue): return queue + } + } +} + +extension DispatchQueue { + // This method will dispatch the `block` to self. + // If `self` is the main queue, and current thread is main thread, the block + // will be invoked immediately instead of being dispatched. + func safeAsync(_ block: @escaping () -> Void) { + if self === DispatchQueue.main && Thread.isMainThread { + block() + } else { + async { block() } + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Delegate.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Delegate.swift new file mode 100644 index 0000000..9caa1a6 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Delegate.swift @@ -0,0 +1,132 @@ +// +// Delegate.swift +// Kingfisher +// +// Created by onevcat on 2018/10/10. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +/// A class that keeps a weakly reference for `self` when implementing `onXXX` behaviors. +/// Instead of remembering to keep `self` as weak in a stored closure: +/// +/// ```swift +/// // MyClass.swift +/// var onDone: (() -> Void)? +/// func done() { +/// onDone?() +/// } +/// +/// // ViewController.swift +/// var obj: MyClass? +/// +/// func doSomething() { +/// obj = MyClass() +/// obj!.onDone = { [weak self] in +/// self?.reportDone() +/// } +/// } +/// ``` +/// +/// You can create a `Delegate` and observe on `self`. Now, there is no retain cycle inside: +/// +/// ```swift +/// // MyClass.swift +/// let onDone = Delegate<(), Void>() +/// func done() { +/// onDone.call() +/// } +/// +/// // ViewController.swift +/// var obj: MyClass? +/// +/// func doSomething() { +/// obj = MyClass() +/// obj!.onDone.delegate(on: self) { (self, _) +/// // `self` here is shadowed and does not keep a strong ref. +/// // So you can release both `MyClass` instance and `ViewController` instance. +/// self.reportDone() +/// } +/// } +/// ``` +/// +public class Delegate { + public init() {} + + private var block: ((Input) -> Output?)? + public func delegate(on target: T, block: ((T, Input) -> Output)?) { + self.block = { [weak target] input in + guard let target = target else { return nil } + return block?(target, input) + } + } + + public func call(_ input: Input) -> Output? { + return block?(input) + } + + public func callAsFunction(_ input: Input) -> Output? { + return call(input) + } +} + +extension Delegate where Input == Void { + public func call() -> Output? { + return call(()) + } + + public func callAsFunction() -> Output? { + return call() + } +} + +extension Delegate where Input == Void, Output: OptionalProtocol { + public func call() -> Output { + return call(()) + } + + public func callAsFunction() -> Output { + return call() + } +} + +extension Delegate where Output: OptionalProtocol { + public func call(_ input: Input) -> Output { + if let result = block?(input) { + return result + } else { + return Output._createNil + } + } + + public func callAsFunction(_ input: Input) -> Output { + return call(input) + } +} + +public protocol OptionalProtocol { + static var _createNil: Self { get } +} +extension Optional : OptionalProtocol { + public static var _createNil: Optional { + return nil + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift new file mode 100644 index 0000000..f1e1e8b --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift @@ -0,0 +1,125 @@ +// +// ExtensionHelpers.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +extension CGFloat { + var isEven: Bool { + return truncatingRemainder(dividingBy: 2.0) == 0 + } +} + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +extension NSBezierPath { + convenience init(roundedRect rect: NSRect, topLeftRadius: CGFloat, topRightRadius: CGFloat, + bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat) + { + self.init() + + let maxCorner = min(rect.width, rect.height) / 2 + + let radiusTopLeft = min(maxCorner, max(0, topLeftRadius)) + let radiusTopRight = min(maxCorner, max(0, topRightRadius)) + let radiusBottomLeft = min(maxCorner, max(0, bottomLeftRadius)) + let radiusBottomRight = min(maxCorner, max(0, bottomRightRadius)) + + guard !rect.isEmpty else { + return + } + + let topLeft = NSPoint(x: rect.minX, y: rect.maxY) + let topRight = NSPoint(x: rect.maxX, y: rect.maxY) + let bottomRight = NSPoint(x: rect.maxX, y: rect.minY) + + move(to: NSPoint(x: rect.midX, y: rect.maxY)) + appendArc(from: topLeft, to: rect.origin, radius: radiusTopLeft) + appendArc(from: rect.origin, to: bottomRight, radius: radiusBottomLeft) + appendArc(from: bottomRight, to: topRight, radius: radiusBottomRight) + appendArc(from: topRight, to: topLeft, radius: radiusTopRight) + close() + } + + convenience init(roundedRect rect: NSRect, byRoundingCorners corners: RectCorner, radius: CGFloat) { + let radiusTopLeft = corners.contains(.topLeft) ? radius : 0 + let radiusTopRight = corners.contains(.topRight) ? radius : 0 + let radiusBottomLeft = corners.contains(.bottomLeft) ? radius : 0 + let radiusBottomRight = corners.contains(.bottomRight) ? radius : 0 + + self.init(roundedRect: rect, topLeftRadius: radiusTopLeft, topRightRadius: radiusTopRight, + bottomLeftRadius: radiusBottomLeft, bottomRightRadius: radiusBottomRight) + } +} + +extension KFCrossPlatformImage { + // macOS does not support scale. This is just for code compatibility across platforms. + convenience init?(data: Data, scale: CGFloat) { + self.init(data: data) + } +} +#endif + +#if canImport(UIKit) +import UIKit +extension RectCorner { + var uiRectCorner: UIRectCorner { + + var result: UIRectCorner = [] + + if contains(.topLeft) { result.insert(.topLeft) } + if contains(.topRight) { result.insert(.topRight) } + if contains(.bottomLeft) { result.insert(.bottomLeft) } + if contains(.bottomRight) { result.insert(.bottomRight) } + + return result + } +} +#endif + +extension Date { + var isPast: Bool { + return isPast(referenceDate: Date()) + } + + var isFuture: Bool { + return !isPast + } + + func isPast(referenceDate: Date) -> Bool { + return timeIntervalSince(referenceDate) <= 0 + } + + func isFuture(referenceDate: Date) -> Bool { + return !isPast(referenceDate: referenceDate) + } + + // `Date` in memory is a wrap for `TimeInterval`. But in file attribute it can only accept `Int` number. + // By default the system will `round` it. But it is not friendly for testing purpose. + // So we always `ceil` the value when used for file attributes. + var fileAttributeDate: Date { + return Date(timeIntervalSince1970: ceil(timeIntervalSince1970)) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Result.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Result.swift new file mode 100644 index 0000000..b06500d --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Result.swift @@ -0,0 +1,71 @@ +// +// Result.swift +// Kingfisher +// +// Created by onevcat on 2018/09/22. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +// These helper methods are not public since we do not want them to be exposed or cause any conflicting. +// However, they are just wrapper of `ResultUtil` static methods. +extension Result where Failure: Error { + + /// Evaluates the given transform closures to create a single output value. + /// + /// - Parameters: + /// - onSuccess: A closure that transforms the success value. + /// - onFailure: A closure that transforms the error value. + /// - Returns: A single `Output` value. + func match( + onSuccess: (Success) -> Output, + onFailure: (Failure) -> Output) -> Output + { + switch self { + case let .success(value): + return onSuccess(value) + case let .failure(error): + return onFailure(error) + } + } + + func matchSuccess(with folder: (Success?) -> Output) -> Output { + return match( + onSuccess: { value in return folder(value) }, + onFailure: { _ in return folder(nil) } + ) + } + + func matchFailure(with folder: (Error?) -> Output) -> Output { + return match( + onSuccess: { _ in return folder(nil) }, + onFailure: { error in return folder(error) } + ) + } + + func match(with folder: (Success?, Error?) -> Output) -> Output { + return match( + onSuccess: { return folder($0, nil) }, + onFailure: { return folder(nil, $0) } + ) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Runtime.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Runtime.swift new file mode 100644 index 0000000..d5818e2 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Runtime.swift @@ -0,0 +1,35 @@ +// +// Runtime.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/12. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +func getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> T? { + return objc_getAssociatedObject(object, key) as? T +} + +func setRetainedAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: T) { + objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift new file mode 100644 index 0000000..19d05d6 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift @@ -0,0 +1,110 @@ +// +// SizeExtensions.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import CoreGraphics + +extension CGSize: KingfisherCompatibleValue {} +extension KingfisherWrapper where Base == CGSize { + + /// Returns a size by resizing the `base` size to a target size under a given content mode. + /// + /// - Parameters: + /// - size: The target size to resize to. + /// - contentMode: Content mode of the target size should be when resizing. + /// - Returns: The resized size under the given `ContentMode`. + public func resize(to size: CGSize, for contentMode: ContentMode) -> CGSize { + switch contentMode { + case .aspectFit: + return constrained(size) + case .aspectFill: + return filling(size) + case .none: + return size + } + } + + /// Returns a size by resizing the `base` size by making it aspect fitting the given `size`. + /// + /// - Parameter size: The size in which the `base` should fit in. + /// - Returns: The size fitted in by the input `size`, while keeps `base` aspect. + public func constrained(_ size: CGSize) -> CGSize { + let aspectWidth = round(aspectRatio * size.height) + let aspectHeight = round(size.width / aspectRatio) + + return aspectWidth > size.width ? + CGSize(width: size.width, height: aspectHeight) : + CGSize(width: aspectWidth, height: size.height) + } + + /// Returns a size by resizing the `base` size by making it aspect filling the given `size`. + /// + /// - Parameter size: The size in which the `base` should fill. + /// - Returns: The size be filled by the input `size`, while keeps `base` aspect. + public func filling(_ size: CGSize) -> CGSize { + let aspectWidth = round(aspectRatio * size.height) + let aspectHeight = round(size.width / aspectRatio) + + return aspectWidth < size.width ? + CGSize(width: size.width, height: aspectHeight) : + CGSize(width: aspectWidth, height: size.height) + } + + /// Returns a `CGRect` for which the `base` size is constrained to an input `size` at a given `anchor` point. + /// + /// - Parameters: + /// - size: The size in which the `base` should be constrained to. + /// - anchor: An anchor point in which the size constraint should happen. + /// - Returns: The result `CGRect` for the constraint operation. + public func constrainedRect(for size: CGSize, anchor: CGPoint) -> CGRect { + + let unifiedAnchor = CGPoint(x: anchor.x.clamped(to: 0.0...1.0), + y: anchor.y.clamped(to: 0.0...1.0)) + + let x = unifiedAnchor.x * base.width - unifiedAnchor.x * size.width + let y = unifiedAnchor.y * base.height - unifiedAnchor.y * size.height + let r = CGRect(x: x, y: y, width: size.width, height: size.height) + + let ori = CGRect(origin: .zero, size: base) + return ori.intersection(r) + } + + private var aspectRatio: CGFloat { + return base.height == 0.0 ? 1.0 : base.width / base.height + } +} + +extension CGRect { + func scaled(_ scale: CGFloat) -> CGRect { + return CGRect(x: origin.x * scale, y: origin.y * scale, + width: size.width * scale, height: size.height * scale) + } +} + +extension Comparable { + func clamped(to limits: ClosedRange) -> Self { + return min(max(self, limits.lowerBound), limits.upperBound) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/String+MD5.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/String+MD5.swift new file mode 100644 index 0000000..091fb62 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/String+MD5.swift @@ -0,0 +1,288 @@ +// +// String+MD5.swift +// Kingfisher +// +// Created by Wei Wang on 18/09/25. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CommonCrypto + +extension String: KingfisherCompatibleValue { } +extension KingfisherWrapper where Base == String { + var md5: String { + guard let data = base.data(using: .utf8) else { + return base + } + + let message = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in + return [UInt8](bytes) + } + + let MD5Calculator = MD5(message) + let MD5Data = MD5Calculator.calculate() + + var MD5String = String() + for c in MD5Data { + MD5String += String(format: "%02x", c) + } + return MD5String + } + + var ext: String? { + var ext = "" + if let index = base.lastIndex(of: ".") { + let extRange = base.index(index, offsetBy: 1).. 0 ? String(firstSeg) : nil + } +} + +// array of bytes, little-endian representation +func arrayOfBytes(_ value: T, length: Int? = nil) -> [UInt8] { + let totalBytes = length ?? (MemoryLayout.size * 8) + + let valuePointer = UnsafeMutablePointer.allocate(capacity: 1) + valuePointer.pointee = value + + let bytes = valuePointer.withMemoryRebound(to: UInt8.self, capacity: totalBytes) { (bytesPointer) -> [UInt8] in + var bytes = [UInt8](repeating: 0, count: totalBytes) + for j in 0...size, totalBytes) { + bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee + } + return bytes + } + + valuePointer.deinitialize(count: 1) + valuePointer.deallocate() + + return bytes +} + +extension Int { + // Array of bytes with optional padding (little-endian) + func bytes(_ totalBytes: Int = MemoryLayout.size) -> [UInt8] { + return arrayOfBytes(self, length: totalBytes) + } + +} + +extension NSMutableData { + + // Convenient way to append bytes + func appendBytes(_ arrayOfBytes: [UInt8]) { + append(arrayOfBytes, length: arrayOfBytes.count) + } + +} + +protocol HashProtocol { + var message: [UInt8] { get } + // Common part for hash calculation. Prepare header data. + func prepare(_ len: Int) -> [UInt8] +} + +extension HashProtocol { + + func prepare(_ len: Int) -> [UInt8] { + var tmpMessage = message + + // Step 1. Append Padding Bits + tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message + + // append "0" bit until message length in bits ≡ 448 (mod 512) + var msgLength = tmpMessage.count + var counter = 0 + + while msgLength % len != (len - 8) { + counter += 1 + msgLength += 1 + } + + tmpMessage += [UInt8](repeating: 0, count: counter) + return tmpMessage + } +} + +func toUInt32Array(_ slice: ArraySlice) -> [UInt32] { + var result = [UInt32]() + result.reserveCapacity(16) + + for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout.size) { + let d0 = UInt32(slice[idx.advanced(by: 3)]) << 24 + let d1 = UInt32(slice[idx.advanced(by: 2)]) << 16 + let d2 = UInt32(slice[idx.advanced(by: 1)]) << 8 + let d3 = UInt32(slice[idx]) + let val: UInt32 = d0 | d1 | d2 | d3 + + result.append(val) + } + return result +} + +struct BytesIterator: IteratorProtocol { + + let chunkSize: Int + let data: [UInt8] + + init(chunkSize: Int, data: [UInt8]) { + self.chunkSize = chunkSize + self.data = data + } + + var offset = 0 + + mutating func next() -> ArraySlice? { + let end = min(chunkSize, data.count - offset) + let result = data[offset.. 0 ? result : nil + } +} + +struct BytesSequence: Sequence { + let chunkSize: Int + let data: [UInt8] + + func makeIterator() -> BytesIterator { + return BytesIterator(chunkSize: chunkSize, data: data) + } +} + +func rotateLeft(_ value: UInt32, bits: UInt32) -> UInt32 { + return ((value << bits) & 0xFFFFFFFF) | (value >> (32 - bits)) +} + +class MD5: HashProtocol { + + static let size = 16 // 128 / 8 + let message: [UInt8] + + init (_ message: [UInt8]) { + self.message = message + } + + // specifies the per-round shift amounts + private let shifts: [UInt32] = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21] + + // binary integer part of the sines of integers (Radians) + private let sines: [UInt32] = [0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, + 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, + 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, + 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05, + 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, + 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391] + + private let hashes: [UInt32] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] + + func calculate() -> [UInt8] { + var tmpMessage = prepare(64) + tmpMessage.reserveCapacity(tmpMessage.count + 4) + + // hash values + var hh = hashes + + // Step 2. Append Length a 64-bit representation of lengthInBits + let lengthInBits = (message.count * 8) + let lengthBytes = lengthInBits.bytes(64 / 8) + tmpMessage += lengthBytes.reversed() + + // Process the message in successive 512-bit chunks: + let chunkSizeBytes = 512 / 8 // 64 + + for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) { + // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15 + let M = toUInt32Array(chunk) + assert(M.count == 16, "Invalid array") + + // Initialize hash value for this chunk: + var A: UInt32 = hh[0] + var B: UInt32 = hh[1] + var C: UInt32 = hh[2] + var D: UInt32 = hh[3] + + var dTemp: UInt32 = 0 + + // Main loop + for j in 0 ..< sines.count { + var g = 0 + var F: UInt32 = 0 + + switch j { + case 0...15: + F = (B & C) | ((~B) & D) + g = j + case 16...31: + F = (D & B) | (~D & C) + g = (5 * j + 1) % 16 + case 32...47: + F = B ^ C ^ D + g = (3 * j + 5) % 16 + case 48...63: + F = C ^ (B | (~D)) + g = (7 * j) % 16 + default: + break + } + dTemp = D + D = C + C = B + B = B &+ rotateLeft((A &+ F &+ sines[j] &+ M[g]), bits: shifts[j]) + A = dTemp + } + + hh[0] = hh[0] &+ A + hh[1] = hh[1] &+ B + hh[2] = hh[2] &+ C + hh[3] = hh[3] &+ D + } + var result = [UInt8]() + result.reserveCapacity(hh.count / 4) + + hh.forEach { + let itemLE = $0.littleEndian + let r1 = UInt8(itemLE & 0xff) + let r2 = UInt8((itemLE >> 8) & 0xff) + let r3 = UInt8((itemLE >> 16) & 0xff) + let r4 = UInt8((itemLE >> 24) & 0xff) + result += [r1, r2, r3, r4] + } + return result + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift new file mode 100644 index 0000000..94c2b7b --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift @@ -0,0 +1,660 @@ +// +// AnimatableImageView.swift +// Kingfisher +// +// Created by bl4ckra1sond3tre on 4/22/16. +// +// The AnimatableImageView, AnimatedFrame and Animator is a modified version of +// some classes from kaishin's Gifu project (https://github.com/kaishin/Gifu) +// +// The MIT License (MIT) +// +// Copyright (c) 2019 Reda Lemeden. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// The name and characters used in the demo of this software are property of their +// respective owners. + +#if !os(watchOS) +#if canImport(UIKit) +import UIKit +import ImageIO + +/// Protocol of `AnimatedImageView`. +public protocol AnimatedImageViewDelegate: AnyObject { + + /// Called after the animatedImageView has finished each animation loop. + /// + /// - Parameters: + /// - imageView: The `AnimatedImageView` that is being animated. + /// - count: The looped count. + func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) + + /// Called after the `AnimatedImageView` has reached the max repeat count. + /// + /// - Parameter imageView: The `AnimatedImageView` that is being animated. + func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) +} + +extension AnimatedImageViewDelegate { + public func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) {} + public func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) {} +} + +let KFRunLoopModeCommon = RunLoop.Mode.common + +/// Represents a subclass of `UIImageView` for displaying animated image. +/// Different from showing animated image in a normal `UIImageView` (which load all frames at one time), +/// `AnimatedImageView` only tries to load several frames (defined by `framePreloadCount`) to reduce memory usage. +/// It provides a tradeoff between memory usage and CPU time. If you have a memory issue when using a normal image +/// view to load GIF data, you could give this class a try. +/// +/// Kingfisher supports setting GIF animated data to either `UIImageView` and `AnimatedImageView` out of box. So +/// it would be fairly easy to switch between them. +open class AnimatedImageView: UIImageView { + /// Proxy object for preventing a reference cycle between the `CADDisplayLink` and `AnimatedImageView`. + class TargetProxy { + private weak var target: AnimatedImageView? + + init(target: AnimatedImageView) { + self.target = target + } + + @objc func onScreenUpdate() { + target?.updateFrameIfNeeded() + } + } + + /// Enumeration that specifies repeat count of GIF + public enum RepeatCount: Equatable { + case once + case finite(count: UInt) + case infinite + + public static func ==(lhs: RepeatCount, rhs: RepeatCount) -> Bool { + switch (lhs, rhs) { + case let (.finite(l), .finite(r)): + return l == r + case (.once, .once), + (.infinite, .infinite): + return true + case (.once, .finite(let count)), + (.finite(let count), .once): + return count == 1 + case (.once, _), + (.infinite, _), + (.finite, _): + return false + } + } + } + + // MARK: - Public property + /// Whether automatically play the animation when the view become visible. Default is `true`. + public var autoPlayAnimatedImage = true + + /// The count of the frames should be preloaded before shown. + public var framePreloadCount = 10 + + /// Specifies whether the GIF frames should be pre-scaled to the image view's size or not. + /// If the downloaded image is larger than the image view's size, it will help to reduce some memory use. + /// Default is `true`. + public var needsPrescaling = true + + /// Decode the GIF frames in background thread before using. It will decode frames data and do a off-screen + /// rendering to extract pixel information in background. This can reduce the main thread CPU usage. + public var backgroundDecode = true + + /// The animation timer's run loop mode. Default is `RunLoop.Mode.common`. + /// Set this property to `RunLoop.Mode.default` will make the animation pause during UIScrollView scrolling. + public var runLoopMode = KFRunLoopModeCommon { + willSet { + guard runLoopMode != newValue else { return } + stopAnimating() + displayLink.remove(from: .main, forMode: runLoopMode) + displayLink.add(to: .main, forMode: newValue) + startAnimating() + } + } + + /// The repeat count. The animated image will keep animate until it the loop count reaches this value. + /// Setting this value to another one will reset current animation. + /// + /// Default is `.infinite`, which means the animation will last forever. + public var repeatCount = RepeatCount.infinite { + didSet { + if oldValue != repeatCount { + reset() + setNeedsDisplay() + layer.setNeedsDisplay() + } + } + } + + /// Delegate of this `AnimatedImageView` object. See `AnimatedImageViewDelegate` protocol for more. + public weak var delegate: AnimatedImageViewDelegate? + + /// The `Animator` instance that holds the frames of a specific image in memory. + public private(set) var animator: Animator? + + // MARK: - Private property + // Dispatch queue used for preloading images. + private lazy var preloadQueue: DispatchQueue = { + return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue") + }() + + // A flag to avoid invalidating the displayLink on deinit if it was never created, because displayLink is so lazy. + private var isDisplayLinkInitialized: Bool = false + + // A display link that keeps calling the `updateFrame` method on every screen refresh. + private lazy var displayLink: CADisplayLink = { + isDisplayLinkInitialized = true + let displayLink = CADisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate)) + displayLink.add(to: .main, forMode: runLoopMode) + displayLink.isPaused = true + return displayLink + }() + + // MARK: - Override + override open var image: KFCrossPlatformImage? { + didSet { + if image != oldValue { + reset() + } + setNeedsDisplay() + layer.setNeedsDisplay() + } + } + + open override var isHighlighted: Bool { + get { + super.isHighlighted + } + set { + // Highlighted image is unsupported for animated images. + // See https://github.com/onevcat/Kingfisher/issues/1679 + if displayLink.isPaused { + super.isHighlighted = newValue + } + } + } + + deinit { + if isDisplayLinkInitialized { + displayLink.invalidate() + } + } + + override open var isAnimating: Bool { + if isDisplayLinkInitialized { + return !displayLink.isPaused + } else { + return super.isAnimating + } + } + + /// Starts the animation. + override open func startAnimating() { + guard !isAnimating else { return } + guard let animator = animator else { return } + guard !animator.isReachMaxRepeatCount else { return } + + displayLink.isPaused = false + } + + /// Stops the animation. + override open func stopAnimating() { + super.stopAnimating() + if isDisplayLinkInitialized { + displayLink.isPaused = true + } + } + + override open func display(_ layer: CALayer) { + layer.contents = animator?.currentFrameImage?.cgImage ?? image?.cgImage + } + + override open func didMoveToWindow() { + super.didMoveToWindow() + didMove() + } + + override open func didMoveToSuperview() { + super.didMoveToSuperview() + didMove() + } + + // This is for back compatibility that using regular `UIImageView` to show animated image. + override func shouldPreloadAllAnimation() -> Bool { + return false + } + + // Reset the animator. + private func reset() { + animator = nil + if let image = image, let imageSource = image.kf.imageSource { + let targetSize = bounds.scaled(UIScreen.main.scale).size + let animator = Animator( + imageSource: imageSource, + contentMode: contentMode, + size: targetSize, + imageSize: image.kf.size, + imageScale: image.kf.scale, + framePreloadCount: framePreloadCount, + repeatCount: repeatCount, + preloadQueue: preloadQueue) + animator.delegate = self + animator.needsPrescaling = needsPrescaling + animator.backgroundDecode = backgroundDecode + animator.prepareFramesAsynchronously() + self.animator = animator + } + didMove() + } + + private func didMove() { + if autoPlayAnimatedImage && animator != nil { + if let _ = superview, let _ = window { + startAnimating() + } else { + stopAnimating() + } + } + } + + /// Update the current frame with the displayLink duration. + private func updateFrameIfNeeded() { + guard let animator = animator else { + return + } + + guard !animator.isFinished else { + stopAnimating() + delegate?.animatedImageViewDidFinishAnimating(self) + return + } + + let duration: CFTimeInterval + + // CA based display link is opt-out from ProMotion by default. + // So the duration and its FPS might not match. + // See [#718](https://github.com/onevcat/Kingfisher/issues/718) + // By setting CADisableMinimumFrameDuration to YES in Info.plist may + // cause the preferredFramesPerSecond being 0 + let preferredFramesPerSecond = displayLink.preferredFramesPerSecond + if preferredFramesPerSecond == 0 { + duration = displayLink.duration + } else { + // Some devices (like iPad Pro 10.5) will have a different FPS. + duration = 1.0 / TimeInterval(preferredFramesPerSecond) + } + + animator.shouldChangeFrame(with: duration) { [weak self] hasNewFrame in + if hasNewFrame { + self?.layer.setNeedsDisplay() + } + } + } +} + +protocol AnimatorDelegate: AnyObject { + func animator(_ animator: AnimatedImageView.Animator, didPlayAnimationLoops count: UInt) +} + +extension AnimatedImageView: AnimatorDelegate { + func animator(_ animator: Animator, didPlayAnimationLoops count: UInt) { + delegate?.animatedImageView(self, didPlayAnimationLoops: count) + } +} + +extension AnimatedImageView { + + // Represents a single frame in a GIF. + struct AnimatedFrame { + + // The image to display for this frame. Its value is nil when the frame is removed from the buffer. + let image: UIImage? + + // The duration that this frame should remain active. + let duration: TimeInterval + + // A placeholder frame with no image assigned. + // Used to replace frames that are no longer needed in the animation. + var placeholderFrame: AnimatedFrame { + return AnimatedFrame(image: nil, duration: duration) + } + + // Whether this frame instance contains an image or not. + var isPlaceholder: Bool { + return image == nil + } + + // Returns a new instance from an optional image. + // + // - parameter image: An optional `UIImage` instance to be assigned to the new frame. + // - returns: An `AnimatedFrame` instance. + func makeAnimatedFrame(image: UIImage?) -> AnimatedFrame { + return AnimatedFrame(image: image, duration: duration) + } + } +} + +extension AnimatedImageView { + + // MARK: - Animator + + /// An animator which used to drive the data behind `AnimatedImageView`. + public class Animator { + private let size: CGSize + + private let imageSize: CGSize + private let imageScale: CGFloat + + /// The maximum count of image frames that needs preload. + public let maxFrameCount: Int + + private let imageSource: CGImageSource + private let maxRepeatCount: RepeatCount + + private let maxTimeStep: TimeInterval = 1.0 + private let animatedFrames = SafeArray() + private var frameCount = 0 + private var timeSinceLastFrameChange: TimeInterval = 0.0 + private var currentRepeatCount: UInt = 0 + + var isFinished: Bool = false + + var needsPrescaling = true + + var backgroundDecode = true + + weak var delegate: AnimatorDelegate? + + // Total duration of one animation loop + var loopDuration: TimeInterval = 0 + + /// The image of the current frame. + public var currentFrameImage: UIImage? { + return frame(at: currentFrameIndex) + } + + /// The duration of the current active frame duration. + public var currentFrameDuration: TimeInterval { + return duration(at: currentFrameIndex) + } + + /// The index of the current animation frame. + public internal(set) var currentFrameIndex = 0 { + didSet { + previousFrameIndex = oldValue + } + } + + var previousFrameIndex = 0 { + didSet { + preloadQueue.async { + self.updatePreloadedFrames() + } + } + } + + var isReachMaxRepeatCount: Bool { + switch maxRepeatCount { + case .once: + return currentRepeatCount >= 1 + case .finite(let maxCount): + return currentRepeatCount >= maxCount + case .infinite: + return false + } + } + + /// Whether the current frame is the last frame or not in the animation sequence. + public var isLastFrame: Bool { + return currentFrameIndex == frameCount - 1 + } + + var preloadingIsNeeded: Bool { + return maxFrameCount < frameCount - 1 + } + + var contentMode = UIView.ContentMode.scaleToFill + + private lazy var preloadQueue: DispatchQueue = { + return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue") + }() + + /// Creates an animator with image source reference. + /// + /// - Parameters: + /// - source: The reference of animated image. + /// - mode: Content mode of the `AnimatedImageView`. + /// - size: Size of the `AnimatedImageView`. + /// - imageSize: Size of the `KingfisherWrapper`. + /// - imageScale: Scale of the `KingfisherWrapper`. + /// - count: Count of frames needed to be preloaded. + /// - repeatCount: The repeat count should this animator uses. + /// - preloadQueue: Dispatch queue used for preloading images. + init(imageSource source: CGImageSource, + contentMode mode: UIView.ContentMode, + size: CGSize, + imageSize: CGSize, + imageScale: CGFloat, + framePreloadCount count: Int, + repeatCount: RepeatCount, + preloadQueue: DispatchQueue) { + self.imageSource = source + self.contentMode = mode + self.size = size + self.imageSize = imageSize + self.imageScale = imageScale + self.maxFrameCount = count + self.maxRepeatCount = repeatCount + self.preloadQueue = preloadQueue + + GraphicsContext.begin(size: imageSize, scale: imageScale) + } + + deinit { + resetAnimatedFrames() + GraphicsContext.end() + } + + /// Gets the image frame of a given index. + /// - Parameter index: The index of desired image. + /// - Returns: The decoded image at the frame. `nil` if the index is out of bound or the image is not yet loaded. + public func frame(at index: Int) -> KFCrossPlatformImage? { + return animatedFrames[index]?.image + } + + public func duration(at index: Int) -> TimeInterval { + return animatedFrames[index]?.duration ?? .infinity + } + + func prepareFramesAsynchronously() { + frameCount = Int(CGImageSourceGetCount(imageSource)) + animatedFrames.reserveCapacity(frameCount) + preloadQueue.async { [weak self] in + self?.setupAnimatedFrames() + } + } + + func shouldChangeFrame(with duration: CFTimeInterval, handler: (Bool) -> Void) { + incrementTimeSinceLastFrameChange(with: duration) + + if currentFrameDuration > timeSinceLastFrameChange { + handler(false) + } else { + resetTimeSinceLastFrameChange() + incrementCurrentFrameIndex() + handler(true) + } + } + + private func setupAnimatedFrames() { + resetAnimatedFrames() + + var duration: TimeInterval = 0 + + (0.. maxFrameCount { return } + animatedFrames[index] = animatedFrames[index]?.makeAnimatedFrame(image: loadFrame(at: index)) + } + + self.loopDuration = duration + } + + private func resetAnimatedFrames() { + animatedFrames.removeAll() + } + + private func loadFrame(at index: Int) -> UIImage? { + let resize = needsPrescaling && size != .zero + let options: [CFString: Any]? + if resize { + options = [ + kCGImageSourceCreateThumbnailFromImageIfAbsent: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height) + ] + } else { + options = nil + } + + guard let cgImage = CGImageSourceCreateImageAtIndex(imageSource, index, options as CFDictionary?) else { + return nil + } + + let image = KFCrossPlatformImage(cgImage: cgImage) + + guard let context = GraphicsContext.current(size: imageSize, scale: imageScale, inverting: true, cgImage: cgImage) else { + return image + } + + return backgroundDecode ? image.kf.decoded(on: context) : image + } + + private func updatePreloadedFrames() { + guard preloadingIsNeeded else { + return + } + + animatedFrames[previousFrameIndex] = animatedFrames[previousFrameIndex]?.placeholderFrame + + preloadIndexes(start: currentFrameIndex).forEach { index in + guard let currentAnimatedFrame = animatedFrames[index] else { return } + if !currentAnimatedFrame.isPlaceholder { return } + animatedFrames[index] = currentAnimatedFrame.makeAnimatedFrame(image: loadFrame(at: index)) + } + } + + private func incrementCurrentFrameIndex() { + let wasLastFrame = isLastFrame + currentFrameIndex = increment(frameIndex: currentFrameIndex) + if isLastFrame { + currentRepeatCount += 1 + if isReachMaxRepeatCount { + isFinished = true + + // Notify the delegate here because the animation is stopping. + delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount) + } + } else if wasLastFrame { + + // Notify the delegate that the loop completed + delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount) + } + } + + private func incrementTimeSinceLastFrameChange(with duration: TimeInterval) { + timeSinceLastFrameChange += min(maxTimeStep, duration) + } + + private func resetTimeSinceLastFrameChange() { + timeSinceLastFrameChange -= currentFrameDuration + } + + private func increment(frameIndex: Int, by value: Int = 1) -> Int { + return (frameIndex + value) % frameCount + } + + private func preloadIndexes(start index: Int) -> [Int] { + let nextIndex = increment(frameIndex: index) + let lastIndex = increment(frameIndex: index, by: maxFrameCount) + + if lastIndex >= nextIndex { + return [Int](nextIndex...lastIndex) + } else { + return [Int](nextIndex.. { + private var array: Array = [] + private let lock = NSLock() + + subscript(index: Int) -> Element? { + get { + lock.lock() + defer { lock.unlock() } + return array.indices ~= index ? array[index] : nil + } + + set { + lock.lock() + defer { lock.unlock() } + if let newValue = newValue, array.indices ~= index { + array[index] = newValue + } + } + } + + var count : Int { + lock.lock() + defer { lock.unlock() } + return array.count + } + + func reserveCapacity(_ count: Int) { + lock.lock() + defer { lock.unlock() } + array.reserveCapacity(count) + } + + func append(_ element: Element) { + lock.lock() + defer { lock.unlock() } + array += [element] + } + + func removeAll() { + lock.lock() + defer { lock.unlock() } + array = [] + } +} +#endif +#endif diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Views/Indicator.swift b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Views/Indicator.swift new file mode 100644 index 0000000..a3fa5a4 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Views/Indicator.swift @@ -0,0 +1,231 @@ +// +// Indicator.swift +// Kingfisher +// +// Created by João D. Moreira on 30/08/16. +// +// Copyright (c) 2019 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if !os(watchOS) + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +public typealias IndicatorView = NSView +#else +import UIKit +public typealias IndicatorView = UIView +#endif + +/// Represents the activity indicator type which should be added to +/// an image view when an image is being downloaded. +/// +/// - none: No indicator. +/// - activity: Uses the system activity indicator. +/// - image: Uses an image as indicator. GIF is supported. +/// - custom: Uses a custom indicator. The type of associated value should conform to the `Indicator` protocol. +public enum IndicatorType { + /// No indicator. + case none + /// Uses the system activity indicator. + case activity + /// Uses an image as indicator. GIF is supported. + case image(imageData: Data) + /// Uses a custom indicator. The type of associated value should conform to the `Indicator` protocol. + case custom(indicator: Indicator) +} + +/// An indicator type which can be used to show the download task is in progress. +public protocol Indicator { + + /// Called when the indicator should start animating. + func startAnimatingView() + + /// Called when the indicator should stop animating. + func stopAnimatingView() + + /// Center offset of the indicator. Kingfisher will use this value to determine the position of + /// indicator in the super view. + var centerOffset: CGPoint { get } + + /// The indicator view which would be added to the super view. + var view: IndicatorView { get } + + /// The size strategy used when adding the indicator to image view. + /// - Parameter imageView: The super view of indicator. + func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy +} + +public enum IndicatorSizeStrategy { + case intrinsicSize + case full + case size(CGSize) +} + +extension Indicator { + + /// Default implementation of `centerOffset` of `Indicator`. The default value is `.zero`, means that there is + /// no offset for the indicator view. + public var centerOffset: CGPoint { return .zero } + + /// Default implementation of `centerOffset` of `Indicator`. The default value is `.full`, means that the indicator + /// will pin to the same height and width as the image view. + public func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy { + return .full + } +} + +// Displays a NSProgressIndicator / UIActivityIndicatorView +final class ActivityIndicator: Indicator { + + #if os(macOS) + private let activityIndicatorView: NSProgressIndicator + #else + private let activityIndicatorView: UIActivityIndicatorView + #endif + private var animatingCount = 0 + + var view: IndicatorView { + return activityIndicatorView + } + + func startAnimatingView() { + if animatingCount == 0 { + #if os(macOS) + activityIndicatorView.startAnimation(nil) + #else + activityIndicatorView.startAnimating() + #endif + activityIndicatorView.isHidden = false + } + animatingCount += 1 + } + + func stopAnimatingView() { + animatingCount = max(animatingCount - 1, 0) + if animatingCount == 0 { + #if os(macOS) + activityIndicatorView.stopAnimation(nil) + #else + activityIndicatorView.stopAnimating() + #endif + activityIndicatorView.isHidden = true + } + } + + func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy { + return .intrinsicSize + } + + init() { + #if os(macOS) + activityIndicatorView = NSProgressIndicator(frame: CGRect(x: 0, y: 0, width: 16, height: 16)) + activityIndicatorView.controlSize = .small + activityIndicatorView.style = .spinning + #else + let indicatorStyle: UIActivityIndicatorView.Style + + #if os(tvOS) + if #available(tvOS 13.0, *) { + indicatorStyle = UIActivityIndicatorView.Style.large + } else { + indicatorStyle = UIActivityIndicatorView.Style.white + } + #else + if #available(iOS 13.0, * ) { + indicatorStyle = UIActivityIndicatorView.Style.medium + } else { + indicatorStyle = UIActivityIndicatorView.Style.gray + } + #endif + + activityIndicatorView = UIActivityIndicatorView(style: indicatorStyle) + #endif + } +} + +#if canImport(UIKit) +extension UIActivityIndicatorView.Style { + #if compiler(>=5.1) + #else + static let large = UIActivityIndicatorView.Style.white + #if !os(tvOS) + static let medium = UIActivityIndicatorView.Style.gray + #endif + #endif +} +#endif + +// MARK: - ImageIndicator +// Displays an ImageView. Supports gif +final class ImageIndicator: Indicator { + private let animatedImageIndicatorView: KFCrossPlatformImageView + + var view: IndicatorView { + return animatedImageIndicatorView + } + + init?( + imageData data: Data, + processor: ImageProcessor = DefaultImageProcessor.default, + options: KingfisherParsedOptionsInfo? = nil) + { + var options = options ?? KingfisherParsedOptionsInfo(nil) + // Use normal image view to show animations, so we need to preload all animation data. + if !options.preloadAllAnimationData { + options.preloadAllAnimationData = true + } + + guard let image = processor.process(item: .data(data), options: options) else { + return nil + } + + animatedImageIndicatorView = KFCrossPlatformImageView() + animatedImageIndicatorView.image = image + + #if os(macOS) + // Need for gif to animate on macOS + animatedImageIndicatorView.imageScaling = .scaleNone + animatedImageIndicatorView.canDrawSubviewsIntoLayer = true + #else + animatedImageIndicatorView.contentMode = .center + #endif + } + + func startAnimatingView() { + #if os(macOS) + animatedImageIndicatorView.animates = true + #else + animatedImageIndicatorView.startAnimating() + #endif + animatedImageIndicatorView.isHidden = false + } + + func stopAnimatingView() { + #if os(macOS) + animatedImageIndicatorView.animates = false + #else + animatedImageIndicatorView.stopAnimating() + #endif + animatedImageIndicatorView.isHidden = true + } +} + +#endif diff --git a/jaem/week7/CatStaGram/Pods/Manifest.lock b/jaem/week7/CatStaGram/Pods/Manifest.lock new file mode 100644 index 0000000..1046ca6 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Manifest.lock @@ -0,0 +1,20 @@ +PODS: + - Alamofire (5.6.1) + - Kingfisher (7.2.2) + +DEPENDENCIES: + - Alamofire + - Kingfisher (~> 7.0) + +SPEC REPOS: + trunk: + - Alamofire + - Kingfisher + +SPEC CHECKSUMS: + Alamofire: 87bd8c952f9a4454320fce00d9cc3de57bcadaf5 + Kingfisher: 184d4d1a8c36666e663caf8e08abe87898595c53 + +PODFILE CHECKSUM: fd492e0a81b684bf01518e165eb2e09de28a0ee6 + +COCOAPODS: 1.11.3 diff --git a/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/project.pbxproj b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b22863b --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/project.pbxproj @@ -0,0 +1,1183 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 005B319B494ED2DAA239B9939A504DFC /* Alamofire-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D167A016457E1A5BD5B9A73C4FF9434 /* Alamofire-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 00BEA6029C428FEE644AC3D42AD83282 /* ImagePrefetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 240168000C008585397987F341FC9AFA /* ImagePrefetcher.swift */; }; + 0285857A24F66E925987A5876F0BE63B /* ImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 597967B6CDA2E72751463CF56198E927 /* ImageDataProvider.swift */; }; + 045DE6EBF9B2F63F60F5BE60C1198E06 /* RedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 499955C35F6D4A8F96E940FF129FF498 /* RedirectHandler.swift */; }; + 04A896288CE3A59B530250337A5F8362 /* Result+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3F986AF94568CEEB8AF631C72F03B2 /* Result+Alamofire.swift */; }; + 0510E8EA51914CB2176AD0F173937FAB /* KFImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26990E9D251C57CAB34BC5C9EC37033 /* KFImageRenderer.swift */; }; + 05228565AAA7FCED4BAFB2B7EF71D53D /* KingfisherOptionsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8B2E44706F2E23444FC30F3BAEE3DC /* KingfisherOptionsInfo.swift */; }; + 059639E700DEFAEF08F56484E5F67BE7 /* NSButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6E93449DD3CC778DD5D443C7780D895 /* NSButton+Kingfisher.swift */; }; + 082EDC820D76DF95C71A5018112DE512 /* UIButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9B50C2ED49A8E73A4D64C205173820 /* UIButton+Kingfisher.swift */; }; + 0ED8FBFD9A86D21BF69137EC9350E575 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9ECAD406B234DD522B04457BCADA54 /* SessionDelegate.swift */; }; + 0F4037DBF307AC8058BD0A3D35C7E7E9 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */; }; + 1185A2B40E14F2FCBC761FC99777CAD8 /* ExtensionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365099CC49560F0DAFBF90D1A7FBFD24 /* ExtensionHelpers.swift */; }; + 122980E44B15C64CF0B14DC94D7EB5C9 /* Kingfisher-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 6C321B1DFC61DFB5E1EB0BF63C4E7B05 /* Kingfisher-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 18E839CC4FD7F1CD252F4BCA4C70BED1 /* Pods-CatStaGram-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = EE2747566D1A67FB4BBC1883CC9ACE4E /* Pods-CatStaGram-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1952485AFF7A1BCCA4D4B142E82FE627 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06442349E67A8F1944EBFB63B46B406E /* AnimatedImageView.swift */; }; + 1976BB7D7E26A12E29283E71154B63B3 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B42FDB98681E0F2B54A53C6263FC0FB7 /* SessionDelegate.swift */; }; + 1AA89F327105C026976BF6E831B193A2 /* ImageBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE0A9D42161F3A6512AEE4A7F63B4A0 /* ImageBinder.swift */; }; + 1EE44196E7BCE57AD96A2C751651EF40 /* AlamofireExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72447455A0B451F260AE94E750E590D7 /* AlamofireExtended.swift */; }; + 1FD2928BC156D990D68B105F518C60B6 /* MemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2601AB15F49167B95C5DDC2FBC7B78 /* MemoryStorage.swift */; }; + 1FE693B5ACC6AD7320CEFC20B64546E4 /* KingfisherManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7AD2807A19FC11299041D8A8777267 /* KingfisherManager.swift */; }; + 1FEE89BF952BE7ACA46E642DA2E48CA2 /* DiskStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D362E01704CD82D45E9C7857FB20AC1E /* DiskStorage.swift */; }; + 22216C300C763044344B9DBF97317E63 /* RetryStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F40A9A287AE4680FD4A041AAD0C5ED /* RetryStrategy.swift */; }; + 22BD1346F66BFCB129AAA44EEF322AC9 /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BFD9705B9A22E25BBBD0A608C90CCC /* Resource.swift */; }; + 243D7CFE1D56ED80ACB2B3E71B4CB603 /* AuthenticationChallengeResponsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DBC7F03013C95C65EE295C4DAC2A64 /* AuthenticationChallengeResponsable.swift */; }; + 25FC036BEA33CAB5D80F5A41644535D3 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCDFE9C10374511CCC375D2E99C812E /* Storage.swift */; }; + 29FF13E23FD52E46D30530549410AD7C /* ImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1FA3BADCA4576957512EC8D60D2B76 /* ImageTransition.swift */; }; + 2BE89C24BFD3FB663E37C607C289B3B6 /* RedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C1BAC7555E3D0AC24A397BB028A3CEC /* RedirectHandler.swift */; }; + 2CBE3651CA006E19F5D64A2DE9B9A028 /* CachedResponseHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A117735F022F30BB9DC641136E51C9 /* CachedResponseHandler.swift */; }; + 2CCD13099063CD560E3067BD132914FA /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC710AFB6066D8535108054FD1115E94 /* Notifications.swift */; }; + 33A7D0F2D03004CE256A75E03DF33C70 /* RetryPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 715993D57B4975C3CF94C6BC997595C6 /* RetryPolicy.swift */; }; + 3AD5DBB915C2623991F7DBACD173BBB4 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */; }; + 3AF7DB9AEFF47F1F7F91AF28440E4AC6 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A723238162323CE3B380FA8E894C865 /* Filter.swift */; }; + 3C4059621E23842C19D4EB5D35B41989 /* Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67030433EFF4B0333EC8811BC7EC47D6 /* Validation.swift */; }; + 420C200A05BB29E1D299D1BADE9139D2 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 872D7EFA572ECEE8EF993C27196E16DD /* CFNetwork.framework */; }; + 457BE444ED617FA7D6851D6DAA9D7234 /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B347AF8E0B52CE83F1B9A8F51F1AE31 /* Delegate.swift */; }; + 46A64A43AFA057B6B63C8F0C12F509B4 /* Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = B114DBD20B001B2A9D93F8F8C47903B6 /* Combine.swift */; }; + 4DCA9775E5CCF599460BDB46E77F6FA4 /* Kingfisher-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 6835FBF931D5C31EFFF83ED559436CDE /* Kingfisher-dummy.m */; }; + 4F37E521D341C47CE73DDCF21BA95A52 /* KingfisherError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3CD3E23CCF124B01CA690E27E72D1B9 /* KingfisherError.swift */; }; + 506128E1CC424E40E2691546D9547549 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C6C8F94FCB9064F02B48B607AB69962 /* Placeholder.swift */; }; + 509490FB1D30FEC59AE4BC21AEEBB7BB /* RequestModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C264879E6B16EA9E07F073E24220A232 /* RequestModifier.swift */; }; + 55AABB1FB38F61A3369ACC555FF3046D /* Alamofire-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D13E09327BDC573DE5AAFE079172AB9 /* Alamofire-dummy.m */; }; + 57FC31B14C753B5C63CEF00560F8A6EF /* SizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CBA959C728F5B6931C11DE61853D88 /* SizeExtensions.swift */; }; + 582D59E0D2EF62E0575933C99B393704 /* GraphicsContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D370E0D48C1925AC14D155BEDF3DC7D4 /* GraphicsContext.swift */; }; + 59BC9047F4BEBBC06235608D974E230D /* NSTextAttachment+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EBE61AE143E2DB816492EB19AA1651F /* NSTextAttachment+Kingfisher.swift */; }; + 5ADB30DD9A03859018550A999ACB0652 /* KFImageOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BAF59EA7A7DD980421989D2A205E6B0 /* KFImageOptions.swift */; }; + 5E27DD292D3A55657712DD7AFA7B8FCA /* KFImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9566B136FA420BDAAE8B272AE10CFE3E /* KFImage.swift */; }; + 5F852F38CBC282496CCBE37C51324B2F /* ImageProgressive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7366FB0DEF319ED32BAF9DE5654C8DB7 /* ImageProgressive.swift */; }; + 68FB2DCB4C77DBCAF9A6037E470F2BDE /* ParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 249A902C9828CB270329EFFAFAC1843A /* ParameterEncoding.swift */; }; + 70FEC06F54286257E1BA1ECA0C99198D /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16E37EF24D9E916BB47E43D8E954D3AF /* Image.swift */; }; + 7483E5327027263F7E4B94A2997191C4 /* AuthenticationInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B7F2B2CCF604EF10790364BAB06391 /* AuthenticationInterceptor.swift */; }; + 75966A9262648D4647D764E3E76BC6AC /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB099A6B514875F22EF98798B56B4BA5 /* Response.swift */; }; + 7930C94414B4C661867AC4FBE82E996C /* URLEncodedFormEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072684B1154141462994E3AD2ACD3606 /* URLEncodedFormEncoder.swift */; }; + 7B068137A8925891446203B5D3D6A4ED /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 872D7EFA572ECEE8EF993C27196E16DD /* CFNetwork.framework */; }; + 7C7418FF01DD7BB909719682B634A8A5 /* SessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C323A988742445270C1D6A4DD40ED07 /* SessionDataTask.swift */; }; + 7E02F5B62BE00E97847DF549FFED2490 /* HTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FAB3233CC80F28F7E66DF4DC9F2FA /* HTTPHeaders.swift */; }; + 7F1BB526AAE3ECDCE90127D9D0E10261 /* StringEncoding+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = D942B0884573FD857CFB17A94E6A5807 /* StringEncoding+Alamofire.swift */; }; + 7FE695DA8EE7FF1286556E06B692009B /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EE41735C363063D401BC89633A455BE /* MultipartFormData.swift */; }; + 7FFE4021A4F14124342AD41CE1117B3E /* KFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86DDDE6D887ED6DFE35529D771930C65 /* KFAnimatedImage.swift */; }; + 80738D8956C9987CCCEDF551961E5069 /* ImageDataProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D911DA9899C96EA4408220F4754219 /* ImageDataProcessor.swift */; }; + 808C960C82D708FC1A42C581D6CB4940 /* URLSessionConfiguration+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7407E80368EF1CE2A091B4F0F8F81FF /* URLSessionConfiguration+Alamofire.swift */; }; + 81B8D2B7CEB25C2448B0BC9B33591A65 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1939B0AEE888F2EB258D6832523F67C /* Session.swift */; }; + 824D816B1EE404F2DD400EE678695CBE /* ResponseSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C32A86B99F0C8B9E42B3D9C7F9730889 /* ResponseSerialization.swift */; }; + 881A35B28D93C56E46E305F6138B1A76 /* ImageDownloaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077E33C20C9BD4FAF66AE7A6EC1657F4 /* ImageDownloaderDelegate.swift */; }; + 8D75FC8D7476C9674234F39F1A820D8C /* URLConvertible+URLRequestConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404743B9B61FE5357078AFD049FC6B45 /* URLConvertible+URLRequestConvertible.swift */; }; + 99D058E53EFEE3AC4857CDE3DBA5C004 /* ParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A1EDFD24518B3F930192EDAE086842 /* ParameterEncoder.swift */; }; + 9B0A78AC22E7EDA755F51D86527E2D9C /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496EC73E3893F724BA57D993B154BF49 /* Source.swift */; }; + 9C9030DEDB0DF955B16FE08C50892D57 /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1529C2E8B334710FA50E4C9A7153006A /* Concurrency.swift */; }; + 9F5FE22DA95B66B8DC21CB13BE25EC9B /* WKInterfaceImage+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C444AA66DFF06EB1D1D714FE5939A17 /* WKInterfaceImage+Kingfisher.swift */; }; + A29100AA1876DDEFF6F54694A51FDB0E /* NetworkReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A043F9EF1DF150712ECDD2FB7CD2E46B /* NetworkReachabilityManager.swift */; }; + A316388A35648CB2987E761771456087 /* KFOptionsSetter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011604690A5B598427758F666A2F13C8 /* KFOptionsSetter.swift */; }; + A39D3555EC8B45B7D6B9505DDAF0F117 /* Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7A8F9A880D8CF92488BF36640F2DBBE /* Kingfisher.swift */; }; + A53BDE589BDD6483F3EEDCE5EA1DCCD3 /* Protected.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFFE766A6E0F3A621DFD9487DA3CAF7E /* Protected.swift */; }; + A88A844D5356E1690E445024CB796E09 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7006AE0546B679604D685DCA785C542C /* Result.swift */; }; + B25E07EA645911443A38DA1E68166156 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B7F4B3A6C95F8600D69F851451D63F6 /* Box.swift */; }; + B3658C29BBDE1033F6269A92E612CB30 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0341B04B64485596391C33DD9360D74E /* Request.swift */; }; + B6C05F9D9AF254B332EAAB3064F7F147 /* Pods-CatStaGram-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7DBB0D476F47E7A38CE0EDF85193CF41 /* Pods-CatStaGram-dummy.m */; }; + B704B198B9B520D449260877E300D821 /* ServerTrustEvaluation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04303942CB47C4C340CF398F8653BB98 /* ServerTrustEvaluation.swift */; }; + BC0ECA8F22DEDE8886E189CD0EAA1197 /* URLRequest+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB31688140758856323E0B3187769487 /* URLRequest+Alamofire.swift */; }; + BD382E78580D295D10100678D4F66A76 /* String+MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23963C2629645E475AB2931F62E451C7 /* String+MD5.swift */; }; + C9392394543C174FFBF84B243D562853 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */; }; + CD7AC3E1C98EA54F7C05C36C52805220 /* CacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC3A37B1FA2F4841F5E8CC3FC102A19 /* CacheSerializer.swift */; }; + CEBFFEED65D877702B2F36102528CF6D /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932C67B305CF8C8942FEBD2375BE2E1F /* EventMonitor.swift */; }; + D0EA90FBF83350C49E6EF6C8A98D6F00 /* AFError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80270ADB4ECE74E17A7DB57236035920 /* AFError.swift */; }; + D603AA58EF97D461A57B2B1BCB883868 /* AVAssetImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74729EFA464A2B107050ED713601AB8 /* AVAssetImageDataProvider.swift */; }; + D6B4751CED01D53E4A1B6A571AAA2F83 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B56F1736D1127A32614C8CBBEF3E5F6 /* HTTPMethod.swift */; }; + DA34899BEF0668D76CBCE8C4CE47B97B /* RequestTaskMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0DD802295003561B6C5741521D716BC /* RequestTaskMap.swift */; }; + DAFC6CE6321395CF4523DD66DADBB9BA /* ImageDrawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7933A5F1F8BD20A48039AA759ADB465E /* ImageDrawing.swift */; }; + DBB8088E14A2ADEDB1CD840BAC835267 /* CPListItem+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142C2CAF3091DF534D3E7B4AAA4DFC0B /* CPListItem+Kingfisher.swift */; }; + DD72DC30CF19FFC81AB19CD0B074000D /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE76C5003FE080CF8820CA18C434EA3 /* ImageDownloader.swift */; }; + DD902FE8D6824681C929D028655AE121 /* RequestInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6599B0D2C1F4B263D4D859853F0F348A /* RequestInterceptor.swift */; }; + DE532EF7D50A9CF68587DAD4C1A02BD7 /* FormatIndicatedCacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8676B059D0C43C5A2FC81E0F55AD63D7 /* FormatIndicatedCacheSerializer.swift */; }; + DF4563832C19B8582C810BF502A5CA29 /* KF.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660CF05D9FA7E4371D6E6A45ED8EE01F /* KF.swift */; }; + DFCDE4638265B4CCD494ECA5D560DBEE /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5723DEFD772E79C36C19B268F7643CE0 /* Indicator.swift */; }; + E54654D504A42C24F284A68F87F7671D /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20EDDB802D0AEBA579A2FAC5B1B6BDE /* OperationQueue+Alamofire.swift */; }; + E5B664771063F1A9A372519A8466860B /* ImageView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 985C3C905A92749F5FB0325692FBF789 /* ImageView+Kingfisher.swift */; }; + E6D6C7D5E458A05CC736C340F853E9F6 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C65DBD1C381FE1E4AA74F824E85F325 /* ImageFormat.swift */; }; + E719A3B025B9DACE693130120BD9B927 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52554E1C9731D5352FDE9E63F8C5466B /* Accelerate.framework */; }; + E9B4C89E7EB3B27D46AFCA452C3D426F /* MultipartUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BA6A6C6AEAC43638A2B248BBEA90EBD /* MultipartUpload.swift */; }; + EBB32304E8DD4BA115454E0050D47DED /* Runtime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CB85CAAF3329112CD2FFF9889E98FF7 /* Runtime.swift */; }; + ED0C8BA7560D7324587B353E0960479F /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE9B6094B04E75ACDBB73C3A194D690 /* ImageCache.swift */; }; + EEC150B66BCCD6C80FDA7E4D1975166B /* DispatchQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48BAA15044E6AD772F77A856FA5107F2 /* DispatchQueue+Alamofire.swift */; }; + EF9C4588CDA85AED8BBCF77451B2A35B /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95F29F7BBB1AA54AD679386E26C7740 /* ImageProcessor.swift */; }; + F17A4CA4664CABB331D39FE902E06843 /* Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76CC7FAE70D848424E201CB9C76CABD3 /* Alamofire.swift */; }; + F17B1F8F2B6580343025237455A29D61 /* TVMonogramView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4193FEA28CDEB3710C7C41537C8D5E2E /* TVMonogramView+Kingfisher.swift */; }; + F24021BDE9B42D604E3341CAD8E34759 /* GIFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF4F904B2EF1F1E836FADDE8C6B05DF /* GIFAnimatedImage.swift */; }; + F5414F8A5B40521D0E4AEEB28378CB49 /* ImageContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB10145B8616970B7B29BFF058CF910 /* ImageContext.swift */; }; + F54DE563418B1783D6EC491A0C3A05DB /* ImageModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07524CF920EFBC25C9A0C9B3E57AA118 /* ImageModifier.swift */; }; + F9537B023E24AC4A724E301F7E372491 /* KFImageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292F77B8C48397E58C30BC7A313423E4 /* KFImageProtocol.swift */; }; + FF09824309346665E2F1F7F5A45FB10F /* CallbackQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CCE8AF433C89864A9FE3C1F9C34A96 /* CallbackQueue.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 86507F3308AD3FD588A9636FBE2FE558 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E8022D22FAA6690B5E1C379C1BCE1491; + remoteInfo = Kingfisher; + }; + BADC5D608C035FEA12332200B1BEA3AA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EAAA1AD3A8A1B59AB91319EE40752C6D; + remoteInfo = Alamofire; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 011604690A5B598427758F666A2F13C8 /* KFOptionsSetter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFOptionsSetter.swift; path = Sources/General/KFOptionsSetter.swift; sourceTree = ""; }; + 0341B04B64485596391C33DD9360D74E /* Request.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Request.swift; path = Source/Request.swift; sourceTree = ""; }; + 04303942CB47C4C340CF398F8653BB98 /* ServerTrustEvaluation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServerTrustEvaluation.swift; path = Source/ServerTrustEvaluation.swift; sourceTree = ""; }; + 06442349E67A8F1944EBFB63B46B406E /* AnimatedImageView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AnimatedImageView.swift; path = Sources/Views/AnimatedImageView.swift; sourceTree = ""; }; + 072684B1154141462994E3AD2ACD3606 /* URLEncodedFormEncoder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = URLEncodedFormEncoder.swift; path = Source/URLEncodedFormEncoder.swift; sourceTree = ""; }; + 07524CF920EFBC25C9A0C9B3E57AA118 /* ImageModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageModifier.swift; path = Sources/Networking/ImageModifier.swift; sourceTree = ""; }; + 077E33C20C9BD4FAF66AE7A6EC1657F4 /* ImageDownloaderDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDownloaderDelegate.swift; path = Sources/Networking/ImageDownloaderDelegate.swift; sourceTree = ""; }; + 0B347AF8E0B52CE83F1B9A8F51F1AE31 /* Delegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Delegate.swift; path = Sources/Utility/Delegate.swift; sourceTree = ""; }; + 0B7F4B3A6C95F8600D69F851451D63F6 /* Box.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Box.swift; path = Sources/Utility/Box.swift; sourceTree = ""; }; + 0BC3A37B1FA2F4841F5E8CC3FC102A19 /* CacheSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CacheSerializer.swift; path = Sources/Cache/CacheSerializer.swift; sourceTree = ""; }; + 0C323A988742445270C1D6A4DD40ED07 /* SessionDataTask.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDataTask.swift; path = Sources/Networking/SessionDataTask.swift; sourceTree = ""; }; + 0D167A016457E1A5BD5B9A73C4FF9434 /* Alamofire-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Alamofire-umbrella.h"; sourceTree = ""; }; + 0D470C70EF958AF31663B789E7801669 /* Pods-CatStaGram-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-CatStaGram-acknowledgements.plist"; sourceTree = ""; }; + 142C2CAF3091DF534D3E7B4AAA4DFC0B /* CPListItem+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "CPListItem+Kingfisher.swift"; path = "Sources/Extensions/CPListItem+Kingfisher.swift"; sourceTree = ""; }; + 148FA9BD1508B6E7E220C9D9BF1928F8 /* Kingfisher-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Kingfisher-prefix.pch"; sourceTree = ""; }; + 1529C2E8B334710FA50E4C9A7153006A /* Concurrency.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Concurrency.swift; path = Source/Concurrency.swift; sourceTree = ""; }; + 16E37EF24D9E916BB47E43D8E954D3AF /* Image.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Image.swift; path = Sources/Image/Image.swift; sourceTree = ""; }; + 1986AFB6D3CAD62304F3FA85BE29D575 /* Kingfisher-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Kingfisher-Info.plist"; sourceTree = ""; }; + 1B8B2E44706F2E23444FC30F3BAEE3DC /* KingfisherOptionsInfo.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherOptionsInfo.swift; path = Sources/General/KingfisherOptionsInfo.swift; sourceTree = ""; }; + 1C1BAC7555E3D0AC24A397BB028A3CEC /* RedirectHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedirectHandler.swift; path = Sources/Networking/RedirectHandler.swift; sourceTree = ""; }; + 1C444AA66DFF06EB1D1D714FE5939A17 /* WKInterfaceImage+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "WKInterfaceImage+Kingfisher.swift"; path = "Sources/Extensions/WKInterfaceImage+Kingfisher.swift"; sourceTree = ""; }; + 1CB85CAAF3329112CD2FFF9889E98FF7 /* Runtime.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Runtime.swift; path = Sources/Utility/Runtime.swift; sourceTree = ""; }; + 1EBE61AE143E2DB816492EB19AA1651F /* NSTextAttachment+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSTextAttachment+Kingfisher.swift"; path = "Sources/Extensions/NSTextAttachment+Kingfisher.swift"; sourceTree = ""; }; + 21DBC7F03013C95C65EE295C4DAC2A64 /* AuthenticationChallengeResponsable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthenticationChallengeResponsable.swift; path = Sources/Networking/AuthenticationChallengeResponsable.swift; sourceTree = ""; }; + 23963C2629645E475AB2931F62E451C7 /* String+MD5.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "String+MD5.swift"; path = "Sources/Utility/String+MD5.swift"; sourceTree = ""; }; + 2397257AE270C74C5FF63E810A481FDE /* Pods-CatStaGram.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-CatStaGram.modulemap"; sourceTree = ""; }; + 240168000C008585397987F341FC9AFA /* ImagePrefetcher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImagePrefetcher.swift; path = Sources/Networking/ImagePrefetcher.swift; sourceTree = ""; }; + 249A902C9828CB270329EFFAFAC1843A /* ParameterEncoding.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ParameterEncoding.swift; path = Source/ParameterEncoding.swift; sourceTree = ""; }; + 24A6C9EC04946183C998CEC9FFF72EFD /* Alamofire-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Alamofire-prefix.pch"; sourceTree = ""; }; + 27F40A9A287AE4680FD4A041AAD0C5ED /* RetryStrategy.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RetryStrategy.swift; path = Sources/Networking/RetryStrategy.swift; sourceTree = ""; }; + 292F77B8C48397E58C30BC7A313423E4 /* KFImageProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageProtocol.swift; path = Sources/SwiftUI/KFImageProtocol.swift; sourceTree = ""; }; + 2BAF59EA7A7DD980421989D2A205E6B0 /* KFImageOptions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageOptions.swift; path = Sources/SwiftUI/KFImageOptions.swift; sourceTree = ""; }; + 2C309F94D8E35CA72C8F08ACB621D88F /* Pods-CatStaGram-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-CatStaGram-acknowledgements.markdown"; sourceTree = ""; }; + 2F3F986AF94568CEEB8AF631C72F03B2 /* Result+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Result+Alamofire.swift"; path = "Source/Result+Alamofire.swift"; sourceTree = ""; }; + 365099CC49560F0DAFBF90D1A7FBFD24 /* ExtensionHelpers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExtensionHelpers.swift; path = Sources/Utility/ExtensionHelpers.swift; sourceTree = ""; }; + 37D911DA9899C96EA4408220F4754219 /* ImageDataProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDataProcessor.swift; path = Sources/Networking/ImageDataProcessor.swift; sourceTree = ""; }; + 3C65DBD1C381FE1E4AA74F824E85F325 /* ImageFormat.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageFormat.swift; path = Sources/Image/ImageFormat.swift; sourceTree = ""; }; + 3D13E09327BDC573DE5AAFE079172AB9 /* Alamofire-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Alamofire-dummy.m"; sourceTree = ""; }; + 404743B9B61FE5357078AFD049FC6B45 /* URLConvertible+URLRequestConvertible.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URLConvertible+URLRequestConvertible.swift"; path = "Source/URLConvertible+URLRequestConvertible.swift"; sourceTree = ""; }; + 40CCE8AF433C89864A9FE3C1F9C34A96 /* CallbackQueue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CallbackQueue.swift; path = Sources/Utility/CallbackQueue.swift; sourceTree = ""; }; + 4193FEA28CDEB3710C7C41537C8D5E2E /* TVMonogramView+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "TVMonogramView+Kingfisher.swift"; path = "Sources/Extensions/TVMonogramView+Kingfisher.swift"; sourceTree = ""; }; + 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + 44A117735F022F30BB9DC641136E51C9 /* CachedResponseHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CachedResponseHandler.swift; path = Source/CachedResponseHandler.swift; sourceTree = ""; }; + 48BAA15044E6AD772F77A856FA5107F2 /* DispatchQueue+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "DispatchQueue+Alamofire.swift"; path = "Source/DispatchQueue+Alamofire.swift"; sourceTree = ""; }; + 496EC73E3893F724BA57D993B154BF49 /* Source.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Source.swift; path = Sources/General/ImageSource/Source.swift; sourceTree = ""; }; + 499955C35F6D4A8F96E940FF129FF498 /* RedirectHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedirectHandler.swift; path = Source/RedirectHandler.swift; sourceTree = ""; }; + 4C1FA3BADCA4576957512EC8D60D2B76 /* ImageTransition.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageTransition.swift; path = Sources/Image/ImageTransition.swift; sourceTree = ""; }; + 4EF4F904B2EF1F1E836FADDE8C6B05DF /* GIFAnimatedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GIFAnimatedImage.swift; path = Sources/Image/GIFAnimatedImage.swift; sourceTree = ""; }; + 52554E1C9731D5352FDE9E63F8C5466B /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Accelerate.framework; sourceTree = DEVELOPER_DIR; }; + 56B4F426A74516AD9DF8A17EA00CE61E /* Alamofire.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Alamofire.release.xcconfig; sourceTree = ""; }; + 5723DEFD772E79C36C19B268F7643CE0 /* Indicator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Indicator.swift; path = Sources/Views/Indicator.swift; sourceTree = ""; }; + 597967B6CDA2E72751463CF56198E927 /* ImageDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDataProvider.swift; path = Sources/General/ImageSource/ImageDataProvider.swift; sourceTree = ""; }; + 5B56F1736D1127A32614C8CBBEF3E5F6 /* HTTPMethod.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HTTPMethod.swift; path = Source/HTTPMethod.swift; sourceTree = ""; }; + 5D797E9A5C5782CE845840781FA1CC81 /* Alamofire */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Alamofire; path = Alamofire.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 630CD17355E726BB7AF8B6C6A581C2FE /* Alamofire.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Alamofire.debug.xcconfig; sourceTree = ""; }; + 6599B0D2C1F4B263D4D859853F0F348A /* RequestInterceptor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestInterceptor.swift; path = Source/RequestInterceptor.swift; sourceTree = ""; }; + 660CF05D9FA7E4371D6E6A45ED8EE01F /* KF.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KF.swift; path = Sources/General/KF.swift; sourceTree = ""; }; + 67030433EFF4B0333EC8811BC7EC47D6 /* Validation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Validation.swift; path = Source/Validation.swift; sourceTree = ""; }; + 680FB1C38EA9062A1098F656BF33DEA1 /* Pods-CatStaGram-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-CatStaGram-Info.plist"; sourceTree = ""; }; + 6835FBF931D5C31EFFF83ED559436CDE /* Kingfisher-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Kingfisher-dummy.m"; sourceTree = ""; }; + 6A723238162323CE3B380FA8E894C865 /* Filter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Filter.swift; path = Sources/Image/Filter.swift; sourceTree = ""; }; + 6BCDFE9C10374511CCC375D2E99C812E /* Storage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Storage.swift; path = Sources/Cache/Storage.swift; sourceTree = ""; }; + 6C321B1DFC61DFB5E1EB0BF63C4E7B05 /* Kingfisher-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Kingfisher-umbrella.h"; sourceTree = ""; }; + 6E5C70357A0F35A05639673633F51624 /* Kingfisher.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Kingfisher.debug.xcconfig; sourceTree = ""; }; + 7006AE0546B679604D685DCA785C542C /* Result.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Result.swift; path = Sources/Utility/Result.swift; sourceTree = ""; }; + 70D1A3E8DA5E73DFADB87E35404AAA05 /* Pods-CatStaGram.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-CatStaGram.debug.xcconfig"; sourceTree = ""; }; + 715993D57B4975C3CF94C6BC997595C6 /* RetryPolicy.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RetryPolicy.swift; path = Source/RetryPolicy.swift; sourceTree = ""; }; + 72447455A0B451F260AE94E750E590D7 /* AlamofireExtended.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AlamofireExtended.swift; path = Source/AlamofireExtended.swift; sourceTree = ""; }; + 7366FB0DEF319ED32BAF9DE5654C8DB7 /* ImageProgressive.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProgressive.swift; path = Sources/Image/ImageProgressive.swift; sourceTree = ""; }; + 73DC4358317E61D22885E415D01F8DFF /* Kingfisher.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Kingfisher.modulemap; sourceTree = ""; }; + 76CC7FAE70D848424E201CB9C76CABD3 /* Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Alamofire.swift; path = Source/Alamofire.swift; sourceTree = ""; }; + 792CB57DACF134EC2A6070CDDCF91F56 /* Pods-CatStaGram-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-CatStaGram-frameworks.sh"; sourceTree = ""; }; + 7933A5F1F8BD20A48039AA759ADB465E /* ImageDrawing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDrawing.swift; path = Sources/Image/ImageDrawing.swift; sourceTree = ""; }; + 7B7AD2807A19FC11299041D8A8777267 /* KingfisherManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherManager.swift; path = Sources/General/KingfisherManager.swift; sourceTree = ""; }; + 7C6C8F94FCB9064F02B48B607AB69962 /* Placeholder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Placeholder.swift; path = Sources/Image/Placeholder.swift; sourceTree = ""; }; + 7DBB0D476F47E7A38CE0EDF85193CF41 /* Pods-CatStaGram-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-CatStaGram-dummy.m"; sourceTree = ""; }; + 7EE41735C363063D401BC89633A455BE /* MultipartFormData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultipartFormData.swift; path = Source/MultipartFormData.swift; sourceTree = ""; }; + 80270ADB4ECE74E17A7DB57236035920 /* AFError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AFError.swift; path = Source/AFError.swift; sourceTree = ""; }; + 8676B059D0C43C5A2FC81E0F55AD63D7 /* FormatIndicatedCacheSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FormatIndicatedCacheSerializer.swift; path = Sources/Cache/FormatIndicatedCacheSerializer.swift; sourceTree = ""; }; + 86DDDE6D887ED6DFE35529D771930C65 /* KFAnimatedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFAnimatedImage.swift; path = Sources/SwiftUI/KFAnimatedImage.swift; sourceTree = ""; }; + 872D7EFA572ECEE8EF993C27196E16DD /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; }; + 8AE9B6094B04E75ACDBB73C3A194D690 /* ImageCache.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = Sources/Cache/ImageCache.swift; sourceTree = ""; }; + 8BA6A6C6AEAC43638A2B248BBEA90EBD /* MultipartUpload.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultipartUpload.swift; path = Source/MultipartUpload.swift; sourceTree = ""; }; + 932C67B305CF8C8942FEBD2375BE2E1F /* EventMonitor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = EventMonitor.swift; path = Source/EventMonitor.swift; sourceTree = ""; }; + 9566B136FA420BDAAE8B272AE10CFE3E /* KFImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImage.swift; path = Sources/SwiftUI/KFImage.swift; sourceTree = ""; }; + 95BFD9705B9A22E25BBBD0A608C90CCC /* Resource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Resource.swift; path = Sources/General/ImageSource/Resource.swift; sourceTree = ""; }; + 985C3C905A92749F5FB0325692FBF789 /* ImageView+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "ImageView+Kingfisher.swift"; path = "Sources/Extensions/ImageView+Kingfisher.swift"; sourceTree = ""; }; + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + A043F9EF1DF150712ECDD2FB7CD2E46B /* NetworkReachabilityManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkReachabilityManager.swift; path = Source/NetworkReachabilityManager.swift; sourceTree = ""; }; + A0DD802295003561B6C5741521D716BC /* RequestTaskMap.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestTaskMap.swift; path = Source/RequestTaskMap.swift; sourceTree = ""; }; + A1939B0AEE888F2EB258D6832523F67C /* Session.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Session.swift; path = Source/Session.swift; sourceTree = ""; }; + A20EDDB802D0AEBA579A2FAC5B1B6BDE /* OperationQueue+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "OperationQueue+Alamofire.swift"; path = "Source/OperationQueue+Alamofire.swift"; sourceTree = ""; }; + A9CCB3A1E65C7F0BD9B4C55FDA273F66 /* Pods-CatStaGram */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-CatStaGram"; path = Pods_CatStaGram.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AC2601AB15F49167B95C5DDC2FBC7B78 /* MemoryStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MemoryStorage.swift; path = Sources/Cache/MemoryStorage.swift; sourceTree = ""; }; + AFFE766A6E0F3A621DFD9487DA3CAF7E /* Protected.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Protected.swift; path = Source/Protected.swift; sourceTree = ""; }; + B0BAB418BAD6B4B421F289A06BD5909C /* Alamofire-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Alamofire-Info.plist"; sourceTree = ""; }; + B114DBD20B001B2A9D93F8F8C47903B6 /* Combine.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Combine.swift; path = Source/Combine.swift; sourceTree = ""; }; + B42FDB98681E0F2B54A53C6263FC0FB7 /* SessionDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDelegate.swift; path = Source/SessionDelegate.swift; sourceTree = ""; }; + B7407E80368EF1CE2A091B4F0F8F81FF /* URLSessionConfiguration+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URLSessionConfiguration+Alamofire.swift"; path = "Source/URLSessionConfiguration+Alamofire.swift"; sourceTree = ""; }; + B74729EFA464A2B107050ED713601AB8 /* AVAssetImageDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AVAssetImageDataProvider.swift; path = Sources/General/ImageSource/AVAssetImageDataProvider.swift; sourceTree = ""; }; + BA9B50C2ED49A8E73A4D64C205173820 /* UIButton+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIButton+Kingfisher.swift"; path = "Sources/Extensions/UIButton+Kingfisher.swift"; sourceTree = ""; }; + BB099A6B514875F22EF98798B56B4BA5 /* Response.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Response.swift; path = Source/Response.swift; sourceTree = ""; }; + BB31688140758856323E0B3187769487 /* URLRequest+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URLRequest+Alamofire.swift"; path = "Source/URLRequest+Alamofire.swift"; sourceTree = ""; }; + C264879E6B16EA9E07F073E24220A232 /* RequestModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestModifier.swift; path = Sources/Networking/RequestModifier.swift; sourceTree = ""; }; + C26990E9D251C57CAB34BC5C9EC37033 /* KFImageRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageRenderer.swift; path = Sources/SwiftUI/KFImageRenderer.swift; sourceTree = ""; }; + C32A86B99F0C8B9E42B3D9C7F9730889 /* ResponseSerialization.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ResponseSerialization.swift; path = Source/ResponseSerialization.swift; sourceTree = ""; }; + C3CD3E23CCF124B01CA690E27E72D1B9 /* KingfisherError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherError.swift; path = Sources/General/KingfisherError.swift; sourceTree = ""; }; + C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Kingfisher; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C6E93449DD3CC778DD5D443C7780D895 /* NSButton+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSButton+Kingfisher.swift"; path = "Sources/Extensions/NSButton+Kingfisher.swift"; sourceTree = ""; }; + C95F29F7BBB1AA54AD679386E26C7740 /* ImageProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProcessor.swift; path = Sources/Image/ImageProcessor.swift; sourceTree = ""; }; + CE9ECAD406B234DD522B04457BCADA54 /* SessionDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDelegate.swift; path = Sources/Networking/SessionDelegate.swift; sourceTree = ""; }; + D362E01704CD82D45E9C7857FB20AC1E /* DiskStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DiskStorage.swift; path = Sources/Cache/DiskStorage.swift; sourceTree = ""; }; + D370E0D48C1925AC14D155BEDF3DC7D4 /* GraphicsContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GraphicsContext.swift; path = Sources/Image/GraphicsContext.swift; sourceTree = ""; }; + D8CBA959C728F5B6931C11DE61853D88 /* SizeExtensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SizeExtensions.swift; path = Sources/Utility/SizeExtensions.swift; sourceTree = ""; }; + D942B0884573FD857CFB17A94E6A5807 /* StringEncoding+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "StringEncoding+Alamofire.swift"; path = "Source/StringEncoding+Alamofire.swift"; sourceTree = ""; }; + DAE0A9D42161F3A6512AEE4A7F63B4A0 /* ImageBinder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageBinder.swift; path = Sources/SwiftUI/ImageBinder.swift; sourceTree = ""; }; + DB1FAB3233CC80F28F7E66DF4DC9F2FA /* HTTPHeaders.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HTTPHeaders.swift; path = Source/HTTPHeaders.swift; sourceTree = ""; }; + E7A8F9A880D8CF92488BF36640F2DBBE /* Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Kingfisher.swift; path = Sources/General/Kingfisher.swift; sourceTree = ""; }; + EDEF84AAA5BEDA67174B11EECD29C8BD /* Kingfisher.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Kingfisher.release.xcconfig; sourceTree = ""; }; + EE2747566D1A67FB4BBC1883CC9ACE4E /* Pods-CatStaGram-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-CatStaGram-umbrella.h"; sourceTree = ""; }; + EEE76C5003FE080CF8820CA18C434EA3 /* ImageDownloader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDownloader.swift; path = Sources/Networking/ImageDownloader.swift; sourceTree = ""; }; + F1B7F2B2CCF604EF10790364BAB06391 /* AuthenticationInterceptor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthenticationInterceptor.swift; path = Source/AuthenticationInterceptor.swift; sourceTree = ""; }; + F368C1F054959568F9FC5262C8FD9F8C /* Pods-CatStaGram.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-CatStaGram.release.xcconfig"; sourceTree = ""; }; + F6A1EDFD24518B3F930192EDAE086842 /* ParameterEncoder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ParameterEncoder.swift; path = Source/ParameterEncoder.swift; sourceTree = ""; }; + FAB10145B8616970B7B29BFF058CF910 /* ImageContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageContext.swift; path = Sources/SwiftUI/ImageContext.swift; sourceTree = ""; }; + FB0BD0DA3BA3BF98EFC7BF4305A340BA /* Alamofire.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Alamofire.modulemap; sourceTree = ""; }; + FC710AFB6066D8535108054FD1115E94 /* Notifications.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Notifications.swift; path = Source/Notifications.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 15DC142A7EE833251AA37FC8E2B8E01F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B068137A8925891446203B5D3D6A4ED /* CFNetwork.framework in Frameworks */, + 0F4037DBF307AC8058BD0A3D35C7E7E9 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 81DB1665E1495609510BA493822E5A85 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E719A3B025B9DACE693130120BD9B927 /* Accelerate.framework in Frameworks */, + 420C200A05BB29E1D299D1BADE9139D2 /* CFNetwork.framework in Frameworks */, + 3AD5DBB915C2623991F7DBACD173BBB4 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B23375F7E905D644F561F7559FA187BF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C9392394543C174FFBF84B243D562853 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 03C5C200A0787E300053CFA8F53CA094 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5EE21CBB8EFB615AB450D209E054FAE6 /* iOS */, + ); + name = Frameworks; + sourceTree = ""; + }; + 0F4AF1A3447E9E41BB475E0A5B868464 /* Kingfisher */ = { + isa = PBXGroup; + children = ( + 06442349E67A8F1944EBFB63B46B406E /* AnimatedImageView.swift */, + 21DBC7F03013C95C65EE295C4DAC2A64 /* AuthenticationChallengeResponsable.swift */, + B74729EFA464A2B107050ED713601AB8 /* AVAssetImageDataProvider.swift */, + 0B7F4B3A6C95F8600D69F851451D63F6 /* Box.swift */, + 0BC3A37B1FA2F4841F5E8CC3FC102A19 /* CacheSerializer.swift */, + 40CCE8AF433C89864A9FE3C1F9C34A96 /* CallbackQueue.swift */, + 142C2CAF3091DF534D3E7B4AAA4DFC0B /* CPListItem+Kingfisher.swift */, + 0B347AF8E0B52CE83F1B9A8F51F1AE31 /* Delegate.swift */, + D362E01704CD82D45E9C7857FB20AC1E /* DiskStorage.swift */, + 365099CC49560F0DAFBF90D1A7FBFD24 /* ExtensionHelpers.swift */, + 6A723238162323CE3B380FA8E894C865 /* Filter.swift */, + 8676B059D0C43C5A2FC81E0F55AD63D7 /* FormatIndicatedCacheSerializer.swift */, + 4EF4F904B2EF1F1E836FADDE8C6B05DF /* GIFAnimatedImage.swift */, + D370E0D48C1925AC14D155BEDF3DC7D4 /* GraphicsContext.swift */, + 16E37EF24D9E916BB47E43D8E954D3AF /* Image.swift */, + DAE0A9D42161F3A6512AEE4A7F63B4A0 /* ImageBinder.swift */, + 8AE9B6094B04E75ACDBB73C3A194D690 /* ImageCache.swift */, + FAB10145B8616970B7B29BFF058CF910 /* ImageContext.swift */, + 37D911DA9899C96EA4408220F4754219 /* ImageDataProcessor.swift */, + 597967B6CDA2E72751463CF56198E927 /* ImageDataProvider.swift */, + EEE76C5003FE080CF8820CA18C434EA3 /* ImageDownloader.swift */, + 077E33C20C9BD4FAF66AE7A6EC1657F4 /* ImageDownloaderDelegate.swift */, + 7933A5F1F8BD20A48039AA759ADB465E /* ImageDrawing.swift */, + 3C65DBD1C381FE1E4AA74F824E85F325 /* ImageFormat.swift */, + 07524CF920EFBC25C9A0C9B3E57AA118 /* ImageModifier.swift */, + 240168000C008585397987F341FC9AFA /* ImagePrefetcher.swift */, + C95F29F7BBB1AA54AD679386E26C7740 /* ImageProcessor.swift */, + 7366FB0DEF319ED32BAF9DE5654C8DB7 /* ImageProgressive.swift */, + 4C1FA3BADCA4576957512EC8D60D2B76 /* ImageTransition.swift */, + 985C3C905A92749F5FB0325692FBF789 /* ImageView+Kingfisher.swift */, + 5723DEFD772E79C36C19B268F7643CE0 /* Indicator.swift */, + 660CF05D9FA7E4371D6E6A45ED8EE01F /* KF.swift */, + 86DDDE6D887ED6DFE35529D771930C65 /* KFAnimatedImage.swift */, + 9566B136FA420BDAAE8B272AE10CFE3E /* KFImage.swift */, + 2BAF59EA7A7DD980421989D2A205E6B0 /* KFImageOptions.swift */, + 292F77B8C48397E58C30BC7A313423E4 /* KFImageProtocol.swift */, + C26990E9D251C57CAB34BC5C9EC37033 /* KFImageRenderer.swift */, + 011604690A5B598427758F666A2F13C8 /* KFOptionsSetter.swift */, + E7A8F9A880D8CF92488BF36640F2DBBE /* Kingfisher.swift */, + C3CD3E23CCF124B01CA690E27E72D1B9 /* KingfisherError.swift */, + 7B7AD2807A19FC11299041D8A8777267 /* KingfisherManager.swift */, + 1B8B2E44706F2E23444FC30F3BAEE3DC /* KingfisherOptionsInfo.swift */, + AC2601AB15F49167B95C5DDC2FBC7B78 /* MemoryStorage.swift */, + C6E93449DD3CC778DD5D443C7780D895 /* NSButton+Kingfisher.swift */, + 1EBE61AE143E2DB816492EB19AA1651F /* NSTextAttachment+Kingfisher.swift */, + 7C6C8F94FCB9064F02B48B607AB69962 /* Placeholder.swift */, + 1C1BAC7555E3D0AC24A397BB028A3CEC /* RedirectHandler.swift */, + C264879E6B16EA9E07F073E24220A232 /* RequestModifier.swift */, + 95BFD9705B9A22E25BBBD0A608C90CCC /* Resource.swift */, + 7006AE0546B679604D685DCA785C542C /* Result.swift */, + 27F40A9A287AE4680FD4A041AAD0C5ED /* RetryStrategy.swift */, + 1CB85CAAF3329112CD2FFF9889E98FF7 /* Runtime.swift */, + 0C323A988742445270C1D6A4DD40ED07 /* SessionDataTask.swift */, + CE9ECAD406B234DD522B04457BCADA54 /* SessionDelegate.swift */, + D8CBA959C728F5B6931C11DE61853D88 /* SizeExtensions.swift */, + 496EC73E3893F724BA57D993B154BF49 /* Source.swift */, + 6BCDFE9C10374511CCC375D2E99C812E /* Storage.swift */, + 23963C2629645E475AB2931F62E451C7 /* String+MD5.swift */, + 4193FEA28CDEB3710C7C41537C8D5E2E /* TVMonogramView+Kingfisher.swift */, + BA9B50C2ED49A8E73A4D64C205173820 /* UIButton+Kingfisher.swift */, + 1C444AA66DFF06EB1D1D714FE5939A17 /* WKInterfaceImage+Kingfisher.swift */, + 729C05219705FA5F82BEFFE650B8F5E9 /* Support Files */, + ); + name = Kingfisher; + path = Kingfisher; + sourceTree = ""; + }; + 1A54762D1F676258FBB651CD2FE8D36D /* Targets Support Files */ = { + isa = PBXGroup; + children = ( + 8136E58DE0A16734FC0D913EA683C1E8 /* Pods-CatStaGram */, + ); + name = "Targets Support Files"; + sourceTree = ""; + }; + 1E75F81CC79CF69CB4EA4C194E904C70 /* Pods */ = { + isa = PBXGroup; + children = ( + 78C3F1E5DD9988656EC031F46E619FBD /* Alamofire */, + 0F4AF1A3447E9E41BB475E0A5B868464 /* Kingfisher */, + ); + name = Pods; + sourceTree = ""; + }; + 5EE21CBB8EFB615AB450D209E054FAE6 /* iOS */ = { + isa = PBXGroup; + children = ( + 52554E1C9731D5352FDE9E63F8C5466B /* Accelerate.framework */, + 872D7EFA572ECEE8EF993C27196E16DD /* CFNetwork.framework */, + 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; + 6F1D967767CECA40AC890F7DC03EA2B8 /* Support Files */ = { + isa = PBXGroup; + children = ( + FB0BD0DA3BA3BF98EFC7BF4305A340BA /* Alamofire.modulemap */, + 3D13E09327BDC573DE5AAFE079172AB9 /* Alamofire-dummy.m */, + B0BAB418BAD6B4B421F289A06BD5909C /* Alamofire-Info.plist */, + 24A6C9EC04946183C998CEC9FFF72EFD /* Alamofire-prefix.pch */, + 0D167A016457E1A5BD5B9A73C4FF9434 /* Alamofire-umbrella.h */, + 630CD17355E726BB7AF8B6C6A581C2FE /* Alamofire.debug.xcconfig */, + 56B4F426A74516AD9DF8A17EA00CE61E /* Alamofire.release.xcconfig */, + ); + name = "Support Files"; + path = "../Target Support Files/Alamofire"; + sourceTree = ""; + }; + 729C05219705FA5F82BEFFE650B8F5E9 /* Support Files */ = { + isa = PBXGroup; + children = ( + 73DC4358317E61D22885E415D01F8DFF /* Kingfisher.modulemap */, + 6835FBF931D5C31EFFF83ED559436CDE /* Kingfisher-dummy.m */, + 1986AFB6D3CAD62304F3FA85BE29D575 /* Kingfisher-Info.plist */, + 148FA9BD1508B6E7E220C9D9BF1928F8 /* Kingfisher-prefix.pch */, + 6C321B1DFC61DFB5E1EB0BF63C4E7B05 /* Kingfisher-umbrella.h */, + 6E5C70357A0F35A05639673633F51624 /* Kingfisher.debug.xcconfig */, + EDEF84AAA5BEDA67174B11EECD29C8BD /* Kingfisher.release.xcconfig */, + ); + name = "Support Files"; + path = "../Target Support Files/Kingfisher"; + sourceTree = ""; + }; + 78C3F1E5DD9988656EC031F46E619FBD /* Alamofire */ = { + isa = PBXGroup; + children = ( + 80270ADB4ECE74E17A7DB57236035920 /* AFError.swift */, + 76CC7FAE70D848424E201CB9C76CABD3 /* Alamofire.swift */, + 72447455A0B451F260AE94E750E590D7 /* AlamofireExtended.swift */, + F1B7F2B2CCF604EF10790364BAB06391 /* AuthenticationInterceptor.swift */, + 44A117735F022F30BB9DC641136E51C9 /* CachedResponseHandler.swift */, + B114DBD20B001B2A9D93F8F8C47903B6 /* Combine.swift */, + 1529C2E8B334710FA50E4C9A7153006A /* Concurrency.swift */, + 48BAA15044E6AD772F77A856FA5107F2 /* DispatchQueue+Alamofire.swift */, + 932C67B305CF8C8942FEBD2375BE2E1F /* EventMonitor.swift */, + DB1FAB3233CC80F28F7E66DF4DC9F2FA /* HTTPHeaders.swift */, + 5B56F1736D1127A32614C8CBBEF3E5F6 /* HTTPMethod.swift */, + 7EE41735C363063D401BC89633A455BE /* MultipartFormData.swift */, + 8BA6A6C6AEAC43638A2B248BBEA90EBD /* MultipartUpload.swift */, + A043F9EF1DF150712ECDD2FB7CD2E46B /* NetworkReachabilityManager.swift */, + FC710AFB6066D8535108054FD1115E94 /* Notifications.swift */, + A20EDDB802D0AEBA579A2FAC5B1B6BDE /* OperationQueue+Alamofire.swift */, + F6A1EDFD24518B3F930192EDAE086842 /* ParameterEncoder.swift */, + 249A902C9828CB270329EFFAFAC1843A /* ParameterEncoding.swift */, + AFFE766A6E0F3A621DFD9487DA3CAF7E /* Protected.swift */, + 499955C35F6D4A8F96E940FF129FF498 /* RedirectHandler.swift */, + 0341B04B64485596391C33DD9360D74E /* Request.swift */, + 6599B0D2C1F4B263D4D859853F0F348A /* RequestInterceptor.swift */, + A0DD802295003561B6C5741521D716BC /* RequestTaskMap.swift */, + BB099A6B514875F22EF98798B56B4BA5 /* Response.swift */, + C32A86B99F0C8B9E42B3D9C7F9730889 /* ResponseSerialization.swift */, + 2F3F986AF94568CEEB8AF631C72F03B2 /* Result+Alamofire.swift */, + 715993D57B4975C3CF94C6BC997595C6 /* RetryPolicy.swift */, + 04303942CB47C4C340CF398F8653BB98 /* ServerTrustEvaluation.swift */, + A1939B0AEE888F2EB258D6832523F67C /* Session.swift */, + B42FDB98681E0F2B54A53C6263FC0FB7 /* SessionDelegate.swift */, + D942B0884573FD857CFB17A94E6A5807 /* StringEncoding+Alamofire.swift */, + 404743B9B61FE5357078AFD049FC6B45 /* URLConvertible+URLRequestConvertible.swift */, + 072684B1154141462994E3AD2ACD3606 /* URLEncodedFormEncoder.swift */, + BB31688140758856323E0B3187769487 /* URLRequest+Alamofire.swift */, + B7407E80368EF1CE2A091B4F0F8F81FF /* URLSessionConfiguration+Alamofire.swift */, + 67030433EFF4B0333EC8811BC7EC47D6 /* Validation.swift */, + 6F1D967767CECA40AC890F7DC03EA2B8 /* Support Files */, + ); + name = Alamofire; + path = Alamofire; + sourceTree = ""; + }; + 8136E58DE0A16734FC0D913EA683C1E8 /* Pods-CatStaGram */ = { + isa = PBXGroup; + children = ( + 2397257AE270C74C5FF63E810A481FDE /* Pods-CatStaGram.modulemap */, + 2C309F94D8E35CA72C8F08ACB621D88F /* Pods-CatStaGram-acknowledgements.markdown */, + 0D470C70EF958AF31663B789E7801669 /* Pods-CatStaGram-acknowledgements.plist */, + 7DBB0D476F47E7A38CE0EDF85193CF41 /* Pods-CatStaGram-dummy.m */, + 792CB57DACF134EC2A6070CDDCF91F56 /* Pods-CatStaGram-frameworks.sh */, + 680FB1C38EA9062A1098F656BF33DEA1 /* Pods-CatStaGram-Info.plist */, + EE2747566D1A67FB4BBC1883CC9ACE4E /* Pods-CatStaGram-umbrella.h */, + 70D1A3E8DA5E73DFADB87E35404AAA05 /* Pods-CatStaGram.debug.xcconfig */, + F368C1F054959568F9FC5262C8FD9F8C /* Pods-CatStaGram.release.xcconfig */, + ); + name = "Pods-CatStaGram"; + path = "Target Support Files/Pods-CatStaGram"; + sourceTree = ""; + }; + CF1408CF629C7361332E53B88F7BD30C = { + isa = PBXGroup; + children = ( + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, + 03C5C200A0787E300053CFA8F53CA094 /* Frameworks */, + 1E75F81CC79CF69CB4EA4C194E904C70 /* Pods */, + E258A569C65C0563D771E2C2FC871EC4 /* Products */, + 1A54762D1F676258FBB651CD2FE8D36D /* Targets Support Files */, + ); + sourceTree = ""; + }; + E258A569C65C0563D771E2C2FC871EC4 /* Products */ = { + isa = PBXGroup; + children = ( + 5D797E9A5C5782CE845840781FA1CC81 /* Alamofire */, + C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */, + A9CCB3A1E65C7F0BD9B4C55FDA273F66 /* Pods-CatStaGram */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 43CCF09A05DFAC4EB3150A6E0E759A87 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 18E839CC4FD7F1CD252F4BCA4C70BED1 /* Pods-CatStaGram-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 52E6F7B26483BE3BC9393C6C05D32424 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 005B319B494ED2DAA239B9939A504DFC /* Alamofire-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7AE52B176E9872452FD890FC7F460CE1 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 122980E44B15C64CF0B14DC94D7EB5C9 /* Kingfisher-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */ = { + isa = PBXNativeTarget; + buildConfigurationList = 69ACD8654734266A348C6FF68E734010 /* Build configuration list for PBXNativeTarget "Kingfisher" */; + buildPhases = ( + 7AE52B176E9872452FD890FC7F460CE1 /* Headers */, + 3C1DA515D615F8CE75565ACE14378882 /* Sources */, + 81DB1665E1495609510BA493822E5A85 /* Frameworks */, + CFEB3E9FD20A01120B40D65B82D8F26F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Kingfisher; + productName = Kingfisher; + productReference = C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */; + productType = "com.apple.product-type.framework"; + }; + EAAA1AD3A8A1B59AB91319EE40752C6D /* Alamofire */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8A212264186B8822192F9C369D7DE4BB /* Build configuration list for PBXNativeTarget "Alamofire" */; + buildPhases = ( + 52E6F7B26483BE3BC9393C6C05D32424 /* Headers */, + F5D2A45FBA06D86A537CB441D5BF4FF4 /* Sources */, + 15DC142A7EE833251AA37FC8E2B8E01F /* Frameworks */, + E9D4145FA41F60FFAB33A07796D9ED97 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Alamofire; + productName = Alamofire; + productReference = 5D797E9A5C5782CE845840781FA1CC81 /* Alamofire */; + productType = "com.apple.product-type.framework"; + }; + F393950DF5E71F5A62A43D9F28802975 /* Pods-CatStaGram */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8D021AE99749D2A1BE107C5220D8DAE8 /* Build configuration list for PBXNativeTarget "Pods-CatStaGram" */; + buildPhases = ( + 43CCF09A05DFAC4EB3150A6E0E759A87 /* Headers */, + 33C417A8C3A976B1FCC021782A062736 /* Sources */, + B23375F7E905D644F561F7559FA187BF /* Frameworks */, + 0C3EF2A38ACDF47EE76DDF4D565771CC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 683A175A1094AF133A14F0CFE57BDAEA /* PBXTargetDependency */, + F5BC4F5CBDFA1DEFFA581E75C37BB059 /* PBXTargetDependency */, + ); + name = "Pods-CatStaGram"; + productName = Pods_CatStaGram; + productReference = A9CCB3A1E65C7F0BD9B4C55FDA273F66 /* Pods-CatStaGram */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BFDFE7DC352907FC980B868725387E98 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1240; + LastUpgradeCheck = 1240; + }; + buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = CF1408CF629C7361332E53B88F7BD30C; + productRefGroup = E258A569C65C0563D771E2C2FC871EC4 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + EAAA1AD3A8A1B59AB91319EE40752C6D /* Alamofire */, + E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */, + F393950DF5E71F5A62A43D9F28802975 /* Pods-CatStaGram */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0C3EF2A38ACDF47EE76DDF4D565771CC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CFEB3E9FD20A01120B40D65B82D8F26F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E9D4145FA41F60FFAB33A07796D9ED97 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33C417A8C3A976B1FCC021782A062736 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B6C05F9D9AF254B332EAAB3064F7F147 /* Pods-CatStaGram-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3C1DA515D615F8CE75565ACE14378882 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1952485AFF7A1BCCA4D4B142E82FE627 /* AnimatedImageView.swift in Sources */, + 243D7CFE1D56ED80ACB2B3E71B4CB603 /* AuthenticationChallengeResponsable.swift in Sources */, + D603AA58EF97D461A57B2B1BCB883868 /* AVAssetImageDataProvider.swift in Sources */, + B25E07EA645911443A38DA1E68166156 /* Box.swift in Sources */, + CD7AC3E1C98EA54F7C05C36C52805220 /* CacheSerializer.swift in Sources */, + FF09824309346665E2F1F7F5A45FB10F /* CallbackQueue.swift in Sources */, + DBB8088E14A2ADEDB1CD840BAC835267 /* CPListItem+Kingfisher.swift in Sources */, + 457BE444ED617FA7D6851D6DAA9D7234 /* Delegate.swift in Sources */, + 1FEE89BF952BE7ACA46E642DA2E48CA2 /* DiskStorage.swift in Sources */, + 1185A2B40E14F2FCBC761FC99777CAD8 /* ExtensionHelpers.swift in Sources */, + 3AF7DB9AEFF47F1F7F91AF28440E4AC6 /* Filter.swift in Sources */, + DE532EF7D50A9CF68587DAD4C1A02BD7 /* FormatIndicatedCacheSerializer.swift in Sources */, + F24021BDE9B42D604E3341CAD8E34759 /* GIFAnimatedImage.swift in Sources */, + 582D59E0D2EF62E0575933C99B393704 /* GraphicsContext.swift in Sources */, + 70FEC06F54286257E1BA1ECA0C99198D /* Image.swift in Sources */, + 1AA89F327105C026976BF6E831B193A2 /* ImageBinder.swift in Sources */, + ED0C8BA7560D7324587B353E0960479F /* ImageCache.swift in Sources */, + F5414F8A5B40521D0E4AEEB28378CB49 /* ImageContext.swift in Sources */, + 80738D8956C9987CCCEDF551961E5069 /* ImageDataProcessor.swift in Sources */, + 0285857A24F66E925987A5876F0BE63B /* ImageDataProvider.swift in Sources */, + DD72DC30CF19FFC81AB19CD0B074000D /* ImageDownloader.swift in Sources */, + 881A35B28D93C56E46E305F6138B1A76 /* ImageDownloaderDelegate.swift in Sources */, + DAFC6CE6321395CF4523DD66DADBB9BA /* ImageDrawing.swift in Sources */, + E6D6C7D5E458A05CC736C340F853E9F6 /* ImageFormat.swift in Sources */, + F54DE563418B1783D6EC491A0C3A05DB /* ImageModifier.swift in Sources */, + 00BEA6029C428FEE644AC3D42AD83282 /* ImagePrefetcher.swift in Sources */, + EF9C4588CDA85AED8BBCF77451B2A35B /* ImageProcessor.swift in Sources */, + 5F852F38CBC282496CCBE37C51324B2F /* ImageProgressive.swift in Sources */, + 29FF13E23FD52E46D30530549410AD7C /* ImageTransition.swift in Sources */, + E5B664771063F1A9A372519A8466860B /* ImageView+Kingfisher.swift in Sources */, + DFCDE4638265B4CCD494ECA5D560DBEE /* Indicator.swift in Sources */, + DF4563832C19B8582C810BF502A5CA29 /* KF.swift in Sources */, + 7FFE4021A4F14124342AD41CE1117B3E /* KFAnimatedImage.swift in Sources */, + 5E27DD292D3A55657712DD7AFA7B8FCA /* KFImage.swift in Sources */, + 5ADB30DD9A03859018550A999ACB0652 /* KFImageOptions.swift in Sources */, + F9537B023E24AC4A724E301F7E372491 /* KFImageProtocol.swift in Sources */, + 0510E8EA51914CB2176AD0F173937FAB /* KFImageRenderer.swift in Sources */, + A316388A35648CB2987E761771456087 /* KFOptionsSetter.swift in Sources */, + A39D3555EC8B45B7D6B9505DDAF0F117 /* Kingfisher.swift in Sources */, + 4DCA9775E5CCF599460BDB46E77F6FA4 /* Kingfisher-dummy.m in Sources */, + 4F37E521D341C47CE73DDCF21BA95A52 /* KingfisherError.swift in Sources */, + 1FE693B5ACC6AD7320CEFC20B64546E4 /* KingfisherManager.swift in Sources */, + 05228565AAA7FCED4BAFB2B7EF71D53D /* KingfisherOptionsInfo.swift in Sources */, + 1FD2928BC156D990D68B105F518C60B6 /* MemoryStorage.swift in Sources */, + 059639E700DEFAEF08F56484E5F67BE7 /* NSButton+Kingfisher.swift in Sources */, + 59BC9047F4BEBBC06235608D974E230D /* NSTextAttachment+Kingfisher.swift in Sources */, + 506128E1CC424E40E2691546D9547549 /* Placeholder.swift in Sources */, + 2BE89C24BFD3FB663E37C607C289B3B6 /* RedirectHandler.swift in Sources */, + 509490FB1D30FEC59AE4BC21AEEBB7BB /* RequestModifier.swift in Sources */, + 22BD1346F66BFCB129AAA44EEF322AC9 /* Resource.swift in Sources */, + A88A844D5356E1690E445024CB796E09 /* Result.swift in Sources */, + 22216C300C763044344B9DBF97317E63 /* RetryStrategy.swift in Sources */, + EBB32304E8DD4BA115454E0050D47DED /* Runtime.swift in Sources */, + 7C7418FF01DD7BB909719682B634A8A5 /* SessionDataTask.swift in Sources */, + 0ED8FBFD9A86D21BF69137EC9350E575 /* SessionDelegate.swift in Sources */, + 57FC31B14C753B5C63CEF00560F8A6EF /* SizeExtensions.swift in Sources */, + 9B0A78AC22E7EDA755F51D86527E2D9C /* Source.swift in Sources */, + 25FC036BEA33CAB5D80F5A41644535D3 /* Storage.swift in Sources */, + BD382E78580D295D10100678D4F66A76 /* String+MD5.swift in Sources */, + F17B1F8F2B6580343025237455A29D61 /* TVMonogramView+Kingfisher.swift in Sources */, + 082EDC820D76DF95C71A5018112DE512 /* UIButton+Kingfisher.swift in Sources */, + 9F5FE22DA95B66B8DC21CB13BE25EC9B /* WKInterfaceImage+Kingfisher.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F5D2A45FBA06D86A537CB441D5BF4FF4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0EA90FBF83350C49E6EF6C8A98D6F00 /* AFError.swift in Sources */, + F17A4CA4664CABB331D39FE902E06843 /* Alamofire.swift in Sources */, + 55AABB1FB38F61A3369ACC555FF3046D /* Alamofire-dummy.m in Sources */, + 1EE44196E7BCE57AD96A2C751651EF40 /* AlamofireExtended.swift in Sources */, + 7483E5327027263F7E4B94A2997191C4 /* AuthenticationInterceptor.swift in Sources */, + 2CBE3651CA006E19F5D64A2DE9B9A028 /* CachedResponseHandler.swift in Sources */, + 46A64A43AFA057B6B63C8F0C12F509B4 /* Combine.swift in Sources */, + 9C9030DEDB0DF955B16FE08C50892D57 /* Concurrency.swift in Sources */, + EEC150B66BCCD6C80FDA7E4D1975166B /* DispatchQueue+Alamofire.swift in Sources */, + CEBFFEED65D877702B2F36102528CF6D /* EventMonitor.swift in Sources */, + 7E02F5B62BE00E97847DF549FFED2490 /* HTTPHeaders.swift in Sources */, + D6B4751CED01D53E4A1B6A571AAA2F83 /* HTTPMethod.swift in Sources */, + 7FE695DA8EE7FF1286556E06B692009B /* MultipartFormData.swift in Sources */, + E9B4C89E7EB3B27D46AFCA452C3D426F /* MultipartUpload.swift in Sources */, + A29100AA1876DDEFF6F54694A51FDB0E /* NetworkReachabilityManager.swift in Sources */, + 2CCD13099063CD560E3067BD132914FA /* Notifications.swift in Sources */, + E54654D504A42C24F284A68F87F7671D /* OperationQueue+Alamofire.swift in Sources */, + 99D058E53EFEE3AC4857CDE3DBA5C004 /* ParameterEncoder.swift in Sources */, + 68FB2DCB4C77DBCAF9A6037E470F2BDE /* ParameterEncoding.swift in Sources */, + A53BDE589BDD6483F3EEDCE5EA1DCCD3 /* Protected.swift in Sources */, + 045DE6EBF9B2F63F60F5BE60C1198E06 /* RedirectHandler.swift in Sources */, + B3658C29BBDE1033F6269A92E612CB30 /* Request.swift in Sources */, + DD902FE8D6824681C929D028655AE121 /* RequestInterceptor.swift in Sources */, + DA34899BEF0668D76CBCE8C4CE47B97B /* RequestTaskMap.swift in Sources */, + 75966A9262648D4647D764E3E76BC6AC /* Response.swift in Sources */, + 824D816B1EE404F2DD400EE678695CBE /* ResponseSerialization.swift in Sources */, + 04A896288CE3A59B530250337A5F8362 /* Result+Alamofire.swift in Sources */, + 33A7D0F2D03004CE256A75E03DF33C70 /* RetryPolicy.swift in Sources */, + B704B198B9B520D449260877E300D821 /* ServerTrustEvaluation.swift in Sources */, + 81B8D2B7CEB25C2448B0BC9B33591A65 /* Session.swift in Sources */, + 1976BB7D7E26A12E29283E71154B63B3 /* SessionDelegate.swift in Sources */, + 7F1BB526AAE3ECDCE90127D9D0E10261 /* StringEncoding+Alamofire.swift in Sources */, + 8D75FC8D7476C9674234F39F1A820D8C /* URLConvertible+URLRequestConvertible.swift in Sources */, + 7930C94414B4C661867AC4FBE82E996C /* URLEncodedFormEncoder.swift in Sources */, + BC0ECA8F22DEDE8886E189CD0EAA1197 /* URLRequest+Alamofire.swift in Sources */, + 808C960C82D708FC1A42C581D6CB4940 /* URLSessionConfiguration+Alamofire.swift in Sources */, + 3C4059621E23842C19D4EB5D35B41989 /* Validation.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 683A175A1094AF133A14F0CFE57BDAEA /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Alamofire; + target = EAAA1AD3A8A1B59AB91319EE40752C6D /* Alamofire */; + targetProxy = BADC5D608C035FEA12332200B1BEA3AA /* PBXContainerItemProxy */; + }; + F5BC4F5CBDFA1DEFFA581E75C37BB059 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Kingfisher; + target = E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */; + targetProxy = 86507F3308AD3FD588A9636FBE2FE558 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 08ABDF3E2A01819F2628F573E591761D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 70D1A3E8DA5E73DFADB87E35404AAA05 /* Pods-CatStaGram.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-CatStaGram/Pods-CatStaGram-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-CatStaGram/Pods-CatStaGram.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 4214E6946993DF02ED98D89047C64016 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_DEBUG=1", + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Debug; + }; + 90A4588B06F8745E7FCD1B00204D6241 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 56B4F426A74516AD9DF8A17EA00CE61E /* Alamofire.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/Alamofire/Alamofire-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/Alamofire/Alamofire-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/Alamofire/Alamofire.modulemap"; + PRODUCT_MODULE_NAME = Alamofire; + PRODUCT_NAME = Alamofire; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.5; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 980A58862D8A5086E2825CF9017AC8DD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6E5C70357A0F35A05639673633F51624 /* Kingfisher.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/Kingfisher/Kingfisher-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/Kingfisher/Kingfisher-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/Kingfisher/Kingfisher.modulemap"; + PRODUCT_MODULE_NAME = Kingfisher; + PRODUCT_NAME = Kingfisher; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 9E98C04A5FA16D8AD5D48C1861179497 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 630CD17355E726BB7AF8B6C6A581C2FE /* Alamofire.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/Alamofire/Alamofire-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/Alamofire/Alamofire-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/Alamofire/Alamofire.modulemap"; + PRODUCT_MODULE_NAME = Alamofire; + PRODUCT_NAME = Alamofire; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.5; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + BFD9E4B58F44191AF73A3434AAF6831F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EDEF84AAA5BEDA67174B11EECD29C8BD /* Kingfisher.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/Kingfisher/Kingfisher-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/Kingfisher/Kingfisher-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/Kingfisher/Kingfisher.modulemap"; + PRODUCT_MODULE_NAME = Kingfisher; + PRODUCT_NAME = Kingfisher; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + C5B5D0306C480B3EDBBDE65FF216B6ED /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F368C1F054959568F9FC5262C8FD9F8C /* Pods-CatStaGram.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-CatStaGram/Pods-CatStaGram-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-CatStaGram/Pods-CatStaGram.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + CD85FD90473CFBE797E4264E7BBC70AF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_RELEASE=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4214E6946993DF02ED98D89047C64016 /* Debug */, + CD85FD90473CFBE797E4264E7BBC70AF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 69ACD8654734266A348C6FF68E734010 /* Build configuration list for PBXNativeTarget "Kingfisher" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 980A58862D8A5086E2825CF9017AC8DD /* Debug */, + BFD9E4B58F44191AF73A3434AAF6831F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8A212264186B8822192F9C369D7DE4BB /* Build configuration list for PBXNativeTarget "Alamofire" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9E98C04A5FA16D8AD5D48C1861179497 /* Debug */, + 90A4588B06F8745E7FCD1B00204D6241 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8D021AE99749D2A1BE107C5220D8DAE8 /* Build configuration list for PBXNativeTarget "Pods-CatStaGram" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 08ABDF3E2A01819F2628F573E591761D /* Debug */, + C5B5D0306C480B3EDBBDE65FF216B6ED /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BFDFE7DC352907FC980B868725387E98 /* Project object */; +} diff --git a/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Alamofire.xcscheme b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Alamofire.xcscheme new file mode 100644 index 0000000..120775c --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Alamofire.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Kingfisher.xcscheme b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Kingfisher.xcscheme new file mode 100644 index 0000000..0074170 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Kingfisher.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-CatStaGram.xcscheme b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-CatStaGram.xcscheme new file mode 100644 index 0000000..7f65c30 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-CatStaGram.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..f8edfc0 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,32 @@ + + + + + SchemeUserState + + Alamofire.xcscheme + + isShown + + orderHint + 0 + + Kingfisher.xcscheme + + isShown + + orderHint + 1 + + Pods-CatStaGram.xcscheme + + isShown + + orderHint + 2 + + + SuppressBuildableAutocreation + + + diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-Info.plist b/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-Info.plist new file mode 100644 index 0000000..a3f6596 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 5.6.1 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-dummy.m b/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-dummy.m new file mode 100644 index 0000000..a6c4594 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Alamofire : NSObject +@end +@implementation PodsDummy_Alamofire +@end diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-prefix.pch b/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-umbrella.h b/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-umbrella.h new file mode 100644 index 0000000..00014e3 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double AlamofireVersionNumber; +FOUNDATION_EXPORT const unsigned char AlamofireVersionString[]; + diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire.debug.xcconfig b/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire.debug.xcconfig new file mode 100644 index 0000000..7d169c4 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire.debug.xcconfig @@ -0,0 +1,14 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Alamofire +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "CFNetwork" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/Alamofire +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire.modulemap b/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire.modulemap new file mode 100644 index 0000000..d1f125f --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire.modulemap @@ -0,0 +1,6 @@ +framework module Alamofire { + umbrella header "Alamofire-umbrella.h" + + export * + module * { export * } +} diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire.release.xcconfig b/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire.release.xcconfig new file mode 100644 index 0000000..7d169c4 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire.release.xcconfig @@ -0,0 +1,14 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Alamofire +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "CFNetwork" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/Alamofire +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist b/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist new file mode 100644 index 0000000..f35e78c --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 7.2.2 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m b/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m new file mode 100644 index 0000000..1b89d0e --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Kingfisher : NSObject +@end +@implementation PodsDummy_Kingfisher +@end diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch b/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h b/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h new file mode 100644 index 0000000..75a7996 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double KingfisherVersionNumber; +FOUNDATION_EXPORT const unsigned char KingfisherVersionString[]; + diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig b/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig new file mode 100644 index 0000000..2dcd91e --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig @@ -0,0 +1,14 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/Kingfisher +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap b/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap new file mode 100644 index 0000000..2a20d91 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap @@ -0,0 +1,6 @@ +framework module Kingfisher { + umbrella header "Kingfisher-umbrella.h" + + export * + module * { export * } +} diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig b/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig new file mode 100644 index 0000000..2dcd91e --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig @@ -0,0 +1,14 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/Kingfisher +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-Info.plist b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-Info.plist new file mode 100644 index 0000000..2243fe6 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.markdown b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.markdown new file mode 100644 index 0000000..115a439 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.markdown @@ -0,0 +1,52 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## Alamofire + +Copyright (c) 2014-2022 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +## Kingfisher + +The MIT License (MIT) + +Copyright (c) 2019 Wei Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +Generated by CocoaPods - https://cocoapods.org diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.plist b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.plist new file mode 100644 index 0000000..f5305a5 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.plist @@ -0,0 +1,90 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + Copyright (c) 2014-2022 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + License + MIT + Title + Alamofire + Type + PSGroupSpecifier + + + FooterText + The MIT License (MIT) + +Copyright (c) 2019 Wei Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + License + MIT + Title + Kingfisher + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-dummy.m b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-dummy.m new file mode 100644 index 0000000..a4dd9c4 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_CatStaGram : NSObject +@end +@implementation PodsDummy_Pods_CatStaGram +@end diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-input-files.xcfilelist b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-input-files.xcfilelist new file mode 100644 index 0000000..f5030f5 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-input-files.xcfilelist @@ -0,0 +1,3 @@ +${PODS_ROOT}/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks.sh +${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework \ No newline at end of file diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-output-files.xcfilelist b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-output-files.xcfilelist new file mode 100644 index 0000000..cfe803f --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-output-files.xcfilelist @@ -0,0 +1,2 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework \ No newline at end of file diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-input-files.xcfilelist b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-input-files.xcfilelist new file mode 100644 index 0000000..f5030f5 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-input-files.xcfilelist @@ -0,0 +1,3 @@ +${PODS_ROOT}/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks.sh +${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework \ No newline at end of file diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-output-files.xcfilelist b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-output-files.xcfilelist new file mode 100644 index 0000000..cfe803f --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-output-files.xcfilelist @@ -0,0 +1,2 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework \ No newline at end of file diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks.sh b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks.sh new file mode 100755 index 0000000..21fad2e --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks.sh @@ -0,0 +1,188 @@ +#!/bin/sh +set -e +set -u +set -o pipefail + +function on_error { + echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" +} +trap 'on_error $LINENO' ERR + +if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then + # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy + # frameworks to, so exit 0 (signalling the script phase was successful). + exit 0 +fi + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" +SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" +BCSYMBOLMAP_DIR="BCSymbolMaps" + + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +# Copies and strips a vendored framework +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi + + if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then + # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied + find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do + echo "Installing $f" + install_bcsymbolmap "$f" "$destination" + rm "$f" + done + rmdir "${source}/${BCSYMBOLMAP_DIR}" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + elif [ -L "${binary}" ]; then + echo "Destination binary is symlinked..." + dirname="$(dirname "${binary}")" + binary="${dirname}/$(readlink "${binary}")" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} +# Copies and strips a vendored dSYM +install_dsym() { + local source="$1" + warn_missing_arch=${2:-true} + if [ -r "$source" ]; then + # Copy the dSYM into the targets temp dir. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" + + local basename + basename="$(basename -s .dSYM "$source")" + binary_name="$(ls "$source/Contents/Resources/DWARF")" + binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" + + # Strip invalid architectures from the dSYM. + if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then + strip_invalid_archs "$binary" "$warn_missing_arch" + fi + if [[ $STRIP_BINARY_RETVAL == 0 ]]; then + # Move the stripped file into its final destination. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" + else + # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. + mkdir -p "${DWARF_DSYM_FOLDER_PATH}" + touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" + fi + fi +} + +# Used as a return value for each invocation of `strip_invalid_archs` function. +STRIP_BINARY_RETVAL=0 + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + warn_missing_arch=${2:-true} + # Get architectures for current target binary + binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" + # Intersect them with the architectures we are building for + intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" + # If there are no archs supported by this binary then warn the user + if [[ -z "$intersected_archs" ]]; then + if [[ "$warn_missing_arch" == "true" ]]; then + echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." + fi + STRIP_BINARY_RETVAL=1 + return + fi + stripped="" + for arch in $binary_archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi + STRIP_BINARY_RETVAL=0 +} + +# Copies the bcsymbolmap files of a vendored framework +install_bcsymbolmap() { + local bcsymbolmap_path="$1" + local destination="${BUILT_PRODUCTS_DIR}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identity + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +if [[ "$CONFIGURATION" == "Debug" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework" + install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework" +fi +if [[ "$CONFIGURATION" == "Release" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework" + install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework" +fi +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-umbrella.h b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-umbrella.h new file mode 100644 index 0000000..a585e6f --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_CatStaGramVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_CatStaGramVersionString[]; + diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.debug.xcconfig b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.debug.xcconfig new file mode 100644 index 0000000..6282fcf --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.debug.xcconfig @@ -0,0 +1,15 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "Alamofire" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.modulemap b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.modulemap new file mode 100644 index 0000000..7404111 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.modulemap @@ -0,0 +1,6 @@ +framework module Pods_CatStaGram { + umbrella header "Pods-CatStaGram-umbrella.h" + + export * + module * { export * } +} diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.release.xcconfig b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.release.xcconfig new file mode 100644 index 0000000..6282fcf --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.release.xcconfig @@ -0,0 +1,15 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "Alamofire" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.pbxproj b/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a36bd2d --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.pbxproj @@ -0,0 +1,669 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + BC2E8F7F28340DB4007CBC0E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2E8F7E28340DB4007CBC0E /* AppDelegate.swift */; }; + BC2E8F8128340DB4007CBC0E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2E8F8028340DB4007CBC0E /* SceneDelegate.swift */; }; + BC2E8F8328340DB4007CBC0E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2E8F8228340DB4007CBC0E /* ViewController.swift */; }; + BC2E8F8628340DB4007CBC0E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BC2E8F8428340DB4007CBC0E /* Main.storyboard */; }; + BC2E8F8828340DB5007CBC0E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BC2E8F8728340DB5007CBC0E /* Assets.xcassets */; }; + BC2E8F8B28340DB5007CBC0E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BC2E8F8928340DB5007CBC0E /* LaunchScreen.storyboard */; }; + BC2E8F9628340DB5007CBC0E /* Todo_MVVMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2E8F9528340DB5007CBC0E /* Todo_MVVMTests.swift */; }; + BC2E8FA028340DB5007CBC0E /* Todo_MVVMUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2E8F9F28340DB5007CBC0E /* Todo_MVVMUITests.swift */; }; + BC2E8FA228340DB5007CBC0E /* Todo_MVVMUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2E8FA128340DB5007CBC0E /* Todo_MVVMUITestsLaunchTests.swift */; }; + BC2E8FB128341034007CBC0E /* TodoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2E8FB028341034007CBC0E /* TodoModel.swift */; }; + BC2E8FB428341F9F007CBC0E /* TodoVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2E8FB328341F9F007CBC0E /* TodoVM.swift */; }; + BC2E8FB7283423B2007CBC0E /* TodoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2E8FB6283423B2007CBC0E /* TodoTableViewCell.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + BC2E8F9228340DB5007CBC0E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BC2E8F7328340DB4007CBC0E /* Project object */; + proxyType = 1; + remoteGlobalIDString = BC2E8F7A28340DB4007CBC0E; + remoteInfo = Todo_MVVM; + }; + BC2E8F9C28340DB5007CBC0E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BC2E8F7328340DB4007CBC0E /* Project object */; + proxyType = 1; + remoteGlobalIDString = BC2E8F7A28340DB4007CBC0E; + remoteInfo = Todo_MVVM; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + BC2E8F7B28340DB4007CBC0E /* Todo_MVVM.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Todo_MVVM.app; sourceTree = BUILT_PRODUCTS_DIR; }; + BC2E8F7E28340DB4007CBC0E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + BC2E8F8028340DB4007CBC0E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + BC2E8F8228340DB4007CBC0E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + BC2E8F8528340DB4007CBC0E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + BC2E8F8728340DB5007CBC0E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + BC2E8F8A28340DB5007CBC0E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + BC2E8F8C28340DB5007CBC0E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BC2E8F9128340DB5007CBC0E /* Todo_MVVMTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Todo_MVVMTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BC2E8F9528340DB5007CBC0E /* Todo_MVVMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Todo_MVVMTests.swift; sourceTree = ""; }; + BC2E8F9B28340DB5007CBC0E /* Todo_MVVMUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Todo_MVVMUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BC2E8F9F28340DB5007CBC0E /* Todo_MVVMUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Todo_MVVMUITests.swift; sourceTree = ""; }; + BC2E8FA128340DB5007CBC0E /* Todo_MVVMUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Todo_MVVMUITestsLaunchTests.swift; sourceTree = ""; }; + BC2E8FB028341034007CBC0E /* TodoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoModel.swift; sourceTree = ""; }; + BC2E8FB328341F9F007CBC0E /* TodoVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoVM.swift; sourceTree = ""; }; + BC2E8FB6283423B2007CBC0E /* TodoTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoTableViewCell.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + BC2E8F7828340DB4007CBC0E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC2E8F8E28340DB5007CBC0E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC2E8F9828340DB5007CBC0E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + BC2E8F7228340DB4007CBC0E = { + isa = PBXGroup; + children = ( + BC2E8F7D28340DB4007CBC0E /* Todo_MVVM */, + BC2E8F9428340DB5007CBC0E /* Todo_MVVMTests */, + BC2E8F9E28340DB5007CBC0E /* Todo_MVVMUITests */, + BC2E8F7C28340DB4007CBC0E /* Products */, + ); + sourceTree = ""; + }; + BC2E8F7C28340DB4007CBC0E /* Products */ = { + isa = PBXGroup; + children = ( + BC2E8F7B28340DB4007CBC0E /* Todo_MVVM.app */, + BC2E8F9128340DB5007CBC0E /* Todo_MVVMTests.xctest */, + BC2E8F9B28340DB5007CBC0E /* Todo_MVVMUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + BC2E8F7D28340DB4007CBC0E /* Todo_MVVM */ = { + isa = PBXGroup; + children = ( + BC2E8FB52834239A007CBC0E /* Cell */, + BC2E8FB228341F8F007CBC0E /* ViewModel */, + BC2E8FAF28341023007CBC0E /* Model */, + BC2E8FB928342505007CBC0E /* Apps */, + BC2E8FB8283424F6007CBC0E /* ViewController */, + BC2E8FAE28340DC5007CBC0E /* View */, + BC2E8F8728340DB5007CBC0E /* Assets.xcassets */, + BC2E8F8928340DB5007CBC0E /* LaunchScreen.storyboard */, + BC2E8F8C28340DB5007CBC0E /* Info.plist */, + ); + path = Todo_MVVM; + sourceTree = ""; + }; + BC2E8F9428340DB5007CBC0E /* Todo_MVVMTests */ = { + isa = PBXGroup; + children = ( + BC2E8F9528340DB5007CBC0E /* Todo_MVVMTests.swift */, + ); + path = Todo_MVVMTests; + sourceTree = ""; + }; + BC2E8F9E28340DB5007CBC0E /* Todo_MVVMUITests */ = { + isa = PBXGroup; + children = ( + BC2E8F9F28340DB5007CBC0E /* Todo_MVVMUITests.swift */, + BC2E8FA128340DB5007CBC0E /* Todo_MVVMUITestsLaunchTests.swift */, + ); + path = Todo_MVVMUITests; + sourceTree = ""; + }; + BC2E8FAE28340DC5007CBC0E /* View */ = { + isa = PBXGroup; + children = ( + BC2E8F8428340DB4007CBC0E /* Main.storyboard */, + ); + path = View; + sourceTree = ""; + }; + BC2E8FAF28341023007CBC0E /* Model */ = { + isa = PBXGroup; + children = ( + BC2E8FB028341034007CBC0E /* TodoModel.swift */, + ); + path = Model; + sourceTree = ""; + }; + BC2E8FB228341F8F007CBC0E /* ViewModel */ = { + isa = PBXGroup; + children = ( + BC2E8FB328341F9F007CBC0E /* TodoVM.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + BC2E8FB52834239A007CBC0E /* Cell */ = { + isa = PBXGroup; + children = ( + BC2E8FB6283423B2007CBC0E /* TodoTableViewCell.swift */, + ); + path = Cell; + sourceTree = ""; + }; + BC2E8FB8283424F6007CBC0E /* ViewController */ = { + isa = PBXGroup; + children = ( + BC2E8F8228340DB4007CBC0E /* ViewController.swift */, + ); + path = ViewController; + sourceTree = ""; + }; + BC2E8FB928342505007CBC0E /* Apps */ = { + isa = PBXGroup; + children = ( + BC2E8F7E28340DB4007CBC0E /* AppDelegate.swift */, + BC2E8F8028340DB4007CBC0E /* SceneDelegate.swift */, + ); + path = Apps; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + BC2E8F7A28340DB4007CBC0E /* Todo_MVVM */ = { + isa = PBXNativeTarget; + buildConfigurationList = BC2E8FA528340DB5007CBC0E /* Build configuration list for PBXNativeTarget "Todo_MVVM" */; + buildPhases = ( + BC2E8F7728340DB4007CBC0E /* Sources */, + BC2E8F7828340DB4007CBC0E /* Frameworks */, + BC2E8F7928340DB4007CBC0E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Todo_MVVM; + productName = Todo_MVVM; + productReference = BC2E8F7B28340DB4007CBC0E /* Todo_MVVM.app */; + productType = "com.apple.product-type.application"; + }; + BC2E8F9028340DB5007CBC0E /* Todo_MVVMTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = BC2E8FA828340DB5007CBC0E /* Build configuration list for PBXNativeTarget "Todo_MVVMTests" */; + buildPhases = ( + BC2E8F8D28340DB5007CBC0E /* Sources */, + BC2E8F8E28340DB5007CBC0E /* Frameworks */, + BC2E8F8F28340DB5007CBC0E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BC2E8F9328340DB5007CBC0E /* PBXTargetDependency */, + ); + name = Todo_MVVMTests; + productName = Todo_MVVMTests; + productReference = BC2E8F9128340DB5007CBC0E /* Todo_MVVMTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + BC2E8F9A28340DB5007CBC0E /* Todo_MVVMUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = BC2E8FAB28340DB5007CBC0E /* Build configuration list for PBXNativeTarget "Todo_MVVMUITests" */; + buildPhases = ( + BC2E8F9728340DB5007CBC0E /* Sources */, + BC2E8F9828340DB5007CBC0E /* Frameworks */, + BC2E8F9928340DB5007CBC0E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BC2E8F9D28340DB5007CBC0E /* PBXTargetDependency */, + ); + name = Todo_MVVMUITests; + productName = Todo_MVVMUITests; + productReference = BC2E8F9B28340DB5007CBC0E /* Todo_MVVMUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BC2E8F7328340DB4007CBC0E /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1320; + LastUpgradeCheck = 1320; + TargetAttributes = { + BC2E8F7A28340DB4007CBC0E = { + CreatedOnToolsVersion = 13.2.1; + }; + BC2E8F9028340DB5007CBC0E = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = BC2E8F7A28340DB4007CBC0E; + }; + BC2E8F9A28340DB5007CBC0E = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = BC2E8F7A28340DB4007CBC0E; + }; + }; + }; + buildConfigurationList = BC2E8F7628340DB4007CBC0E /* Build configuration list for PBXProject "Todo_MVVM" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = BC2E8F7228340DB4007CBC0E; + productRefGroup = BC2E8F7C28340DB4007CBC0E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + BC2E8F7A28340DB4007CBC0E /* Todo_MVVM */, + BC2E8F9028340DB5007CBC0E /* Todo_MVVMTests */, + BC2E8F9A28340DB5007CBC0E /* Todo_MVVMUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + BC2E8F7928340DB4007CBC0E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC2E8F8B28340DB5007CBC0E /* LaunchScreen.storyboard in Resources */, + BC2E8F8828340DB5007CBC0E /* Assets.xcassets in Resources */, + BC2E8F8628340DB4007CBC0E /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC2E8F8F28340DB5007CBC0E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC2E8F9928340DB5007CBC0E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + BC2E8F7728340DB4007CBC0E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC2E8FB428341F9F007CBC0E /* TodoVM.swift in Sources */, + BC2E8F8328340DB4007CBC0E /* ViewController.swift in Sources */, + BC2E8FB128341034007CBC0E /* TodoModel.swift in Sources */, + BC2E8FB7283423B2007CBC0E /* TodoTableViewCell.swift in Sources */, + BC2E8F7F28340DB4007CBC0E /* AppDelegate.swift in Sources */, + BC2E8F8128340DB4007CBC0E /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC2E8F8D28340DB5007CBC0E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC2E8F9628340DB5007CBC0E /* Todo_MVVMTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC2E8F9728340DB5007CBC0E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC2E8FA228340DB5007CBC0E /* Todo_MVVMUITestsLaunchTests.swift in Sources */, + BC2E8FA028340DB5007CBC0E /* Todo_MVVMUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + BC2E8F9328340DB5007CBC0E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BC2E8F7A28340DB4007CBC0E /* Todo_MVVM */; + targetProxy = BC2E8F9228340DB5007CBC0E /* PBXContainerItemProxy */; + }; + BC2E8F9D28340DB5007CBC0E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BC2E8F7A28340DB4007CBC0E /* Todo_MVVM */; + targetProxy = BC2E8F9C28340DB5007CBC0E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + BC2E8F8428340DB4007CBC0E /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BC2E8F8528340DB4007CBC0E /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + BC2E8F8928340DB5007CBC0E /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BC2E8F8A28340DB5007CBC0E /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + BC2E8FA328340DB5007CBC0E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + BC2E8FA428340DB5007CBC0E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + BC2E8FA628340DB5007CBC0E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Todo_MVVM/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "jaem.Todo-MVVM"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + BC2E8FA728340DB5007CBC0E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Todo_MVVM/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "jaem.Todo-MVVM"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + BC2E8FA928340DB5007CBC0E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "jaem.Todo-MVVMTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Todo_MVVM.app/Todo_MVVM"; + }; + name = Debug; + }; + BC2E8FAA28340DB5007CBC0E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "jaem.Todo-MVVMTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Todo_MVVM.app/Todo_MVVM"; + }; + name = Release; + }; + BC2E8FAC28340DB5007CBC0E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "jaem.Todo-MVVMUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Todo_MVVM; + }; + name = Debug; + }; + BC2E8FAD28340DB5007CBC0E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "jaem.Todo-MVVMUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Todo_MVVM; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + BC2E8F7628340DB4007CBC0E /* Build configuration list for PBXProject "Todo_MVVM" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC2E8FA328340DB5007CBC0E /* Debug */, + BC2E8FA428340DB5007CBC0E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BC2E8FA528340DB5007CBC0E /* Build configuration list for PBXNativeTarget "Todo_MVVM" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC2E8FA628340DB5007CBC0E /* Debug */, + BC2E8FA728340DB5007CBC0E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BC2E8FA828340DB5007CBC0E /* Build configuration list for PBXNativeTarget "Todo_MVVMTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC2E8FA928340DB5007CBC0E /* Debug */, + BC2E8FAA28340DB5007CBC0E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BC2E8FAB28340DB5007CBC0E /* Build configuration list for PBXNativeTarget "Todo_MVVMUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC2E8FAC28340DB5007CBC0E /* Debug */, + BC2E8FAD28340DB5007CBC0E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BC2E8F7328340DB4007CBC0E /* Project object */; +} diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..99bfab76a8c1dc1f53f61fb77768ce59747cbfdd GIT binary patch literal 32069 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≮^B@<-)IZIzF>KoN}svlH8tA11cuBOx;YJWHbNK@yk3)IExQgyky zL2Xf6)y-;~x>dbQJ)l0I{!sm;`Wy9k>L1iUsejQ-)c9!vH6fa*ng~slW}3#R$742@0GrkSnj)bwfQX%=V}X|C3+)NIh~&^)5quX$PXn&wT-A#p_GPSN^k{j^iHQQBCoTpO=V(PnD1wRze?tyx>Ct=86Q8?>$3 zF6|=iQtfi>3hhem&DwR^+q4_CcWQTOAJ@L1J)-?Udt7@`drJGc_Dk(I+V8YKBrpl% z5+){iBuq;1O7KkxNC-{{OOPfcCsZZ0BrHtWnXotESi-lu@j4$}ur5?5)kW%rLn8Pp zI9;NyTX(R3bRb!ioStk*HYVpJ?@8X9{7&-WA)C)5%{apGp3H8ar*mG`DFU z(>$k5ndURiZ(6{#;Ax@LrcO(nHe=f2X90-yDs5caq%^NI-!%U; zX__oek)}*jr)kr4X_;x+X?badX{NN&w9d4JXS zPftouNuQpcp5BmtW%{P{ed(WOOw3Sbm@+yuuFtqRV_n9EjExz0XY9<_m9abHp^Qf| z9?Lk8aWLbRjMp>X$~cs9CgYp|H&6z~Fwx*{@H9*|Of|$Cv<96a$uQjj-;rj>GL##v zhB=1$hARzM8x|Xu8Ll;4Z&+ox(Qvb2tKojbPQxz4Zo|Wd#|%#xo-#aZc+Rlj@R8xq zOlf9Trag0Y=AO*K%6@OFwbMbG*=ZgO{u_jN`WRsW4$K+=UFh!fcfdW@12GruC)`raMdnrn^n|nYNi8H@#pwYWmQ0!t{yhGt+6) z*QPV3?@d3Mel2k?@hq8K;#J~P;$IR}5>gUY5?&Hn5?zv2(o%A5$+nW$OMWQzE!CG+ zmiClhS$cKp;?kw1D@$)JU0=GPbYtnArJG83l1(k)AO)ZNmi!GCv#g|PlD<~^2D=jN8t17E0n_1RYHmj_o?Devv zWhcu{m3?0JW!Znqek}W?>}=UNv&>v)UTR)xUSnQszQufxd6#*Q`4RKu=BLaD%!B6l z%ciR~6q>oT>P}a#rQsO0IHA=DpS?Ws`*umt5#Rt zS=C>)rRv_QZB;v}c2(`E+E;a;>R{C?Rj*gQRrPk&yH$s)PFMX{9aycaHdNPFJF4eZ zFRQ+``ugfs)i+k(T)nRPw(5JTAFO_=`q}DzHU2fynz$NeO=?YgO=iu;nyoeW*E~?m z)_T?Y)&|t}*7CJWYnRu4RQq-9ncDB`3hOHBn(CVCTIy!jIqEjn-Ceh>Zb#j&y4`h8 z*Bz{Tr|!eL6Lp`|eO7n6?)$nQ>wc*_TlYsjR_|8tUGG;PSRYb9wLY$1QLn1k*6Zt& z>J9Z-^||%=^%eC^^{w@@>O1PY>wD|x*3YkBT)(>h&ielPE%o=-Z>!%?zpH*v{l5AG z^#|);seirxt@^j?->pAff1>`I`tRz0sQ4ht$BZXtJUwHt#n%#HiMGU9mRN4E+-BKex!rPyEotPo4#oJs_FZtADez@I@@%vIkLIDnQLC$yt?_;=G&S#Hs95JPxF1v+naYb z?`nRud2j3b)(x%qwm#JQXzLTLPq*%E-QW5`>!H?nTaUDU(0aV}Wb3Kc&)O!osoLt= zmbI;FTi141+kI`@+a79rvTbkMp|%g&j<=m?`?&2)+xKlhw*As}w(XpqwvV$&&ckv;LfoW|N|G634}# N?5pBW*Z;Fu{y#bH2lW5| literal 0 HcmV?d00001 diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist b/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..589d196 --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + Todo_MVVM.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/Assets.xcassets/AccentColor.colorset/Contents.json b/jaem/week8/Todo_MVVM/Todo_MVVM/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/Assets.xcassets/AppIcon.appiconset/Contents.json b/jaem/week8/Todo_MVVM/Todo_MVVM/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/Assets.xcassets/Contents.json b/jaem/week8/Todo_MVVM/Todo_MVVM/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/Base.lproj/LaunchScreen.storyboard b/jaem/week8/Todo_MVVM/Todo_MVVM/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/Cell/TodoTableViewCell.swift b/jaem/week8/Todo_MVVM/Todo_MVVM/Cell/TodoTableViewCell.swift index 9a0d400..c19a5ab 100644 --- a/jaem/week8/Todo_MVVM/Todo_MVVM/Cell/TodoTableViewCell.swift +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/Cell/TodoTableViewCell.swift @@ -4,5 +4,18 @@ // // Created by 송재민 on 2022/05/18. // +import UIKit -import Foundation +class TodoTableViewCell: UITableViewCell{ + + @IBOutlet weak var todoLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + } +} diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/Info.plist b/jaem/week8/Todo_MVVM/Todo_MVVM/Info.plist new file mode 100644 index 0000000..dd3c9af --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/Model/TodoModel.swift b/jaem/week8/Todo_MVVM/Todo_MVVM/Model/TodoModel.swift index affeacf..df094a0 100644 --- a/jaem/week8/Todo_MVVM/Todo_MVVM/Model/TodoModel.swift +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/Model/TodoModel.swift @@ -6,3 +6,30 @@ // import Foundation + +struct TodoModel{ + + var todoList: [Todo] = [Todo(id: UUID(), todo: "example", isFinished: false), Todo(id: UUID(), todo: "example2", isFinished: false)] + + /*mutating 키워드를 붙이면 self 프로퍼티에 새로운 인스턴스 할당 가능*/ + //투두리스트 추가 + mutating func addTodo(todo: Todo){ + todoList.append(todo) + } + + //투두리스트 삭제 + mutating func deleteTodo(_ selectedId: UUID){ + guard let index = todoList.firstIndex(where: { todo in + todo.id == selectedId + }) else {return} + + todoList.remove(at: index) + } + + /*ID 구현을 위해 identifiable 프로토콜 사용?*/ + struct Todo: Identifiable{ + var id: UUID + var todo: String + var isFinished: Bool = false + } +} diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/View/Base.lproj/Main.storyboard b/jaem/week8/Todo_MVVM/Todo_MVVM/View/Base.lproj/Main.storyboard index 25a7638..5470df8 100644 --- a/jaem/week8/Todo_MVVM/Todo_MVVM/View/Base.lproj/Main.storyboard +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/View/Base.lproj/Main.storyboard @@ -1,24 +1,101 @@ - + + - + + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/ViewController/ViewController.swift b/jaem/week8/Todo_MVVM/Todo_MVVM/ViewController/ViewController.swift index ff705b4..52e2cab 100644 --- a/jaem/week8/Todo_MVVM/Todo_MVVM/ViewController/ViewController.swift +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/ViewController/ViewController.swift @@ -8,12 +8,58 @@ import UIKit class ViewController: UIViewController { - + + var todoVM = TodoVM() // View Model + var todoList: Array = [] + @IBOutlet weak var todoTableView: UITableView! + @IBOutlet weak var addBtn: UIBarButtonItem! + + + @IBAction func addTodo(_ sender: Any) { + let alert = UIAlertController(title: "Todo", message: nil, preferredStyle: .alert) + alert.addTextField{ textField in + textField.placeholder = "Add Your Todo" + } + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + alert.addAction(UIAlertAction(title: "Confirm", style: .default, handler:{ _ in guard let text = alert.textFields?[0].text else {return} + if text != ""{ + self.todoVM.addTodo(todo: TodoModel.Todo(id: UUID(), todo: text, isFinished: false)) + } + })) + + todoTableView.reloadData() + + self.present(alert, animated: true, completion: nil) + } + override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. + todoList = todoVM.todoList() + print(todoList.count) + self.todoTableView.dataSource = self + self.todoTableView.delegate = self } } +extension ViewController: UITableViewDataSource, UITableViewDelegate{ + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return todoList.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "TodoTableViewCell", for: indexPath) as! TodoTableViewCell + cell.selectionStyle = .none + cell.todoLabel.text = todoList[indexPath.row].todo + + return cell + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 44 + } +} + + diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/ViewModel/TodoVM.swift b/jaem/week8/Todo_MVVM/Todo_MVVM/ViewModel/TodoVM.swift index e1dfcee..f39bf02 100644 --- a/jaem/week8/Todo_MVVM/Todo_MVVM/ViewModel/TodoVM.swift +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/ViewModel/TodoVM.swift @@ -5,4 +5,24 @@ // Created by 송재민 on 2022/05/18. // -import Foundation +import UIKit + +class TodoVM: ObservableObject{ + @Published public var model = TodoModel() + typealias Todo = TodoModel.Todo + + //전체 투두 리스트 + func todoList() -> [Todo]{ + return model.todoList + } + + //투두 추가 + func addTodo(todo: Todo){ + model.addTodo(todo: todo) + } + + //투두 삭제 + func deleteTodo(_ selectedId: UUID){ + model.deleteTodo(selectedId) + } +} diff --git a/jaem/week8/Todo_MVVM/Todo_MVVMTests/Todo_MVVMTests.swift b/jaem/week8/Todo_MVVM/Todo_MVVMTests/Todo_MVVMTests.swift new file mode 100644 index 0000000..ad5a86c --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVMTests/Todo_MVVMTests.swift @@ -0,0 +1,36 @@ +// +// Todo_MVVMTests.swift +// Todo_MVVMTests +// +// Created by 송재민 on 2022/05/18. +// + +import XCTest +@testable import Todo_MVVM + +class Todo_MVVMTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/jaem/week8/Todo_MVVM/Todo_MVVMUITests/Todo_MVVMUITests.swift b/jaem/week8/Todo_MVVM/Todo_MVVMUITests/Todo_MVVMUITests.swift new file mode 100644 index 0000000..6c34c3b --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVMUITests/Todo_MVVMUITests.swift @@ -0,0 +1,42 @@ +// +// Todo_MVVMUITests.swift +// Todo_MVVMUITests +// +// Created by 송재민 on 2022/05/18. +// + +import XCTest + +class Todo_MVVMUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/jaem/week8/Todo_MVVM/Todo_MVVMUITests/Todo_MVVMUITestsLaunchTests.swift b/jaem/week8/Todo_MVVM/Todo_MVVMUITests/Todo_MVVMUITestsLaunchTests.swift new file mode 100644 index 0000000..e2a9b98 --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVMUITests/Todo_MVVMUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// Todo_MVVMUITestsLaunchTests.swift +// Todo_MVVMUITests +// +// Created by 송재민 on 2022/05/18. +// + +import XCTest + +class Todo_MVVMUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} From e41d140972b3c796ae03ef187ab49236f101e038 Mon Sep 17 00:00:00 2001 From: xongjaemin Date: Thu, 19 May 2022 02:18:24 +0900 Subject: [PATCH 6/9] =?UTF-8?q?dev=20:=20MVVM=20=ED=88=AC=EB=91=90?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserInterfaceState.xcuserstate | Bin 32069 -> 37742 bytes .../Todo_MVVM/Cell/TodoTableViewCell.swift | 1 + .../Todo_MVVM/Todo_MVVM/Model/TodoModel.swift | 16 ++++- .../Todo_MVVM/View/Base.lproj/Main.storyboard | 17 +++++- .../ViewController/ViewController.swift | 56 ++++++++++++++++-- .../Todo_MVVM/ViewModel/TodoVM.swift | 10 ++++ 6 files changed, 94 insertions(+), 6 deletions(-) diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate index 99bfab76a8c1dc1f53f61fb77768ce59747cbfdd..39bbadf8eeca761515708b3b29dbe9064631efa2 100644 GIT binary patch literal 37742 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┃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 G`~Ltw*GU5a diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/Cell/TodoTableViewCell.swift b/jaem/week8/Todo_MVVM/Todo_MVVM/Cell/TodoTableViewCell.swift index c19a5ab..890fa8c 100644 --- a/jaem/week8/Todo_MVVM/Todo_MVVM/Cell/TodoTableViewCell.swift +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/Cell/TodoTableViewCell.swift @@ -9,6 +9,7 @@ import UIKit class TodoTableViewCell: UITableViewCell{ @IBOutlet weak var todoLabel: UILabel! + @IBOutlet weak var checkIcon: UIImageView! override func awakeFromNib() { super.awakeFromNib() diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/Model/TodoModel.swift b/jaem/week8/Todo_MVVM/Todo_MVVM/Model/TodoModel.swift index df094a0..082945b 100644 --- a/jaem/week8/Todo_MVVM/Todo_MVVM/Model/TodoModel.swift +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/Model/TodoModel.swift @@ -9,7 +9,7 @@ import Foundation struct TodoModel{ - var todoList: [Todo] = [Todo(id: UUID(), todo: "example", isFinished: false), Todo(id: UUID(), todo: "example2", isFinished: false)] + var todoList: [Todo] = [Todo(id: UUID(), todo: "example", isFinished: false)] /*mutating 키워드를 붙이면 self 프로퍼티에 새로운 인스턴스 할당 가능*/ //투두리스트 추가 @@ -26,6 +26,20 @@ struct TodoModel{ todoList.remove(at: index) } + //투두리스트 중간에 추가 (row 이동시에 사용) + mutating func insertTodo(todo: Todo, index: Int){ + todoList.insert(todo, at: index) + } + + //투두 완료/취소 + mutating func completeTodo(_ selectedId: UUID){ + guard let index = todoList.firstIndex(where: { todo in + todo.id == selectedId + }) else {return} + + todoList[index].isFinished = !todoList[index].isFinished + } + /*ID 구현을 위해 identifiable 프로토콜 사용?*/ struct Todo: Identifiable{ var id: UUID diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/View/Base.lproj/Main.storyboard b/jaem/week8/Todo_MVVM/Todo_MVVM/View/Base.lproj/Main.storyboard index 5470df8..2f6bc87 100644 --- a/jaem/week8/Todo_MVVM/Todo_MVVM/View/Base.lproj/Main.storyboard +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/View/Base.lproj/Main.storyboard @@ -33,14 +33,24 @@ + + + + + + + + + + @@ -57,7 +67,11 @@ - + + + + + @@ -66,6 +80,7 @@ + diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/ViewController/ViewController.swift b/jaem/week8/Todo_MVVM/Todo_MVVM/ViewController/ViewController.swift index 52e2cab..274c25c 100644 --- a/jaem/week8/Todo_MVVM/Todo_MVVM/ViewController/ViewController.swift +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/ViewController/ViewController.swift @@ -13,22 +13,35 @@ class ViewController: UIViewController { var todoList: Array = [] @IBOutlet weak var todoTableView: UITableView! @IBOutlet weak var addBtn: UIBarButtonItem! + @IBOutlet weak var editDoneBtn: UIBarButtonItem! + /*edit 버튼 액션*/ + @IBAction func editTable(_ sender: Any) { + if(self.todoTableView.isEditing){ + self.editDoneBtn.title = "Edit" + self.todoTableView.setEditing(false, animated: true) + }else{ + self.editDoneBtn.title = "Done" + self.todoTableView.setEditing(true, animated: true) + } + } + /*추가 버튼 액션*/ @IBAction func addTodo(_ sender: Any) { let alert = UIAlertController(title: "Todo", message: nil, preferredStyle: .alert) alert.addTextField{ textField in textField.placeholder = "Add Your Todo" } - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + alert.addAction(UIAlertAction(title: "Confirm", style: .default, handler:{ _ in guard let text = alert.textFields?[0].text else {return} if text != ""{ self.todoVM.addTodo(todo: TodoModel.Todo(id: UUID(), todo: text, isFinished: false)) + self.todoList = self.todoVM.todoList() + self.todoTableView.reloadData() } })) - todoTableView.reloadData() - + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) self.present(alert, animated: true, completion: nil) } @@ -36,7 +49,10 @@ class ViewController: UIViewController { super.viewDidLoad() // Do any additional setup after loading the view. todoList = todoVM.todoList() - print(todoList.count) + + self.todoTableView.separatorInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) + self.todoTableView.frame.inset(by: UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)) + self.todoTableView.tableHeaderView = UIView() self.todoTableView.dataSource = self self.todoTableView.delegate = self } @@ -54,12 +70,44 @@ extension ViewController: UITableViewDataSource, UITableViewDelegate{ cell.selectionStyle = .none cell.todoLabel.text = todoList[indexPath.row].todo + if(todoList[indexPath.row].isFinished){ + cell.checkIcon.image = UIImage(systemName: "checkmark") + }else{ + cell.checkIcon.image = nil + } + return cell } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 44 } + + /*edit 동작*/ + func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + if editingStyle == UITableViewCell.EditingStyle.delete{ + self.todoVM.deleteTodo(todoList[indexPath.row].id) + self.todoList = self.todoVM.todoList() + self.todoTableView.reloadData() + } + } + + /*Row 이동*/ + func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { + let targetItem: String = self.todoList[sourceIndexPath.row].todo + self.todoVM.deleteTodo(todoList[sourceIndexPath.row].id) + self.todoVM.insertTodo(todo: TodoModel.Todo(id: UUID(), todo: targetItem, isFinished: false), index: destinationIndexPath.row) + + self.todoList = self.todoVM.todoList() + self.todoTableView.reloadData() + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + self.todoVM.completeTodo(self.todoList[indexPath.row].id) + + self.todoList = self.todoVM.todoList() + self.todoTableView.reloadData() + } } diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/ViewModel/TodoVM.swift b/jaem/week8/Todo_MVVM/Todo_MVVM/ViewModel/TodoVM.swift index f39bf02..5a2ad71 100644 --- a/jaem/week8/Todo_MVVM/Todo_MVVM/ViewModel/TodoVM.swift +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/ViewModel/TodoVM.swift @@ -21,8 +21,18 @@ class TodoVM: ObservableObject{ model.addTodo(todo: todo) } + //투두 중간에 삽입 + func insertTodo(todo: Todo, index: Int){ + model.insertTodo(todo: todo, index: index) + } + //투두 삭제 func deleteTodo(_ selectedId: UUID){ model.deleteTodo(selectedId) } + + //투두 완료/취소 + func completeTodo(_ selectedId: UUID){ + model.completeTodo(selectedId) + } } From 9c246b84a4a802903517253904991082ca83403e Mon Sep 17 00:00:00 2001 From: xongjaemin Date: Thu, 26 May 2022 01:08:53 +0900 Subject: [PATCH 7/9] =?UTF-8?q?dev=20:=20memorize=20=ED=83=AD=EB=B0=94,=20?= =?UTF-8?q?=EB=82=B4=EB=B9=84=EB=B0=94,=20=EB=A9=94=EB=89=B4=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=EB=B7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CatStaGram/CatStaGram/ReelsCell.swift | 8 +++ .../CatStaGram/ReelsViewController.swift | 29 +++++++++++ .../CatStaGram/VideoPlayerView.swift | 8 +++ .../MemorizeGame/Apps/AppDelegate.swift | 36 +++++++++++++ .../MemorizeGame/Apps/SceneDelegate.swift | 52 +++++++++++++++++++ .../MemorizeGame/Cell/MenuTableViewCell.swift | 8 +++ .../MemorizeGame/Model/MenuModel.swift | 8 +++ .../View/Base.lproj/Main.storyboard | 24 +++++++++ .../MemorizeGame/ViewModel/MenuVM.swift | 8 +++ 9 files changed, 181 insertions(+) create mode 100644 jaem/week7/CatStaGram/CatStaGram/ReelsCell.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/ReelsViewController.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/VideoPlayerView.swift create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/Apps/AppDelegate.swift create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/Apps/SceneDelegate.swift create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/Cell/MenuTableViewCell.swift create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/Model/MenuModel.swift create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/View/Base.lproj/Main.storyboard create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/ViewModel/MenuVM.swift diff --git a/jaem/week7/CatStaGram/CatStaGram/ReelsCell.swift b/jaem/week7/CatStaGram/CatStaGram/ReelsCell.swift new file mode 100644 index 0000000..1830194 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/ReelsCell.swift @@ -0,0 +1,8 @@ +// +// ReelsCell.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/21. +// + +import Foundation diff --git a/jaem/week7/CatStaGram/CatStaGram/ReelsViewController.swift b/jaem/week7/CatStaGram/CatStaGram/ReelsViewController.swift new file mode 100644 index 0000000..3503487 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/ReelsViewController.swift @@ -0,0 +1,29 @@ +// +// ReelsViewController.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/21. +// + +import UIKit + +class ReelsViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/jaem/week7/CatStaGram/CatStaGram/VideoPlayerView.swift b/jaem/week7/CatStaGram/CatStaGram/VideoPlayerView.swift new file mode 100644 index 0000000..e1614fe --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/VideoPlayerView.swift @@ -0,0 +1,8 @@ +// +// VideoPlayerView.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/21. +// + +import Foundation diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Apps/AppDelegate.swift b/jaem/week9/MemorizeGame/MemorizeGame/Apps/AppDelegate.swift new file mode 100644 index 0000000..4b7edee --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/Apps/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// MemorizeGame +// +// Created by 송재민 on 2022/05/25. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Apps/SceneDelegate.swift b/jaem/week9/MemorizeGame/MemorizeGame/Apps/SceneDelegate.swift new file mode 100644 index 0000000..e956294 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/Apps/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// MemorizeGame +// +// Created by 송재민 on 2022/05/25. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Cell/MenuTableViewCell.swift b/jaem/week9/MemorizeGame/MemorizeGame/Cell/MenuTableViewCell.swift new file mode 100644 index 0000000..dbc1ac3 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/Cell/MenuTableViewCell.swift @@ -0,0 +1,8 @@ +// +// MenuTableViewCell.swift +// MemorizeGame +// +// Created by 송재민 on 2022/05/26. +// + +import Foundation diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Model/MenuModel.swift b/jaem/week9/MemorizeGame/MemorizeGame/Model/MenuModel.swift new file mode 100644 index 0000000..c0e688d --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/Model/MenuModel.swift @@ -0,0 +1,8 @@ +// +// MenuModel.swift +// MemorizeGame +// +// Created by 송재민 on 2022/05/26. +// + +import Foundation diff --git a/jaem/week9/MemorizeGame/MemorizeGame/View/Base.lproj/Main.storyboard b/jaem/week9/MemorizeGame/MemorizeGame/View/Base.lproj/Main.storyboard new file mode 100644 index 0000000..25a7638 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/View/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/MenuVM.swift b/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/MenuVM.swift new file mode 100644 index 0000000..fc38a5a --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/MenuVM.swift @@ -0,0 +1,8 @@ +// +// MenuVM.swift +// MemorizeGame +// +// Created by 송재민 on 2022/05/26. +// + +import Foundation From 897895926f67cd734101cce2f7046ffa8d963acc Mon Sep 17 00:00:00 2001 From: xongjaemin Date: Thu, 26 May 2022 01:09:25 +0900 Subject: [PATCH 8/9] =?UTF-8?q?dev:=20memorize=20game=20=ED=83=AD=EB=B0=94?= =?UTF-8?q?,=20=EB=84=A4=EB=B9=84=EB=B0=94,=20=EB=A9=94=EB=89=B4=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=EB=B7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CatStaGram.xcodeproj/project.pbxproj | 20 + .../xcschemes/xcschememanagement.plist | 2 +- .../UserInterfaceState.xcuserstate | Bin 60381 -> 62479 bytes .../CatStaGram/Base.lproj/Main.storyboard | 39 + .../CatStaGram/CatStaGram/ReelsCell.swift | 97 +- .../CatStaGram/ReelsViewController.swift | 77 +- .../CatStaGram/VideoPlayerView.swift | 44 +- jaem/week7/CatStaGram/Podfile | 1 + jaem/week7/CatStaGram/Podfile.lock | 6 +- jaem/week7/CatStaGram/Pods/Manifest.lock | 6 +- .../Pods/Pods.xcodeproj/project.pbxproj | 1171 +++++++++++------ .../xcschemes/SnapKit.xcscheme | 58 + .../xcschemes/xcschememanagement.plist | 7 + jaem/week7/CatStaGram/Pods/SnapKit/LICENSE | 19 + jaem/week7/CatStaGram/Pods/SnapKit/README.md | 155 +++ .../Pods/SnapKit/Sources/Constraint.swift | 341 +++++ .../Sources/ConstraintAttributes.swift | 203 +++ .../SnapKit/Sources/ConstraintConfig.swift | 37 + .../Sources/ConstraintConstantTarget.swift | 213 +++ .../Pods/SnapKit/Sources/ConstraintDSL.swift | 209 +++ .../Sources/ConstraintDescription.swift | 69 + .../ConstraintDirectionalInsetTarget.swift | 49 + .../Sources/ConstraintDirectionalInsets.swift | 34 + .../Sources/ConstraintInsetTarget.swift | 72 + .../SnapKit/Sources/ConstraintInsets.swift | 35 + .../Pods/SnapKit/Sources/ConstraintItem.swift | 61 + .../ConstraintLayoutGuide+Extensions.swift | 36 + .../Sources/ConstraintLayoutGuide.swift | 37 + .../Sources/ConstraintLayoutGuideDSL.swift | 66 + .../Sources/ConstraintLayoutSupport.swift | 36 + .../Sources/ConstraintLayoutSupportDSL.swift | 56 + .../SnapKit/Sources/ConstraintMaker.swift | 224 ++++ .../Sources/ConstraintMakerEditable.swift | 64 + .../Sources/ConstraintMakerExtendable.swift | 195 +++ .../Sources/ConstraintMakerFinalizable.swift | 49 + .../ConstraintMakerPrioritizable.swift | 70 + .../ConstraintMakerRelatable+Extensions.swift | 57 + .../Sources/ConstraintMakerRelatable.swift | 115 ++ .../Sources/ConstraintMultiplierTarget.swift | 75 ++ .../Sources/ConstraintOffsetTarget.swift | 69 + .../SnapKit/Sources/ConstraintPriority.swift | 77 ++ .../Sources/ConstraintPriorityTarget.swift | 85 ++ .../Sources/ConstraintRelatableTarget.swift | 72 + .../SnapKit/Sources/ConstraintRelation.swift | 48 + .../Sources/ConstraintView+Extensions.swift | 152 +++ .../Pods/SnapKit/Sources/ConstraintView.swift | 35 + .../SnapKit/Sources/ConstraintViewDSL.swift | 101 ++ .../Pods/SnapKit/Sources/Debugging.swift | 169 +++ .../SnapKit/Sources/LayoutConstraint.swift | 61 + .../Sources/LayoutConstraintItem.swift | 93 ++ .../Pods/SnapKit/Sources/Typealiases.swift | 42 + .../Sources/UILayoutSupport+Extensions.swift | 36 + .../Pods-CatStaGram-acknowledgements.markdown | 23 + .../Pods-CatStaGram-acknowledgements.plist | 29 + ...am-frameworks-Debug-input-files.xcfilelist | 3 +- ...m-frameworks-Debug-output-files.xcfilelist | 3 +- ...-frameworks-Release-input-files.xcfilelist | 3 +- ...frameworks-Release-output-files.xcfilelist | 3 +- .../Pods-CatStaGram-frameworks.sh | 2 + .../Pods-CatStaGram.debug.xcconfig | 6 +- .../Pods-CatStaGram.release.xcconfig | 6 +- .../SnapKit/SnapKit-Info.plist | 26 + .../SnapKit/SnapKit-dummy.m | 5 + .../SnapKit/SnapKit-prefix.pch | 12 + .../SnapKit/SnapKit-umbrella.h | 16 + .../SnapKit/SnapKit.debug.xcconfig | 13 + .../SnapKit/SnapKit.modulemap | 6 + .../SnapKit/SnapKit.release.xcconfig | 13 + .../UserInterfaceState.xcuserstate | Bin 37742 -> 39430 bytes .../Todo_MVVM/Todo_MVVM/Model/TodoModel.swift | 2 +- .../ViewController/ViewController.swift | 2 + .../MemorizeGame.xcodeproj/project.pbxproj | 661 ++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 41849 bytes .../xcschemes/xcschememanagement.plist | 14 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 ++ .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../MemorizeGame/Cell/MenuTableViewCell.swift | 16 +- .../MemorizeGame/MemorizeGame/HomeVC.swift | 58 + .../MemorizeGame/MemorizeGame/Info.plist | 25 + .../MemorizeGame/Model/MenuModel.swift | 25 + .../View/Base.lproj/Main.storyboard | 140 +- .../MemorizeGame/ViewModel/MenuVM.swift | 15 +- .../MemorizeGameTests/MemorizeGameTests.swift | 36 + .../MemorizeGameUITests.swift | 42 + .../MemorizeGameUITestsLaunchTests.swift | 32 + 89 files changed, 6081 insertions(+), 456 deletions(-) create mode 100644 jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/SnapKit.xcscheme create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/LICENSE create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/README.md create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/Constraint.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintAttributes.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintConfig.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintConstantTarget.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDSL.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDescription.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDirectionalInsetTarget.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDirectionalInsets.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintInsetTarget.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintInsets.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintItem.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutGuide+Extensions.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutGuide.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutGuideDSL.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutSupport.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutSupportDSL.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMaker.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerEditable.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerExtendable.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerFinalizable.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerPrioritizable.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerRelatable+Extensions.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerRelatable.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMultiplierTarget.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintOffsetTarget.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintPriority.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintPriorityTarget.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintRelatableTarget.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintRelation.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintView+Extensions.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintView.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintViewDSL.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/Debugging.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/LayoutConstraint.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/LayoutConstraintItem.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/Typealiases.swift create mode 100644 jaem/week7/CatStaGram/Pods/SnapKit/Sources/UILayoutSupport+Extensions.swift create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-Info.plist create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-dummy.m create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-prefix.pch create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-umbrella.h create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit.debug.xcconfig create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit.modulemap create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit.release.xcconfig create mode 100644 jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.pbxproj create mode 100644 jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/Assets.xcassets/Contents.json create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/Base.lproj/LaunchScreen.storyboard create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/HomeVC.swift create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/Info.plist create mode 100644 jaem/week9/MemorizeGame/MemorizeGameTests/MemorizeGameTests.swift create mode 100644 jaem/week9/MemorizeGame/MemorizeGameUITests/MemorizeGameUITests.swift create mode 100644 jaem/week9/MemorizeGame/MemorizeGameUITests/MemorizeGameUITestsLaunchTests.swift diff --git a/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.pbxproj b/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.pbxproj index 5ee0c35..908eba1 100644 --- a/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.pbxproj +++ b/jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.pbxproj @@ -21,6 +21,11 @@ BC0DC51727F895650045EFD2 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC51627F895650045EFD2 /* UIViewExtension.swift */; }; BC0DC51B27F896580045EFD2 /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC51A27F896580045EFD2 /* UIViewController+Extension.swift */; }; BC0DC51D27F89A000045EFD2 /* UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0DC51C27F89A000045EFD2 /* UserInfo.swift */; }; + BC2E13732838AF950009B7CF /* ReelsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2E13722838AF950009B7CF /* ReelsViewController.swift */; }; + BC2E137B2838BCB90009B7CF /* ReelsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2E137A2838BCB90009B7CF /* ReelsCell.swift */; }; + BC2E137D2838BF4E0009B7CF /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2E137C2838BF4E0009B7CF /* VideoPlayerView.swift */; }; + BC2E137E2838C20C0009B7CF /* video1.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = BC2E13782838BAA20009B7CF /* video1.mp4 */; }; + BC2E137F2838C20E0009B7CF /* video2.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = BC2E13792838BAA20009B7CF /* video2.mp4 */; }; BC46D8FC281D8AF0000B2BF5 /* PostCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC46D8FA281D8AF0000B2BF5 /* PostCollectionViewCell.swift */; }; BC46D8FD281D8AF0000B2BF5 /* PostCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC46D8FB281D8AF0000B2BF5 /* PostCollectionViewCell.xib */; }; BC984672281D70050092270E /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC984671281D70050092270E /* ProfileViewController.swift */; }; @@ -82,6 +87,11 @@ BC0DC51627F895650045EFD2 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; BC0DC51A27F896580045EFD2 /* UIViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; BC0DC51C27F89A000045EFD2 /* UserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfo.swift; sourceTree = ""; }; + BC2E13722838AF950009B7CF /* ReelsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReelsViewController.swift; sourceTree = ""; }; + BC2E13782838BAA20009B7CF /* video1.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; name = video1.mp4; path = ../../../../../../../Downloads/video1.mp4; sourceTree = ""; }; + BC2E13792838BAA20009B7CF /* video2.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; name = video2.mp4; path = ../../../../../../../Downloads/video2.mp4; sourceTree = ""; }; + BC2E137A2838BCB90009B7CF /* ReelsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReelsCell.swift; sourceTree = ""; }; + BC2E137C2838BF4E0009B7CF /* VideoPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = ""; }; BC46D8FA281D8AF0000B2BF5 /* PostCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCollectionViewCell.swift; sourceTree = ""; }; BC46D8FB281D8AF0000B2BF5 /* PostCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PostCollectionViewCell.xib; sourceTree = ""; }; BC984671281D70050092270E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; @@ -165,6 +175,11 @@ BC0DC51627F895650045EFD2 /* UIViewExtension.swift */, BC0DC51A27F896580045EFD2 /* UIViewController+Extension.swift */, BC0DC51C27F89A000045EFD2 /* UserInfo.swift */, + BC2E13722838AF950009B7CF /* ReelsViewController.swift */, + BC2E13782838BAA20009B7CF /* video1.mp4 */, + BC2E13792838BAA20009B7CF /* video2.mp4 */, + BC2E137A2838BCB90009B7CF /* ReelsCell.swift */, + BC2E137C2838BF4E0009B7CF /* VideoPlayerView.swift */, ); path = CatStaGram; sourceTree = ""; @@ -444,6 +459,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + BC2E137E2838C20C0009B7CF /* video1.mp4 in Resources */, BC46D8FD281D8AF0000B2BF5 /* PostCollectionViewCell.xib in Resources */, BC0DC4EF27F84A030045EFD2 /* LaunchScreen.storyboard in Resources */, BCE2ACAA280AE8A7007ABF95 /* FeedTableViewCell.xib in Resources */, @@ -451,6 +467,7 @@ BC984677281D74110092270E /* ProfileCollectionViewCell.xib in Resources */, BCE2ACAE280B013B007ABF95 /* StoryTableViewCell.xib in Resources */, BCE2ACB2280B044C007ABF95 /* StoryCollectionViewCell.xib in Resources */, + BC2E137F2838C20E0009B7CF /* video2.mp4 in Resources */, BC0DC4EA27F84A030045EFD2 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -518,6 +535,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BC2E137B2838BCB90009B7CF /* ReelsCell.swift in Sources */, BC0DC51B27F896580045EFD2 /* UIViewController+Extension.swift in Sources */, BC0DC51727F895650045EFD2 /* UIViewExtension.swift in Sources */, BCCC9172282F8E3D00914BD5 /* FeedAPIInput.swift in Sources */, @@ -528,6 +546,7 @@ BCE2ACA5280AE197007ABF95 /* HomeViewController.swift in Sources */, BC0DC4E327F84A030045EFD2 /* AppDelegate.swift in Sources */, BCCC91852830C80100914BD5 /* UserFeedDataManager.swift in Sources */, + BC2E137D2838BF4E0009B7CF /* VideoPlayerView.swift in Sources */, BC984672281D70050092270E /* ProfileViewController.swift in Sources */, BC0DC4E527F84A030045EFD2 /* SceneDelegate.swift in Sources */, BC984676281D74110092270E /* ProfileCollectionViewCell.swift in Sources */, @@ -540,6 +559,7 @@ BC46D8FC281D8AF0000B2BF5 /* PostCollectionViewCell.swift in Sources */, BCCC91832830C39C00914BD5 /* UserFeedModel.swift in Sources */, BCE2ACAD280B013B007ABF95 /* StoryTableViewCell.swift in Sources */, + BC2E13732838AF950009B7CF /* ReelsViewController.swift in Sources */, BCCC9174282F914F00914BD5 /* FeedModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/jaem/week7/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist b/jaem/week7/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist index e0e54bb..a0cc75f 100644 --- a/jaem/week7/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/jaem/week7/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ CatStaGram.xcscheme_^#shared#^_ orderHint - 3 + 4 diff --git a/jaem/week7/CatStaGram/CatStaGram.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week7/CatStaGram/CatStaGram.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate index e3eb89479c7ae0097db1df993f96dcd141c566c9..f015b3eff32eafc17109320deab4b1fd4d61363a 100644 GIT binary patch literal 62479 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 literal 60381 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 + @@ -494,6 +495,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week7/CatStaGram/CatStaGram/ReelsCell.swift b/jaem/week7/CatStaGram/CatStaGram/ReelsCell.swift index 1830194..02ef508 100644 --- a/jaem/week7/CatStaGram/CatStaGram/ReelsCell.swift +++ b/jaem/week7/CatStaGram/CatStaGram/ReelsCell.swift @@ -5,4 +5,99 @@ // Created by 송재민 on 2022/05/21. // -import Foundation +import UIKit +import SnapKit +import AVKit + +class ReelsCell: UICollectionViewCell{ + static let identifier = "ReelsCell" + + var videoView: VideoPlayerView? + + let cellTitleLabel = UILabel() + + let cameraImageView = UIImageView() + + let commentImageView = UIImageView() + + let likeImageView = UIImageView() + + let shareImageView = UIImageView() + + override init(frame: CGRect){ + super.init(frame: frame) + } + + required init?(coder: NSCoder){ + fatalError("init(coder:) has not been implemented") + } + + public func setupURL(_ urlStr: String){ + self.videoView = VideoPlayerView(frame: .zero, urlStr: urlStr) + setupAttribute() + setupLayout() + } + + private func setupAttribute(){ + cellTitleLabel.text = "릴스" + cellTitleLabel.font = .boldSystemFont(ofSize: 25) + cellTitleLabel.textAlignment = .left + cellTitleLabel.textColor = .white + + [cameraImageView, shareImageView, likeImageView, commentImageView] + .forEach{ + $0.contentMode = .scaleAspectFit + $0.tintColor = .white + } + + cameraImageView.image = UIImage(systemName: "camera") + shareImageView.image = UIImage(systemName: "paperplane") + likeImageView.image = UIImage(systemName: "suit.heart") + commentImageView.image = UIImage(systemName: "message") + } + + private func setupLayout(){ + guard let videoView = videoView else {return} + contentView.addSubview(videoView) + + videoView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + [cellTitleLabel, cameraImageView, + likeImageView, + commentImageView, + shareImageView] + .forEach{contentView.addSubview($0)} + + cellTitleLabel.snp.makeConstraints{make in + make.top.equalToSuperview().offset(20) + make.leading.equalToSuperview().offset(20) + } + + cameraImageView.snp.makeConstraints{make in + make.top.equalToSuperview().offset(20) + make.trailing.equalToSuperview().offset(-20) + } + + shareImageView.snp.makeConstraints{make in + make.bottom.equalToSuperview().offset(-50) + make.trailing.trailing.equalToSuperview().offset(-20) + make.width.height.equalTo(35) + } + + commentImageView.snp.makeConstraints{make in + make.bottom.equalTo(shareImageView.snp.top).offset(-10) + make.centerX.equalTo(shareImageView) + make.width.height.equalTo(35) + } + + likeImageView.snp.makeConstraints{make in + make.bottom.equalTo(commentImageView.snp.top).offset(-10) + make.centerX.equalTo(shareImageView) + make.width.height.equalTo(35) + } + + } + +} diff --git a/jaem/week7/CatStaGram/CatStaGram/ReelsViewController.swift b/jaem/week7/CatStaGram/CatStaGram/ReelsViewController.swift index 3503487..a2943e7 100644 --- a/jaem/week7/CatStaGram/CatStaGram/ReelsViewController.swift +++ b/jaem/week7/CatStaGram/CatStaGram/ReelsViewController.swift @@ -8,22 +8,79 @@ import UIKit class ReelsViewController: UIViewController { - + //MARK: - Properties + @IBOutlet weak var collectionView: UICollectionView! + private var nowPage = 0 + + private let videoURLStrArr = ["video1", "video2"] + + //MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() - // Do any additional setup after loading the view. + setupCollectionView() } + //MARK: - Actions + + //MARK: - Helpers + private func setupCollectionView(){ + collectionView.delegate = self + collectionView.dataSource = self + collectionView.decelerationRate = .fast + collectionView.register(ReelsCell.self, forCellWithReuseIdentifier: ReelsCell.identifier) + + starLoop() + } + + private func starLoop() { + let _ = Timer.scheduledTimer(withTimeInterval: 2, repeats: true){ _ in + self.moveNextPage() + } + } + + private func moveNextPage() { + let itemCount = collectionView.numberOfItems(inSection: 0) + + nowPage += 1 + + if(nowPage >= itemCount){ + nowPage = 0 + } + + collectionView.scrollToItem( + at: IndexPath(item: nowPage, section: 0), at: .centeredVertically, animated: true) + } + +} - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. +extension ReelsViewController: UICollectionViewDelegate, UICollectionViewDataSource{ + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + 12 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ReelsCell.identifier, for: indexPath) as? ReelsCell else {return UICollectionViewCell()} + cell.setupURL(videoURLStrArr.randomElement()!) + + return cell } - */ + + func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + + if let cell = collectionView.cellForItem(at: indexPath) as? ReelsCell{ + cell.videoView?.cleanup() + } + } +} +extension ReelsViewController: UICollectionViewDelegateFlowLayout{ + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize( + width: collectionView.frame.width, height: collectionView.frame.height) + } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + 1 + } } diff --git a/jaem/week7/CatStaGram/CatStaGram/VideoPlayerView.swift b/jaem/week7/CatStaGram/CatStaGram/VideoPlayerView.swift index e1614fe..7227105 100644 --- a/jaem/week7/CatStaGram/CatStaGram/VideoPlayerView.swift +++ b/jaem/week7/CatStaGram/CatStaGram/VideoPlayerView.swift @@ -5,4 +5,46 @@ // Created by 송재민 on 2022/05/21. // -import Foundation +import UIKit +import AVKit + +class VideoPlayerView: UIView{ + var playerLayer: AVPlayerLayer? + var playerLooper: AVPlayerLooper? + var queuePlayer: AVQueuePlayer? + var urlStr: String + + init(frame: CGRect, urlStr: String){ + self.urlStr = urlStr + super.init(frame: frame) + + let videoFileURL = Bundle.main.url(forResource: urlStr, withExtension: "mp4")! + let playItem = AVPlayerItem(url: videoFileURL) + + self.queuePlayer = AVQueuePlayer(playerItem: playItem) + playerLayer = AVPlayerLayer() + + playerLayer!.player = queuePlayer + playerLayer!.videoGravity = .resizeAspectFill + + self.layer.addSublayer(playerLayer!) + + playerLooper = AVPlayerLooper(player: queuePlayer!, templateItem: playItem) + queuePlayer!.play() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func cleanup(){ + queuePlayer?.pause() + queuePlayer?.removeAllItems() + queuePlayer = nil + } + + override func layoutSubviews() { + super.layoutSubviews() + playerLayer!.frame = bounds + } +} diff --git a/jaem/week7/CatStaGram/Podfile b/jaem/week7/CatStaGram/Podfile index 6bab783..edc4f83 100644 --- a/jaem/week7/CatStaGram/Podfile +++ b/jaem/week7/CatStaGram/Podfile @@ -8,4 +8,5 @@ target 'CatStaGram' do # Pods for CatStaGram pod 'Alamofire' pod 'Kingfisher', '~> 7.0' + pod 'SnapKit', '~> 5.6.0' end diff --git a/jaem/week7/CatStaGram/Podfile.lock b/jaem/week7/CatStaGram/Podfile.lock index 1046ca6..415d854 100644 --- a/jaem/week7/CatStaGram/Podfile.lock +++ b/jaem/week7/CatStaGram/Podfile.lock @@ -1,20 +1,24 @@ PODS: - Alamofire (5.6.1) - Kingfisher (7.2.2) + - SnapKit (5.6.0) DEPENDENCIES: - Alamofire - Kingfisher (~> 7.0) + - SnapKit (~> 5.6.0) SPEC REPOS: trunk: - Alamofire - Kingfisher + - SnapKit SPEC CHECKSUMS: Alamofire: 87bd8c952f9a4454320fce00d9cc3de57bcadaf5 Kingfisher: 184d4d1a8c36666e663caf8e08abe87898595c53 + SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25 -PODFILE CHECKSUM: fd492e0a81b684bf01518e165eb2e09de28a0ee6 +PODFILE CHECKSUM: aa3d043327ac9b01db6e64909443a3cca7b3245f COCOAPODS: 1.11.3 diff --git a/jaem/week7/CatStaGram/Pods/Manifest.lock b/jaem/week7/CatStaGram/Pods/Manifest.lock index 1046ca6..415d854 100644 --- a/jaem/week7/CatStaGram/Pods/Manifest.lock +++ b/jaem/week7/CatStaGram/Pods/Manifest.lock @@ -1,20 +1,24 @@ PODS: - Alamofire (5.6.1) - Kingfisher (7.2.2) + - SnapKit (5.6.0) DEPENDENCIES: - Alamofire - Kingfisher (~> 7.0) + - SnapKit (~> 5.6.0) SPEC REPOS: trunk: - Alamofire - Kingfisher + - SnapKit SPEC CHECKSUMS: Alamofire: 87bd8c952f9a4454320fce00d9cc3de57bcadaf5 Kingfisher: 184d4d1a8c36666e663caf8e08abe87898595c53 + SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25 -PODFILE CHECKSUM: fd492e0a81b684bf01518e165eb2e09de28a0ee6 +PODFILE CHECKSUM: aa3d043327ac9b01db6e64909443a3cca7b3245f COCOAPODS: 1.11.3 diff --git a/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/project.pbxproj b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/project.pbxproj index b22863b..b98c6b0 100644 --- a/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/project.pbxproj +++ b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/project.pbxproj @@ -7,262 +7,354 @@ objects = { /* Begin PBXBuildFile section */ - 005B319B494ED2DAA239B9939A504DFC /* Alamofire-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D167A016457E1A5BD5B9A73C4FF9434 /* Alamofire-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 00BEA6029C428FEE644AC3D42AD83282 /* ImagePrefetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 240168000C008585397987F341FC9AFA /* ImagePrefetcher.swift */; }; - 0285857A24F66E925987A5876F0BE63B /* ImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 597967B6CDA2E72751463CF56198E927 /* ImageDataProvider.swift */; }; - 045DE6EBF9B2F63F60F5BE60C1198E06 /* RedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 499955C35F6D4A8F96E940FF129FF498 /* RedirectHandler.swift */; }; - 04A896288CE3A59B530250337A5F8362 /* Result+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3F986AF94568CEEB8AF631C72F03B2 /* Result+Alamofire.swift */; }; - 0510E8EA51914CB2176AD0F173937FAB /* KFImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26990E9D251C57CAB34BC5C9EC37033 /* KFImageRenderer.swift */; }; - 05228565AAA7FCED4BAFB2B7EF71D53D /* KingfisherOptionsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8B2E44706F2E23444FC30F3BAEE3DC /* KingfisherOptionsInfo.swift */; }; - 059639E700DEFAEF08F56484E5F67BE7 /* NSButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6E93449DD3CC778DD5D443C7780D895 /* NSButton+Kingfisher.swift */; }; - 082EDC820D76DF95C71A5018112DE512 /* UIButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9B50C2ED49A8E73A4D64C205173820 /* UIButton+Kingfisher.swift */; }; - 0ED8FBFD9A86D21BF69137EC9350E575 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9ECAD406B234DD522B04457BCADA54 /* SessionDelegate.swift */; }; + 005B319B494ED2DAA239B9939A504DFC /* Alamofire-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 85174CF7F69E6FCA3002CE90EB305644 /* Alamofire-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 00A2749F46C967ED4725A32357E3FB1B /* ConstraintLayoutSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C39CAEDC4116F60F5D9A988BBCBA88 /* ConstraintLayoutSupport.swift */; }; + 00BEA6029C428FEE644AC3D42AD83282 /* ImagePrefetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285C69FC27CB96D64460B0BD1DCFFFDC /* ImagePrefetcher.swift */; }; + 0285857A24F66E925987A5876F0BE63B /* ImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13F847746EF4F7AE09B3A747BA544B5 /* ImageDataProvider.swift */; }; + 045DE6EBF9B2F63F60F5BE60C1198E06 /* RedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D24A24D936AA5A0801D5BC8B6EC130 /* RedirectHandler.swift */; }; + 04637C70546B34F93C3A1D79C3F78B37 /* ConstraintDirectionalInsetTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1AD755643055311B24F71017C844D2D /* ConstraintDirectionalInsetTarget.swift */; }; + 04A896288CE3A59B530250337A5F8362 /* Result+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A35F58FA9F1861DD89BD76A1FB6912D /* Result+Alamofire.swift */; }; + 0510E8EA51914CB2176AD0F173937FAB /* KFImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F448016C8356D578FEDC3FF0468E0949 /* KFImageRenderer.swift */; }; + 05228565AAA7FCED4BAFB2B7EF71D53D /* KingfisherOptionsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CF22E24F8E9F6CCEE9CFA8E515A697C /* KingfisherOptionsInfo.swift */; }; + 0591132B5EA1BE4DDA268D8A9C3D0421 /* LayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F42A45BF9A88CF234C5C8B1645A4BEB /* LayoutConstraint.swift */; }; + 059639E700DEFAEF08F56484E5F67BE7 /* NSButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B6FD901E48869144B91F5E03417982 /* NSButton+Kingfisher.swift */; }; + 06E43B4751069B47B3BD4AFD936A57E3 /* ConstraintPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178259530DD4732FDC78DC27F9D1452E /* ConstraintPriority.swift */; }; + 082EDC820D76DF95C71A5018112DE512 /* UIButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A5C0DFC63B97534F154442D36FA9B8 /* UIButton+Kingfisher.swift */; }; + 0833E66E7F19849322305D67777B77DB /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */; }; + 0B994CDC79B1AD3A7BE62490D27C60C8 /* ConstraintAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0EDA61BA557420CA7C3E24BE64E495E /* ConstraintAttributes.swift */; }; + 0ED8FBFD9A86D21BF69137EC9350E575 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CA42D477EB2ADDC226A4709E7B38A7 /* SessionDelegate.swift */; }; 0F4037DBF307AC8058BD0A3D35C7E7E9 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */; }; - 1185A2B40E14F2FCBC761FC99777CAD8 /* ExtensionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365099CC49560F0DAFBF90D1A7FBFD24 /* ExtensionHelpers.swift */; }; - 122980E44B15C64CF0B14DC94D7EB5C9 /* Kingfisher-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 6C321B1DFC61DFB5E1EB0BF63C4E7B05 /* Kingfisher-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 18E839CC4FD7F1CD252F4BCA4C70BED1 /* Pods-CatStaGram-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = EE2747566D1A67FB4BBC1883CC9ACE4E /* Pods-CatStaGram-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 1952485AFF7A1BCCA4D4B142E82FE627 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06442349E67A8F1944EBFB63B46B406E /* AnimatedImageView.swift */; }; - 1976BB7D7E26A12E29283E71154B63B3 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B42FDB98681E0F2B54A53C6263FC0FB7 /* SessionDelegate.swift */; }; - 1AA89F327105C026976BF6E831B193A2 /* ImageBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE0A9D42161F3A6512AEE4A7F63B4A0 /* ImageBinder.swift */; }; - 1EE44196E7BCE57AD96A2C751651EF40 /* AlamofireExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72447455A0B451F260AE94E750E590D7 /* AlamofireExtended.swift */; }; - 1FD2928BC156D990D68B105F518C60B6 /* MemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2601AB15F49167B95C5DDC2FBC7B78 /* MemoryStorage.swift */; }; - 1FE693B5ACC6AD7320CEFC20B64546E4 /* KingfisherManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7AD2807A19FC11299041D8A8777267 /* KingfisherManager.swift */; }; - 1FEE89BF952BE7ACA46E642DA2E48CA2 /* DiskStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D362E01704CD82D45E9C7857FB20AC1E /* DiskStorage.swift */; }; - 22216C300C763044344B9DBF97317E63 /* RetryStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F40A9A287AE4680FD4A041AAD0C5ED /* RetryStrategy.swift */; }; - 22BD1346F66BFCB129AAA44EEF322AC9 /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BFD9705B9A22E25BBBD0A608C90CCC /* Resource.swift */; }; - 243D7CFE1D56ED80ACB2B3E71B4CB603 /* AuthenticationChallengeResponsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DBC7F03013C95C65EE295C4DAC2A64 /* AuthenticationChallengeResponsable.swift */; }; - 25FC036BEA33CAB5D80F5A41644535D3 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCDFE9C10374511CCC375D2E99C812E /* Storage.swift */; }; - 29FF13E23FD52E46D30530549410AD7C /* ImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1FA3BADCA4576957512EC8D60D2B76 /* ImageTransition.swift */; }; - 2BE89C24BFD3FB663E37C607C289B3B6 /* RedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C1BAC7555E3D0AC24A397BB028A3CEC /* RedirectHandler.swift */; }; - 2CBE3651CA006E19F5D64A2DE9B9A028 /* CachedResponseHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A117735F022F30BB9DC641136E51C9 /* CachedResponseHandler.swift */; }; - 2CCD13099063CD560E3067BD132914FA /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC710AFB6066D8535108054FD1115E94 /* Notifications.swift */; }; - 33A7D0F2D03004CE256A75E03DF33C70 /* RetryPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 715993D57B4975C3CF94C6BC997595C6 /* RetryPolicy.swift */; }; + 1185A2B40E14F2FCBC761FC99777CAD8 /* ExtensionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD33114F6764F686C1A80233A8A1D34 /* ExtensionHelpers.swift */; }; + 122980E44B15C64CF0B14DC94D7EB5C9 /* Kingfisher-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 77870AC54264011D51C6F35408B2B263 /* Kingfisher-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 183D8B3E057B885EA7DF9A8CDCCE9029 /* ConstraintMakerRelatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A8F7936CB5C19BE80BD3356E614D52 /* ConstraintMakerRelatable.swift */; }; + 1952485AFF7A1BCCA4D4B142E82FE627 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46CBB1E6E5F76A4B848491596108E707 /* AnimatedImageView.swift */; }; + 1976BB7D7E26A12E29283E71154B63B3 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8D9CBFE45052D73C5F502FB595489BE /* SessionDelegate.swift */; }; + 1AA89F327105C026976BF6E831B193A2 /* ImageBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B465AC0964110BAEAFCF634EF8832801 /* ImageBinder.swift */; }; + 1EE44196E7BCE57AD96A2C751651EF40 /* AlamofireExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 907DC9525547D14B924F35B4D6792664 /* AlamofireExtended.swift */; }; + 1FD2928BC156D990D68B105F518C60B6 /* MemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B76FA6AD6CFF53A0BE51BE13C1109C2 /* MemoryStorage.swift */; }; + 1FE693B5ACC6AD7320CEFC20B64546E4 /* KingfisherManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA3F91769ED2D36CBFCA9056FCE2A63 /* KingfisherManager.swift */; }; + 1FEE89BF952BE7ACA46E642DA2E48CA2 /* DiskStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDE8D838F7402500B258EECED27B9C2 /* DiskStorage.swift */; }; + 220D6AC9B1B1EC8EFFB204F9C1EAC842 /* ConstraintMakerEditable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A4A070E2855042F335EFD9E5E195C5B /* ConstraintMakerEditable.swift */; }; + 22216C300C763044344B9DBF97317E63 /* RetryStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4852E3BB7086025361AA0A3FF25346E /* RetryStrategy.swift */; }; + 22BD1346F66BFCB129AAA44EEF322AC9 /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C0C598F4D69737485E825CF3EB38E2 /* Resource.swift */; }; + 243D7CFE1D56ED80ACB2B3E71B4CB603 /* AuthenticationChallengeResponsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD3326917FBC47DA75A7A9DF4A8D68B /* AuthenticationChallengeResponsable.swift */; }; + 256558233B40ACA6818F143BBC5B8017 /* ConstraintViewDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = F566C7CBE07FDFA59C7103FF1EFF3896 /* ConstraintViewDSL.swift */; }; + 25FC036BEA33CAB5D80F5A41644535D3 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBD249D56D7905164D61C4DAC613E264 /* Storage.swift */; }; + 29FF13E23FD52E46D30530549410AD7C /* ImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7041BB19442F3ACDB15B140F458F078 /* ImageTransition.swift */; }; + 2BE89C24BFD3FB663E37C607C289B3B6 /* RedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28BDE3A9FE93A1357F5CD74F1552342 /* RedirectHandler.swift */; }; + 2CBE3651CA006E19F5D64A2DE9B9A028 /* CachedResponseHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74E82B4D26EE55A623511D1FEF6913B1 /* CachedResponseHandler.swift */; }; + 2CCD13099063CD560E3067BD132914FA /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 797263D3E571383C88815218DCE6B4CC /* Notifications.swift */; }; + 33A7D0F2D03004CE256A75E03DF33C70 /* RetryPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC572DB04F27228F228CA58C17E9016 /* RetryPolicy.swift */; }; + 3660B4F629053ABC3C1DF69366770289 /* ConstraintConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68BB2620AEB63F8E9B3D27DBBCFE300D /* ConstraintConfig.swift */; }; + 38B9D58E94D9C1CECD7E381C64A2329D /* UILayoutSupport+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1F0BB5484DE62961655DC057183C5F /* UILayoutSupport+Extensions.swift */; }; 3AD5DBB915C2623991F7DBACD173BBB4 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */; }; - 3AF7DB9AEFF47F1F7F91AF28440E4AC6 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A723238162323CE3B380FA8E894C865 /* Filter.swift */; }; - 3C4059621E23842C19D4EB5D35B41989 /* Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67030433EFF4B0333EC8811BC7EC47D6 /* Validation.swift */; }; + 3AF7DB9AEFF47F1F7F91AF28440E4AC6 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBBF83A19240F77CB8282A425738F57 /* Filter.swift */; }; + 3C4059621E23842C19D4EB5D35B41989 /* Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C7082784768E1C6C01AF820DFEFDF1 /* Validation.swift */; }; 420C200A05BB29E1D299D1BADE9139D2 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 872D7EFA572ECEE8EF993C27196E16DD /* CFNetwork.framework */; }; - 457BE444ED617FA7D6851D6DAA9D7234 /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B347AF8E0B52CE83F1B9A8F51F1AE31 /* Delegate.swift */; }; - 46A64A43AFA057B6B63C8F0C12F509B4 /* Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = B114DBD20B001B2A9D93F8F8C47903B6 /* Combine.swift */; }; - 4DCA9775E5CCF599460BDB46E77F6FA4 /* Kingfisher-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 6835FBF931D5C31EFFF83ED559436CDE /* Kingfisher-dummy.m */; }; - 4F37E521D341C47CE73DDCF21BA95A52 /* KingfisherError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3CD3E23CCF124B01CA690E27E72D1B9 /* KingfisherError.swift */; }; - 506128E1CC424E40E2691546D9547549 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C6C8F94FCB9064F02B48B607AB69962 /* Placeholder.swift */; }; - 509490FB1D30FEC59AE4BC21AEEBB7BB /* RequestModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C264879E6B16EA9E07F073E24220A232 /* RequestModifier.swift */; }; - 55AABB1FB38F61A3369ACC555FF3046D /* Alamofire-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D13E09327BDC573DE5AAFE079172AB9 /* Alamofire-dummy.m */; }; - 57FC31B14C753B5C63CEF00560F8A6EF /* SizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CBA959C728F5B6931C11DE61853D88 /* SizeExtensions.swift */; }; - 582D59E0D2EF62E0575933C99B393704 /* GraphicsContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D370E0D48C1925AC14D155BEDF3DC7D4 /* GraphicsContext.swift */; }; - 59BC9047F4BEBBC06235608D974E230D /* NSTextAttachment+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EBE61AE143E2DB816492EB19AA1651F /* NSTextAttachment+Kingfisher.swift */; }; - 5ADB30DD9A03859018550A999ACB0652 /* KFImageOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BAF59EA7A7DD980421989D2A205E6B0 /* KFImageOptions.swift */; }; - 5E27DD292D3A55657712DD7AFA7B8FCA /* KFImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9566B136FA420BDAAE8B272AE10CFE3E /* KFImage.swift */; }; - 5F852F38CBC282496CCBE37C51324B2F /* ImageProgressive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7366FB0DEF319ED32BAF9DE5654C8DB7 /* ImageProgressive.swift */; }; - 68FB2DCB4C77DBCAF9A6037E470F2BDE /* ParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 249A902C9828CB270329EFFAFAC1843A /* ParameterEncoding.swift */; }; - 70FEC06F54286257E1BA1ECA0C99198D /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16E37EF24D9E916BB47E43D8E954D3AF /* Image.swift */; }; - 7483E5327027263F7E4B94A2997191C4 /* AuthenticationInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B7F2B2CCF604EF10790364BAB06391 /* AuthenticationInterceptor.swift */; }; - 75966A9262648D4647D764E3E76BC6AC /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB099A6B514875F22EF98798B56B4BA5 /* Response.swift */; }; - 7930C94414B4C661867AC4FBE82E996C /* URLEncodedFormEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072684B1154141462994E3AD2ACD3606 /* URLEncodedFormEncoder.swift */; }; + 42B6ACFCF650183030867CB3EF345E95 /* ConstraintMakerFinalizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEAE448343149DBD139FF6ECDFCB540D /* ConstraintMakerFinalizable.swift */; }; + 457BE444ED617FA7D6851D6DAA9D7234 /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799E181E909BC0CAE1806256859AB012 /* Delegate.swift */; }; + 46A64A43AFA057B6B63C8F0C12F509B4 /* Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BAA0652E6B2091F985485A914ED9663 /* Combine.swift */; }; + 4BAD99B7394E225CEDBF94B8100BCC7F /* ConstraintRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31CB56705A8EE8E7C074CD7E3D8F671B /* ConstraintRelation.swift */; }; + 4DA72FD7F1FB2C0449EDEF4B8A579807 /* ConstraintPriorityTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0238C1747B9EF2D7860DFC44E8C5B6 /* ConstraintPriorityTarget.swift */; }; + 4DCA9775E5CCF599460BDB46E77F6FA4 /* Kingfisher-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 67466EFA454A89EA2AFFBB49504EB9FC /* Kingfisher-dummy.m */; }; + 4F37E521D341C47CE73DDCF21BA95A52 /* KingfisherError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B0717827596EDE05BA395607E31C7FA /* KingfisherError.swift */; }; + 506128E1CC424E40E2691546D9547549 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C54FE1F16AF08A4A5E86EC037CBAC7A /* Placeholder.swift */; }; + 509490FB1D30FEC59AE4BC21AEEBB7BB /* RequestModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3933AD68D7D40D3391FD238893A9E80 /* RequestModifier.swift */; }; + 5380454C48A12A6E376122ABD8096968 /* ConstraintDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9910954DCC3CF80C42993711A0B19F /* ConstraintDescription.swift */; }; + 55AABB1FB38F61A3369ACC555FF3046D /* Alamofire-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7EC45995F1574DEA557A6E3BC05CFEEF /* Alamofire-dummy.m */; }; + 55E51F45F1E157D3B4942BA7252C277E /* ConstraintRelatableTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDE1C754F3F4878CA35D1AAE0FDF70EC /* ConstraintRelatableTarget.swift */; }; + 56A72F6D13D930C4A2568F24DEA33C8D /* ConstraintLayoutSupportDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23350F61A142EDC6837D0E4A8A5FC8EF /* ConstraintLayoutSupportDSL.swift */; }; + 56AC6DC6459AE47E7BC4AF06E5B148D2 /* ConstraintDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B784610AC76ECDEBDCB3AE749016F5 /* ConstraintDSL.swift */; }; + 57FC31B14C753B5C63CEF00560F8A6EF /* SizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0CC80E337D75B5AB065E2F5AE4ECA1 /* SizeExtensions.swift */; }; + 582D59E0D2EF62E0575933C99B393704 /* GraphicsContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF6D1092A6AEA823ADE61284D6A02F5 /* GraphicsContext.swift */; }; + 59BC9047F4BEBBC06235608D974E230D /* NSTextAttachment+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0026A67CFFD17285AF03B23D2DD28B2A /* NSTextAttachment+Kingfisher.swift */; }; + 5ADB30DD9A03859018550A999ACB0652 /* KFImageOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E1D44D618B316B6D82C72F94A26E240 /* KFImageOptions.swift */; }; + 5D36B99F3CAB1FC7337082D9581FECA6 /* Debugging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22CE6618974753838E7BAB54EBCE7DBF /* Debugging.swift */; }; + 5E27DD292D3A55657712DD7AFA7B8FCA /* KFImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82800F15627BA03BC43D65EBE817E6BD /* KFImage.swift */; }; + 5F852F38CBC282496CCBE37C51324B2F /* ImageProgressive.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B0124C50FC6B6BBE8A6278B8F6E983 /* ImageProgressive.swift */; }; + 604D3C93C17978C23600EC415949AB64 /* ConstraintInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B20F22CEC80124AF2B6C9DD78E40F2 /* ConstraintInsets.swift */; }; + 611CEF7FF1EF9EA1BCEF6C73EEE3ACCF /* ConstraintLayoutGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF26F23415267C306D13B6524C4FF622 /* ConstraintLayoutGuide.swift */; }; + 68FB2DCB4C77DBCAF9A6037E470F2BDE /* ParameterEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD59BED079FC427AE725FF857613539 /* ParameterEncoding.swift */; }; + 69DE75BA185BBAB5FDAA28321F3C849F /* ConstraintItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364987DCEA6E58F38028166AB008F9CD /* ConstraintItem.swift */; }; + 6AC1B881BB319C89AD023A02CDC8FC3D /* ConstraintLayoutGuide+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5397B5B4479B64B79C83FC512332BB86 /* ConstraintLayoutGuide+Extensions.swift */; }; + 6EAC8B6FA2D115DE708A58C601DD25E1 /* Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCA8F67D5E603B9D9888F34599FC6AAA /* Constraint.swift */; }; + 70FEC06F54286257E1BA1ECA0C99198D /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEE5E2F810B61452A5C02C51C487AEEF /* Image.swift */; }; + 712E2B43577DA59F7504EA4478C346D0 /* Pods-CatStaGram-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = EE2747566D1A67FB4BBC1883CC9ACE4E /* Pods-CatStaGram-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7483E5327027263F7E4B94A2997191C4 /* AuthenticationInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF4497480929BC383C9404C43E30479 /* AuthenticationInterceptor.swift */; }; + 75966A9262648D4647D764E3E76BC6AC /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F6FA35F52DD4E40C180C9B3FF358E0 /* Response.swift */; }; + 7930C94414B4C661867AC4FBE82E996C /* URLEncodedFormEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21674FEAC995EECAEE8116E61002B452 /* URLEncodedFormEncoder.swift */; }; 7B068137A8925891446203B5D3D6A4ED /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 872D7EFA572ECEE8EF993C27196E16DD /* CFNetwork.framework */; }; - 7C7418FF01DD7BB909719682B634A8A5 /* SessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C323A988742445270C1D6A4DD40ED07 /* SessionDataTask.swift */; }; - 7E02F5B62BE00E97847DF549FFED2490 /* HTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FAB3233CC80F28F7E66DF4DC9F2FA /* HTTPHeaders.swift */; }; - 7F1BB526AAE3ECDCE90127D9D0E10261 /* StringEncoding+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = D942B0884573FD857CFB17A94E6A5807 /* StringEncoding+Alamofire.swift */; }; - 7FE695DA8EE7FF1286556E06B692009B /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EE41735C363063D401BC89633A455BE /* MultipartFormData.swift */; }; - 7FFE4021A4F14124342AD41CE1117B3E /* KFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86DDDE6D887ED6DFE35529D771930C65 /* KFAnimatedImage.swift */; }; - 80738D8956C9987CCCEDF551961E5069 /* ImageDataProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D911DA9899C96EA4408220F4754219 /* ImageDataProcessor.swift */; }; - 808C960C82D708FC1A42C581D6CB4940 /* URLSessionConfiguration+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7407E80368EF1CE2A091B4F0F8F81FF /* URLSessionConfiguration+Alamofire.swift */; }; - 81B8D2B7CEB25C2448B0BC9B33591A65 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1939B0AEE888F2EB258D6832523F67C /* Session.swift */; }; - 824D816B1EE404F2DD400EE678695CBE /* ResponseSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C32A86B99F0C8B9E42B3D9C7F9730889 /* ResponseSerialization.swift */; }; - 881A35B28D93C56E46E305F6138B1A76 /* ImageDownloaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077E33C20C9BD4FAF66AE7A6EC1657F4 /* ImageDownloaderDelegate.swift */; }; - 8D75FC8D7476C9674234F39F1A820D8C /* URLConvertible+URLRequestConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404743B9B61FE5357078AFD049FC6B45 /* URLConvertible+URLRequestConvertible.swift */; }; - 99D058E53EFEE3AC4857CDE3DBA5C004 /* ParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A1EDFD24518B3F930192EDAE086842 /* ParameterEncoder.swift */; }; - 9B0A78AC22E7EDA755F51D86527E2D9C /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496EC73E3893F724BA57D993B154BF49 /* Source.swift */; }; - 9C9030DEDB0DF955B16FE08C50892D57 /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1529C2E8B334710FA50E4C9A7153006A /* Concurrency.swift */; }; - 9F5FE22DA95B66B8DC21CB13BE25EC9B /* WKInterfaceImage+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C444AA66DFF06EB1D1D714FE5939A17 /* WKInterfaceImage+Kingfisher.swift */; }; - A29100AA1876DDEFF6F54694A51FDB0E /* NetworkReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A043F9EF1DF150712ECDD2FB7CD2E46B /* NetworkReachabilityManager.swift */; }; - A316388A35648CB2987E761771456087 /* KFOptionsSetter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011604690A5B598427758F666A2F13C8 /* KFOptionsSetter.swift */; }; - A39D3555EC8B45B7D6B9505DDAF0F117 /* Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7A8F9A880D8CF92488BF36640F2DBBE /* Kingfisher.swift */; }; - A53BDE589BDD6483F3EEDCE5EA1DCCD3 /* Protected.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFFE766A6E0F3A621DFD9487DA3CAF7E /* Protected.swift */; }; - A88A844D5356E1690E445024CB796E09 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7006AE0546B679604D685DCA785C542C /* Result.swift */; }; - B25E07EA645911443A38DA1E68166156 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B7F4B3A6C95F8600D69F851451D63F6 /* Box.swift */; }; - B3658C29BBDE1033F6269A92E612CB30 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0341B04B64485596391C33DD9360D74E /* Request.swift */; }; - B6C05F9D9AF254B332EAAB3064F7F147 /* Pods-CatStaGram-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7DBB0D476F47E7A38CE0EDF85193CF41 /* Pods-CatStaGram-dummy.m */; }; - B704B198B9B520D449260877E300D821 /* ServerTrustEvaluation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04303942CB47C4C340CF398F8653BB98 /* ServerTrustEvaluation.swift */; }; - BC0ECA8F22DEDE8886E189CD0EAA1197 /* URLRequest+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB31688140758856323E0B3187769487 /* URLRequest+Alamofire.swift */; }; - BD382E78580D295D10100678D4F66A76 /* String+MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23963C2629645E475AB2931F62E451C7 /* String+MD5.swift */; }; - C9392394543C174FFBF84B243D562853 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */; }; - CD7AC3E1C98EA54F7C05C36C52805220 /* CacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC3A37B1FA2F4841F5E8CC3FC102A19 /* CacheSerializer.swift */; }; - CEBFFEED65D877702B2F36102528CF6D /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932C67B305CF8C8942FEBD2375BE2E1F /* EventMonitor.swift */; }; - D0EA90FBF83350C49E6EF6C8A98D6F00 /* AFError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80270ADB4ECE74E17A7DB57236035920 /* AFError.swift */; }; - D603AA58EF97D461A57B2B1BCB883868 /* AVAssetImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B74729EFA464A2B107050ED713601AB8 /* AVAssetImageDataProvider.swift */; }; - D6B4751CED01D53E4A1B6A571AAA2F83 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B56F1736D1127A32614C8CBBEF3E5F6 /* HTTPMethod.swift */; }; - DA34899BEF0668D76CBCE8C4CE47B97B /* RequestTaskMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0DD802295003561B6C5741521D716BC /* RequestTaskMap.swift */; }; - DAFC6CE6321395CF4523DD66DADBB9BA /* ImageDrawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7933A5F1F8BD20A48039AA759ADB465E /* ImageDrawing.swift */; }; - DBB8088E14A2ADEDB1CD840BAC835267 /* CPListItem+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142C2CAF3091DF534D3E7B4AAA4DFC0B /* CPListItem+Kingfisher.swift */; }; - DD72DC30CF19FFC81AB19CD0B074000D /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE76C5003FE080CF8820CA18C434EA3 /* ImageDownloader.swift */; }; - DD902FE8D6824681C929D028655AE121 /* RequestInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6599B0D2C1F4B263D4D859853F0F348A /* RequestInterceptor.swift */; }; - DE532EF7D50A9CF68587DAD4C1A02BD7 /* FormatIndicatedCacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8676B059D0C43C5A2FC81E0F55AD63D7 /* FormatIndicatedCacheSerializer.swift */; }; - DF4563832C19B8582C810BF502A5CA29 /* KF.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660CF05D9FA7E4371D6E6A45ED8EE01F /* KF.swift */; }; - DFCDE4638265B4CCD494ECA5D560DBEE /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5723DEFD772E79C36C19B268F7643CE0 /* Indicator.swift */; }; - E54654D504A42C24F284A68F87F7671D /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20EDDB802D0AEBA579A2FAC5B1B6BDE /* OperationQueue+Alamofire.swift */; }; - E5B664771063F1A9A372519A8466860B /* ImageView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 985C3C905A92749F5FB0325692FBF789 /* ImageView+Kingfisher.swift */; }; - E6D6C7D5E458A05CC736C340F853E9F6 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C65DBD1C381FE1E4AA74F824E85F325 /* ImageFormat.swift */; }; + 7C7418FF01DD7BB909719682B634A8A5 /* SessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEA1626758690136CDAF41CB95672E6 /* SessionDataTask.swift */; }; + 7E02F5B62BE00E97847DF549FFED2490 /* HTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81466D995C81B34DEE0A314B083C2D0 /* HTTPHeaders.swift */; }; + 7F1BB526AAE3ECDCE90127D9D0E10261 /* StringEncoding+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93AA7C2C96625A85AEA6FB6E673DDAD /* StringEncoding+Alamofire.swift */; }; + 7FE695DA8EE7FF1286556E06B692009B /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667CD2987F5017A87603DE7297D94774 /* MultipartFormData.swift */; }; + 7FFE4021A4F14124342AD41CE1117B3E /* KFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7389B8989D16FFDD6215BF99EAD4EF83 /* KFAnimatedImage.swift */; }; + 80738D8956C9987CCCEDF551961E5069 /* ImageDataProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99CBCB77AD5710D6D11812BAC486BD6E /* ImageDataProcessor.swift */; }; + 808C960C82D708FC1A42C581D6CB4940 /* URLSessionConfiguration+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B938A45380E77BF8ACD8EF3BA3276F /* URLSessionConfiguration+Alamofire.swift */; }; + 81B8D2B7CEB25C2448B0BC9B33591A65 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BFCE61ADED2F9A2CD76A78A188C0CA9 /* Session.swift */; }; + 824D816B1EE404F2DD400EE678695CBE /* ResponseSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E7079555F2E462A564E0B121EE2DFB /* ResponseSerialization.swift */; }; + 881A35B28D93C56E46E305F6138B1A76 /* ImageDownloaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E785A1D945BC47127CA4CE0DE7A0BA8 /* ImageDownloaderDelegate.swift */; }; + 8D75FC8D7476C9674234F39F1A820D8C /* URLConvertible+URLRequestConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 054BA59643B2127733DA8F8AAD1E4F5E /* URLConvertible+URLRequestConvertible.swift */; }; + 95E194A3AABB5407231E898B6686F73E /* ConstraintMakerRelatable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2643C189D74D592B92995979CC1BEA9D /* ConstraintMakerRelatable+Extensions.swift */; }; + 98F570DA48370453D648BD526FDAAEAA /* LayoutConstraintItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403E973BCBF657DC44AA8E268567E39C /* LayoutConstraintItem.swift */; }; + 99D058E53EFEE3AC4857CDE3DBA5C004 /* ParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D178B7CF7375C36FFB823F1493FB21FC /* ParameterEncoder.swift */; }; + 9B0A78AC22E7EDA755F51D86527E2D9C /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D33079D4E9609D5B2E73B0524B75E5 /* Source.swift */; }; + 9C9030DEDB0DF955B16FE08C50892D57 /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A45C0B52AFEB667ED0777CEBB9BA08 /* Concurrency.swift */; }; + 9C9548E8B4675954566202F7B87FF0E6 /* ConstraintConstantTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EED79B9CB8F66CBC3776943530D0C1 /* ConstraintConstantTarget.swift */; }; + 9E02CFFEFE1BEC978B512286F03D31DA /* ConstraintInsetTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 238EDDD638F2EDB57E54DDDB3B8BEACF /* ConstraintInsetTarget.swift */; }; + 9F5FE22DA95B66B8DC21CB13BE25EC9B /* WKInterfaceImage+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B2551386363716C6DB6FD94B490B81 /* WKInterfaceImage+Kingfisher.swift */; }; + A03FDA8BC5741880B1EF11AFD248EF48 /* ConstraintOffsetTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D722BE2B78702E0D89F4AD6CB7A08D9 /* ConstraintOffsetTarget.swift */; }; + A29100AA1876DDEFF6F54694A51FDB0E /* NetworkReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2CF2CC40AE2571056B934957097004E /* NetworkReachabilityManager.swift */; }; + A316388A35648CB2987E761771456087 /* KFOptionsSetter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FCA8DB37C4EAD7761A2AD3929FDDDD6 /* KFOptionsSetter.swift */; }; + A39D3555EC8B45B7D6B9505DDAF0F117 /* Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC749BFB582FB54281186F7C212DC04 /* Kingfisher.swift */; }; + A3E3AB7C765C21AE17B1E6EE6560040C /* SnapKit-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = F1A28E9E8E3211493299F42561988C41 /* SnapKit-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A53BDE589BDD6483F3EEDCE5EA1DCCD3 /* Protected.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25707AE1D4270D1D1FE989EF8068C8A7 /* Protected.swift */; }; + A5A8BF973BFE9C9304372A26C9F2E35B /* ConstraintMakerExtendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E1DF45629585C93475F05C4C705B54B /* ConstraintMakerExtendable.swift */; }; + A88A844D5356E1690E445024CB796E09 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 987D2724E2957A7A8BB654A970164F95 /* Result.swift */; }; + AC93F7D982E8056E781FDCFB59C24E11 /* Pods-CatStaGram-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7DBB0D476F47E7A38CE0EDF85193CF41 /* Pods-CatStaGram-dummy.m */; }; + B03B5579590D528CBA6D11FF638BC2AF /* Typealiases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E66FE8F126D7F6A70D6214DD496549 /* Typealiases.swift */; }; + B25E07EA645911443A38DA1E68166156 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B0C052B339AA2BDD72BBE9C7CEDFA3C /* Box.swift */; }; + B3658C29BBDE1033F6269A92E612CB30 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6A21BE8DA507842B3F12753CDD5716 /* Request.swift */; }; + B43017FEC99D227D4CDD81DD2C27D9D9 /* ConstraintMakerPrioritizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F22C681FC81C694E8D747D6AECBAEF8E /* ConstraintMakerPrioritizable.swift */; }; + B704B198B9B520D449260877E300D821 /* ServerTrustEvaluation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A3083E1E5289D29F023A8D6809C9837 /* ServerTrustEvaluation.swift */; }; + B752F7C4BECB65894B1F49421049CE5F /* ConstraintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DBF96DDFE3AFC890BE19FD063447453 /* ConstraintView.swift */; }; + BC0ECA8F22DEDE8886E189CD0EAA1197 /* URLRequest+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB0A90C89BD5F9CC88A1A769CF346458 /* URLRequest+Alamofire.swift */; }; + BD382E78580D295D10100678D4F66A76 /* String+MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4545CD1066BB1CDD9E98D073C6339E4B /* String+MD5.swift */; }; + BD47CB74CD9B2B4D2D942C9B65748DFB /* SnapKit-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 14CEEDA7A6DF1930A777B65955863011 /* SnapKit-dummy.m */; }; + CD7AC3E1C98EA54F7C05C36C52805220 /* CacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1EE76507C32C6532481BFCF619D224 /* CacheSerializer.swift */; }; + CEBFFEED65D877702B2F36102528CF6D /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E540405E3D1158FAFBAE4F9BDC7EE9 /* EventMonitor.swift */; }; + D0EA90FBF83350C49E6EF6C8A98D6F00 /* AFError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE64C5352AFEE75EB677A7284ABDE59 /* AFError.swift */; }; + D603AA58EF97D461A57B2B1BCB883868 /* AVAssetImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E472F69AB3AD6146CF5B1687599392 /* AVAssetImageDataProvider.swift */; }; + D6B4751CED01D53E4A1B6A571AAA2F83 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F44A08A5F580F49D5291C4A2BA8CCC /* HTTPMethod.swift */; }; + D7DC8082CC183FA721961479CA965627 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */; }; + DA34899BEF0668D76CBCE8C4CE47B97B /* RequestTaskMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27FBDA82588FFADCB73A08FDC1C0EED8 /* RequestTaskMap.swift */; }; + DAFC6CE6321395CF4523DD66DADBB9BA /* ImageDrawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8E227E931599A9136D2BB9EB051B6D6 /* ImageDrawing.swift */; }; + DBB8088E14A2ADEDB1CD840BAC835267 /* CPListItem+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B109BB831EAE85D48A06C6D4D8E375F /* CPListItem+Kingfisher.swift */; }; + DD72DC30CF19FFC81AB19CD0B074000D /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610292CBAE491B4D17CA51F66D6861EF /* ImageDownloader.swift */; }; + DD902FE8D6824681C929D028655AE121 /* RequestInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17E436AB454460A694A91918ABF6C0A /* RequestInterceptor.swift */; }; + DE532EF7D50A9CF68587DAD4C1A02BD7 /* FormatIndicatedCacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E972E5301F6A20A1F2F4A3F3FB5189DA /* FormatIndicatedCacheSerializer.swift */; }; + DF4563832C19B8582C810BF502A5CA29 /* KF.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86BD1B17487999591CD15009D638029 /* KF.swift */; }; + DFCDE4638265B4CCD494ECA5D560DBEE /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4022A8C85369D0ACA7E38670253E3D1 /* Indicator.swift */; }; + E54654D504A42C24F284A68F87F7671D /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268F34234A1960AFA99595BB83B710CE /* OperationQueue+Alamofire.swift */; }; + E5B664771063F1A9A372519A8466860B /* ImageView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA5130A1FDB34E3B4447038BEA687890 /* ImageView+Kingfisher.swift */; }; + E6D6C7D5E458A05CC736C340F853E9F6 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B43B2C8204242C97F8659DA9C709B3 /* ImageFormat.swift */; }; E719A3B025B9DACE693130120BD9B927 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52554E1C9731D5352FDE9E63F8C5466B /* Accelerate.framework */; }; - E9B4C89E7EB3B27D46AFCA452C3D426F /* MultipartUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BA6A6C6AEAC43638A2B248BBEA90EBD /* MultipartUpload.swift */; }; - EBB32304E8DD4BA115454E0050D47DED /* Runtime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CB85CAAF3329112CD2FFF9889E98FF7 /* Runtime.swift */; }; - ED0C8BA7560D7324587B353E0960479F /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE9B6094B04E75ACDBB73C3A194D690 /* ImageCache.swift */; }; - EEC150B66BCCD6C80FDA7E4D1975166B /* DispatchQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48BAA15044E6AD772F77A856FA5107F2 /* DispatchQueue+Alamofire.swift */; }; - EF9C4588CDA85AED8BBCF77451B2A35B /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95F29F7BBB1AA54AD679386E26C7740 /* ImageProcessor.swift */; }; - F17A4CA4664CABB331D39FE902E06843 /* Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76CC7FAE70D848424E201CB9C76CABD3 /* Alamofire.swift */; }; - F17B1F8F2B6580343025237455A29D61 /* TVMonogramView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4193FEA28CDEB3710C7C41537C8D5E2E /* TVMonogramView+Kingfisher.swift */; }; - F24021BDE9B42D604E3341CAD8E34759 /* GIFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF4F904B2EF1F1E836FADDE8C6B05DF /* GIFAnimatedImage.swift */; }; - F5414F8A5B40521D0E4AEEB28378CB49 /* ImageContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB10145B8616970B7B29BFF058CF910 /* ImageContext.swift */; }; - F54DE563418B1783D6EC491A0C3A05DB /* ImageModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07524CF920EFBC25C9A0C9B3E57AA118 /* ImageModifier.swift */; }; - F9537B023E24AC4A724E301F7E372491 /* KFImageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292F77B8C48397E58C30BC7A313423E4 /* KFImageProtocol.swift */; }; - FF09824309346665E2F1F7F5A45FB10F /* CallbackQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CCE8AF433C89864A9FE3C1F9C34A96 /* CallbackQueue.swift */; }; + E98DC8FB2578E76A812E0C94BA10E1B1 /* ConstraintDirectionalInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE918EBDB02B9B27E71402FCB954402 /* ConstraintDirectionalInsets.swift */; }; + E9B4C89E7EB3B27D46AFCA452C3D426F /* MultipartUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91586153A6E31A45B8184E55BDC2F04F /* MultipartUpload.swift */; }; + EBB32304E8DD4BA115454E0050D47DED /* Runtime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CF18FE28A2E0A8843038908E4A7410E /* Runtime.swift */; }; + ED0C8BA7560D7324587B353E0960479F /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF1A9204468240BA1A125C9445A0047 /* ImageCache.swift */; }; + EEC150B66BCCD6C80FDA7E4D1975166B /* DispatchQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5937AE3145C880EAA9D9560C432F6CC0 /* DispatchQueue+Alamofire.swift */; }; + EF21586EC3DFF6097A58EEC87376A0C1 /* ConstraintLayoutGuideDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C4DE86B5FA3554D2CE8756E7C62C05 /* ConstraintLayoutGuideDSL.swift */; }; + EF9C4588CDA85AED8BBCF77451B2A35B /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D4EE71842ECE47B9B60E26EC9158FE /* ImageProcessor.swift */; }; + F17A4CA4664CABB331D39FE902E06843 /* Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0148907B3A18C2DF9B3D355964E537C /* Alamofire.swift */; }; + F17B1F8F2B6580343025237455A29D61 /* TVMonogramView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A4130C4E6473352951BBE52AB721BC7 /* TVMonogramView+Kingfisher.swift */; }; + F24021BDE9B42D604E3341CAD8E34759 /* GIFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C138D9C30CB4F78726B11E277DCF1ED4 /* GIFAnimatedImage.swift */; }; + F5414F8A5B40521D0E4AEEB28378CB49 /* ImageContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E64283506C8735B64BD9C80EAC66F4 /* ImageContext.swift */; }; + F54DE563418B1783D6EC491A0C3A05DB /* ImageModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34CDE0FD157ECBE5A3270DB4E5818E36 /* ImageModifier.swift */; }; + F7C0960CEEB8F0C18F4503B405EAC08F /* ConstraintMultiplierTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F4E1D29D79341BA832E746E5BDCFFA /* ConstraintMultiplierTarget.swift */; }; + F9537B023E24AC4A724E301F7E372491 /* KFImageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F738ECB6F1F16EEF4863E1874544D4A0 /* KFImageProtocol.swift */; }; + F953AA9104BFE0C2DAD639EA60104A75 /* ConstraintView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923866AFC245CAED8B1E67CAE9A8256A /* ConstraintView+Extensions.swift */; }; + FAB03959C2357E325B19E08BC4775DAB /* ConstraintMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936CE1FB37015B4B9EA165AF51FA4947 /* ConstraintMaker.swift */; }; + FF09824309346665E2F1F7F5A45FB10F /* CallbackQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C549ABE4054C58BADD9499BFD01CB5CC /* CallbackQueue.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 86507F3308AD3FD588A9636FBE2FE558 /* PBXContainerItemProxy */ = { + 33A8CD687A4E5E0DF9CC5D157A31D598 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EAAA1AD3A8A1B59AB91319EE40752C6D; + remoteInfo = Alamofire; + }; + 5DA80A8427E962423623642D1CE4C8A9 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = E8022D22FAA6690B5E1C379C1BCE1491; remoteInfo = Kingfisher; }; - BADC5D608C035FEA12332200B1BEA3AA /* PBXContainerItemProxy */ = { + 950E4A232F336281C8F57BF7EE77C93D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; - remoteGlobalIDString = EAAA1AD3A8A1B59AB91319EE40752C6D; - remoteInfo = Alamofire; + remoteGlobalIDString = 19622742EBA51E823D6DAE3F8CDBFAD4; + remoteInfo = SnapKit; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 011604690A5B598427758F666A2F13C8 /* KFOptionsSetter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFOptionsSetter.swift; path = Sources/General/KFOptionsSetter.swift; sourceTree = ""; }; - 0341B04B64485596391C33DD9360D74E /* Request.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Request.swift; path = Source/Request.swift; sourceTree = ""; }; - 04303942CB47C4C340CF398F8653BB98 /* ServerTrustEvaluation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServerTrustEvaluation.swift; path = Source/ServerTrustEvaluation.swift; sourceTree = ""; }; - 06442349E67A8F1944EBFB63B46B406E /* AnimatedImageView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AnimatedImageView.swift; path = Sources/Views/AnimatedImageView.swift; sourceTree = ""; }; - 072684B1154141462994E3AD2ACD3606 /* URLEncodedFormEncoder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = URLEncodedFormEncoder.swift; path = Source/URLEncodedFormEncoder.swift; sourceTree = ""; }; - 07524CF920EFBC25C9A0C9B3E57AA118 /* ImageModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageModifier.swift; path = Sources/Networking/ImageModifier.swift; sourceTree = ""; }; - 077E33C20C9BD4FAF66AE7A6EC1657F4 /* ImageDownloaderDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDownloaderDelegate.swift; path = Sources/Networking/ImageDownloaderDelegate.swift; sourceTree = ""; }; - 0B347AF8E0B52CE83F1B9A8F51F1AE31 /* Delegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Delegate.swift; path = Sources/Utility/Delegate.swift; sourceTree = ""; }; - 0B7F4B3A6C95F8600D69F851451D63F6 /* Box.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Box.swift; path = Sources/Utility/Box.swift; sourceTree = ""; }; - 0BC3A37B1FA2F4841F5E8CC3FC102A19 /* CacheSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CacheSerializer.swift; path = Sources/Cache/CacheSerializer.swift; sourceTree = ""; }; - 0C323A988742445270C1D6A4DD40ED07 /* SessionDataTask.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDataTask.swift; path = Sources/Networking/SessionDataTask.swift; sourceTree = ""; }; - 0D167A016457E1A5BD5B9A73C4FF9434 /* Alamofire-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Alamofire-umbrella.h"; sourceTree = ""; }; + 0026A67CFFD17285AF03B23D2DD28B2A /* NSTextAttachment+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSTextAttachment+Kingfisher.swift"; path = "Sources/Extensions/NSTextAttachment+Kingfisher.swift"; sourceTree = ""; }; + 03EED79B9CB8F66CBC3776943530D0C1 /* ConstraintConstantTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintConstantTarget.swift; path = Sources/ConstraintConstantTarget.swift; sourceTree = ""; }; + 054BA59643B2127733DA8F8AAD1E4F5E /* URLConvertible+URLRequestConvertible.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URLConvertible+URLRequestConvertible.swift"; path = "Source/URLConvertible+URLRequestConvertible.swift"; sourceTree = ""; }; + 0CA5626C621090BAC155CA46CA4B4699 /* SnapKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SnapKit.debug.xcconfig; sourceTree = ""; }; 0D470C70EF958AF31663B789E7801669 /* Pods-CatStaGram-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-CatStaGram-acknowledgements.plist"; sourceTree = ""; }; - 142C2CAF3091DF534D3E7B4AAA4DFC0B /* CPListItem+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "CPListItem+Kingfisher.swift"; path = "Sources/Extensions/CPListItem+Kingfisher.swift"; sourceTree = ""; }; - 148FA9BD1508B6E7E220C9D9BF1928F8 /* Kingfisher-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Kingfisher-prefix.pch"; sourceTree = ""; }; - 1529C2E8B334710FA50E4C9A7153006A /* Concurrency.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Concurrency.swift; path = Source/Concurrency.swift; sourceTree = ""; }; - 16E37EF24D9E916BB47E43D8E954D3AF /* Image.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Image.swift; path = Sources/Image/Image.swift; sourceTree = ""; }; - 1986AFB6D3CAD62304F3FA85BE29D575 /* Kingfisher-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Kingfisher-Info.plist"; sourceTree = ""; }; - 1B8B2E44706F2E23444FC30F3BAEE3DC /* KingfisherOptionsInfo.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherOptionsInfo.swift; path = Sources/General/KingfisherOptionsInfo.swift; sourceTree = ""; }; - 1C1BAC7555E3D0AC24A397BB028A3CEC /* RedirectHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedirectHandler.swift; path = Sources/Networking/RedirectHandler.swift; sourceTree = ""; }; - 1C444AA66DFF06EB1D1D714FE5939A17 /* WKInterfaceImage+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "WKInterfaceImage+Kingfisher.swift"; path = "Sources/Extensions/WKInterfaceImage+Kingfisher.swift"; sourceTree = ""; }; - 1CB85CAAF3329112CD2FFF9889E98FF7 /* Runtime.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Runtime.swift; path = Sources/Utility/Runtime.swift; sourceTree = ""; }; - 1EBE61AE143E2DB816492EB19AA1651F /* NSTextAttachment+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSTextAttachment+Kingfisher.swift"; path = "Sources/Extensions/NSTextAttachment+Kingfisher.swift"; sourceTree = ""; }; - 21DBC7F03013C95C65EE295C4DAC2A64 /* AuthenticationChallengeResponsable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthenticationChallengeResponsable.swift; path = Sources/Networking/AuthenticationChallengeResponsable.swift; sourceTree = ""; }; - 23963C2629645E475AB2931F62E451C7 /* String+MD5.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "String+MD5.swift"; path = "Sources/Utility/String+MD5.swift"; sourceTree = ""; }; + 11B2551386363716C6DB6FD94B490B81 /* WKInterfaceImage+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "WKInterfaceImage+Kingfisher.swift"; path = "Sources/Extensions/WKInterfaceImage+Kingfisher.swift"; sourceTree = ""; }; + 11E540405E3D1158FAFBAE4F9BDC7EE9 /* EventMonitor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = EventMonitor.swift; path = Source/EventMonitor.swift; sourceTree = ""; }; + 14CEEDA7A6DF1930A777B65955863011 /* SnapKit-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "SnapKit-dummy.m"; sourceTree = ""; }; + 178259530DD4732FDC78DC27F9D1452E /* ConstraintPriority.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintPriority.swift; path = Sources/ConstraintPriority.swift; sourceTree = ""; }; + 1A35F58FA9F1861DD89BD76A1FB6912D /* Result+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Result+Alamofire.swift"; path = "Source/Result+Alamofire.swift"; sourceTree = ""; }; + 1D722BE2B78702E0D89F4AD6CB7A08D9 /* ConstraintOffsetTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintOffsetTarget.swift; path = Sources/ConstraintOffsetTarget.swift; sourceTree = ""; }; + 1F1EE76507C32C6532481BFCF619D224 /* CacheSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CacheSerializer.swift; path = Sources/Cache/CacheSerializer.swift; sourceTree = ""; }; + 21674FEAC995EECAEE8116E61002B452 /* URLEncodedFormEncoder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = URLEncodedFormEncoder.swift; path = Source/URLEncodedFormEncoder.swift; sourceTree = ""; }; + 22CE6618974753838E7BAB54EBCE7DBF /* Debugging.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Debugging.swift; path = Sources/Debugging.swift; sourceTree = ""; }; + 23350F61A142EDC6837D0E4A8A5FC8EF /* ConstraintLayoutSupportDSL.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintLayoutSupportDSL.swift; path = Sources/ConstraintLayoutSupportDSL.swift; sourceTree = ""; }; + 238EDDD638F2EDB57E54DDDB3B8BEACF /* ConstraintInsetTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintInsetTarget.swift; path = Sources/ConstraintInsetTarget.swift; sourceTree = ""; }; 2397257AE270C74C5FF63E810A481FDE /* Pods-CatStaGram.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-CatStaGram.modulemap"; sourceTree = ""; }; - 240168000C008585397987F341FC9AFA /* ImagePrefetcher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImagePrefetcher.swift; path = Sources/Networking/ImagePrefetcher.swift; sourceTree = ""; }; - 249A902C9828CB270329EFFAFAC1843A /* ParameterEncoding.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ParameterEncoding.swift; path = Source/ParameterEncoding.swift; sourceTree = ""; }; - 24A6C9EC04946183C998CEC9FFF72EFD /* Alamofire-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Alamofire-prefix.pch"; sourceTree = ""; }; - 27F40A9A287AE4680FD4A041AAD0C5ED /* RetryStrategy.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RetryStrategy.swift; path = Sources/Networking/RetryStrategy.swift; sourceTree = ""; }; - 292F77B8C48397E58C30BC7A313423E4 /* KFImageProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageProtocol.swift; path = Sources/SwiftUI/KFImageProtocol.swift; sourceTree = ""; }; - 2BAF59EA7A7DD980421989D2A205E6B0 /* KFImageOptions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageOptions.swift; path = Sources/SwiftUI/KFImageOptions.swift; sourceTree = ""; }; + 23A8F7936CB5C19BE80BD3356E614D52 /* ConstraintMakerRelatable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintMakerRelatable.swift; path = Sources/ConstraintMakerRelatable.swift; sourceTree = ""; }; + 24E66FE8F126D7F6A70D6214DD496549 /* Typealiases.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Typealiases.swift; path = Sources/Typealiases.swift; sourceTree = ""; }; + 24E7079555F2E462A564E0B121EE2DFB /* ResponseSerialization.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ResponseSerialization.swift; path = Source/ResponseSerialization.swift; sourceTree = ""; }; + 25707AE1D4270D1D1FE989EF8068C8A7 /* Protected.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Protected.swift; path = Source/Protected.swift; sourceTree = ""; }; + 2643C189D74D592B92995979CC1BEA9D /* ConstraintMakerRelatable+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "ConstraintMakerRelatable+Extensions.swift"; path = "Sources/ConstraintMakerRelatable+Extensions.swift"; sourceTree = ""; }; + 268F34234A1960AFA99595BB83B710CE /* OperationQueue+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "OperationQueue+Alamofire.swift"; path = "Source/OperationQueue+Alamofire.swift"; sourceTree = ""; }; + 26CA42D477EB2ADDC226A4709E7B38A7 /* SessionDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDelegate.swift; path = Sources/Networking/SessionDelegate.swift; sourceTree = ""; }; + 27F553BE51F7C505891FF4C14E5C9FBB /* Kingfisher.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Kingfisher.release.xcconfig; sourceTree = ""; }; + 27FBDA82588FFADCB73A08FDC1C0EED8 /* RequestTaskMap.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestTaskMap.swift; path = Source/RequestTaskMap.swift; sourceTree = ""; }; + 285C69FC27CB96D64460B0BD1DCFFFDC /* ImagePrefetcher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImagePrefetcher.swift; path = Sources/Networking/ImagePrefetcher.swift; sourceTree = ""; }; + 2AE64C5352AFEE75EB677A7284ABDE59 /* AFError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AFError.swift; path = Source/AFError.swift; sourceTree = ""; }; + 2B0C052B339AA2BDD72BBE9C7CEDFA3C /* Box.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Box.swift; path = Sources/Utility/Box.swift; sourceTree = ""; }; 2C309F94D8E35CA72C8F08ACB621D88F /* Pods-CatStaGram-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-CatStaGram-acknowledgements.markdown"; sourceTree = ""; }; - 2F3F986AF94568CEEB8AF631C72F03B2 /* Result+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Result+Alamofire.swift"; path = "Source/Result+Alamofire.swift"; sourceTree = ""; }; - 365099CC49560F0DAFBF90D1A7FBFD24 /* ExtensionHelpers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExtensionHelpers.swift; path = Sources/Utility/ExtensionHelpers.swift; sourceTree = ""; }; - 37D911DA9899C96EA4408220F4754219 /* ImageDataProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDataProcessor.swift; path = Sources/Networking/ImageDataProcessor.swift; sourceTree = ""; }; - 3C65DBD1C381FE1E4AA74F824E85F325 /* ImageFormat.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageFormat.swift; path = Sources/Image/ImageFormat.swift; sourceTree = ""; }; - 3D13E09327BDC573DE5AAFE079172AB9 /* Alamofire-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Alamofire-dummy.m"; sourceTree = ""; }; - 404743B9B61FE5357078AFD049FC6B45 /* URLConvertible+URLRequestConvertible.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URLConvertible+URLRequestConvertible.swift"; path = "Source/URLConvertible+URLRequestConvertible.swift"; sourceTree = ""; }; - 40CCE8AF433C89864A9FE3C1F9C34A96 /* CallbackQueue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CallbackQueue.swift; path = Sources/Utility/CallbackQueue.swift; sourceTree = ""; }; - 4193FEA28CDEB3710C7C41537C8D5E2E /* TVMonogramView+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "TVMonogramView+Kingfisher.swift"; path = "Sources/Extensions/TVMonogramView+Kingfisher.swift"; sourceTree = ""; }; + 2DF4497480929BC383C9404C43E30479 /* AuthenticationInterceptor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthenticationInterceptor.swift; path = Source/AuthenticationInterceptor.swift; sourceTree = ""; }; + 31CB56705A8EE8E7C074CD7E3D8F671B /* ConstraintRelation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintRelation.swift; path = Sources/ConstraintRelation.swift; sourceTree = ""; }; + 31E64283506C8735B64BD9C80EAC66F4 /* ImageContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageContext.swift; path = Sources/SwiftUI/ImageContext.swift; sourceTree = ""; }; + 32F44A08A5F580F49D5291C4A2BA8CCC /* HTTPMethod.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HTTPMethod.swift; path = Source/HTTPMethod.swift; sourceTree = ""; }; + 34CDE0FD157ECBE5A3270DB4E5818E36 /* ImageModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageModifier.swift; path = Sources/Networking/ImageModifier.swift; sourceTree = ""; }; + 364987DCEA6E58F38028166AB008F9CD /* ConstraintItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintItem.swift; path = Sources/ConstraintItem.swift; sourceTree = ""; }; + 3C9910954DCC3CF80C42993711A0B19F /* ConstraintDescription.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintDescription.swift; path = Sources/ConstraintDescription.swift; sourceTree = ""; }; + 3F571B67CBFAF3C7031418A7801B513A /* Alamofire.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Alamofire.release.xcconfig; sourceTree = ""; }; + 403E973BCBF657DC44AA8E268567E39C /* LayoutConstraintItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LayoutConstraintItem.swift; path = Sources/LayoutConstraintItem.swift; sourceTree = ""; }; + 40B6FD901E48869144B91F5E03417982 /* NSButton+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSButton+Kingfisher.swift"; path = "Sources/Extensions/NSButton+Kingfisher.swift"; sourceTree = ""; }; 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; - 44A117735F022F30BB9DC641136E51C9 /* CachedResponseHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CachedResponseHandler.swift; path = Source/CachedResponseHandler.swift; sourceTree = ""; }; - 48BAA15044E6AD772F77A856FA5107F2 /* DispatchQueue+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "DispatchQueue+Alamofire.swift"; path = "Source/DispatchQueue+Alamofire.swift"; sourceTree = ""; }; - 496EC73E3893F724BA57D993B154BF49 /* Source.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Source.swift; path = Sources/General/ImageSource/Source.swift; sourceTree = ""; }; - 499955C35F6D4A8F96E940FF129FF498 /* RedirectHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedirectHandler.swift; path = Source/RedirectHandler.swift; sourceTree = ""; }; - 4C1FA3BADCA4576957512EC8D60D2B76 /* ImageTransition.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageTransition.swift; path = Sources/Image/ImageTransition.swift; sourceTree = ""; }; - 4EF4F904B2EF1F1E836FADDE8C6B05DF /* GIFAnimatedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GIFAnimatedImage.swift; path = Sources/Image/GIFAnimatedImage.swift; sourceTree = ""; }; + 43B784610AC76ECDEBDCB3AE749016F5 /* ConstraintDSL.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintDSL.swift; path = Sources/ConstraintDSL.swift; sourceTree = ""; }; + 4545CD1066BB1CDD9E98D073C6339E4B /* String+MD5.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "String+MD5.swift"; path = "Sources/Utility/String+MD5.swift"; sourceTree = ""; }; + 46CBB1E6E5F76A4B848491596108E707 /* AnimatedImageView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AnimatedImageView.swift; path = Sources/Views/AnimatedImageView.swift; sourceTree = ""; }; + 4A3083E1E5289D29F023A8D6809C9837 /* ServerTrustEvaluation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServerTrustEvaluation.swift; path = Source/ServerTrustEvaluation.swift; sourceTree = ""; }; + 4E0BE569D743ECEC0C714CAAB568C48F /* Alamofire-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Alamofire-Info.plist"; sourceTree = ""; }; + 4E0CC80E337D75B5AB065E2F5AE4ECA1 /* SizeExtensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SizeExtensions.swift; path = Sources/Utility/SizeExtensions.swift; sourceTree = ""; }; + 4F42A45BF9A88CF234C5C8B1645A4BEB /* LayoutConstraint.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LayoutConstraint.swift; path = Sources/LayoutConstraint.swift; sourceTree = ""; }; 52554E1C9731D5352FDE9E63F8C5466B /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Accelerate.framework; sourceTree = DEVELOPER_DIR; }; - 56B4F426A74516AD9DF8A17EA00CE61E /* Alamofire.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Alamofire.release.xcconfig; sourceTree = ""; }; - 5723DEFD772E79C36C19B268F7643CE0 /* Indicator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Indicator.swift; path = Sources/Views/Indicator.swift; sourceTree = ""; }; - 597967B6CDA2E72751463CF56198E927 /* ImageDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDataProvider.swift; path = Sources/General/ImageSource/ImageDataProvider.swift; sourceTree = ""; }; - 5B56F1736D1127A32614C8CBBEF3E5F6 /* HTTPMethod.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HTTPMethod.swift; path = Source/HTTPMethod.swift; sourceTree = ""; }; + 5397B5B4479B64B79C83FC512332BB86 /* ConstraintLayoutGuide+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "ConstraintLayoutGuide+Extensions.swift"; path = "Sources/ConstraintLayoutGuide+Extensions.swift"; sourceTree = ""; }; + 540265494A78FD92012AD9D9D20AD5F2 /* Kingfisher.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Kingfisher.modulemap; sourceTree = ""; }; + 5937AE3145C880EAA9D9560C432F6CC0 /* DispatchQueue+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "DispatchQueue+Alamofire.swift"; path = "Source/DispatchQueue+Alamofire.swift"; sourceTree = ""; }; + 5A1F0BB5484DE62961655DC057183C5F /* UILayoutSupport+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UILayoutSupport+Extensions.swift"; path = "Sources/UILayoutSupport+Extensions.swift"; sourceTree = ""; }; + 5B109BB831EAE85D48A06C6D4D8E375F /* CPListItem+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "CPListItem+Kingfisher.swift"; path = "Sources/Extensions/CPListItem+Kingfisher.swift"; sourceTree = ""; }; + 5BF6D1092A6AEA823ADE61284D6A02F5 /* GraphicsContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GraphicsContext.swift; path = Sources/Image/GraphicsContext.swift; sourceTree = ""; }; 5D797E9A5C5782CE845840781FA1CC81 /* Alamofire */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Alamofire; path = Alamofire.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 630CD17355E726BB7AF8B6C6A581C2FE /* Alamofire.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Alamofire.debug.xcconfig; sourceTree = ""; }; - 6599B0D2C1F4B263D4D859853F0F348A /* RequestInterceptor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestInterceptor.swift; path = Source/RequestInterceptor.swift; sourceTree = ""; }; - 660CF05D9FA7E4371D6E6A45ED8EE01F /* KF.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KF.swift; path = Sources/General/KF.swift; sourceTree = ""; }; - 67030433EFF4B0333EC8811BC7EC47D6 /* Validation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Validation.swift; path = Source/Validation.swift; sourceTree = ""; }; + 5E1D44D618B316B6D82C72F94A26E240 /* KFImageOptions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageOptions.swift; path = Sources/SwiftUI/KFImageOptions.swift; sourceTree = ""; }; + 5FF1A9204468240BA1A125C9445A0047 /* ImageCache.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = Sources/Cache/ImageCache.swift; sourceTree = ""; }; + 610292CBAE491B4D17CA51F66D6861EF /* ImageDownloader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDownloader.swift; path = Sources/Networking/ImageDownloader.swift; sourceTree = ""; }; + 64F6FA35F52DD4E40C180C9B3FF358E0 /* Response.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Response.swift; path = Source/Response.swift; sourceTree = ""; }; + 667CD2987F5017A87603DE7297D94774 /* MultipartFormData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultipartFormData.swift; path = Source/MultipartFormData.swift; sourceTree = ""; }; + 67466EFA454A89EA2AFFBB49504EB9FC /* Kingfisher-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Kingfisher-dummy.m"; sourceTree = ""; }; 680FB1C38EA9062A1098F656BF33DEA1 /* Pods-CatStaGram-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-CatStaGram-Info.plist"; sourceTree = ""; }; - 6835FBF931D5C31EFFF83ED559436CDE /* Kingfisher-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Kingfisher-dummy.m"; sourceTree = ""; }; - 6A723238162323CE3B380FA8E894C865 /* Filter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Filter.swift; path = Sources/Image/Filter.swift; sourceTree = ""; }; - 6BCDFE9C10374511CCC375D2E99C812E /* Storage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Storage.swift; path = Sources/Cache/Storage.swift; sourceTree = ""; }; - 6C321B1DFC61DFB5E1EB0BF63C4E7B05 /* Kingfisher-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Kingfisher-umbrella.h"; sourceTree = ""; }; - 6E5C70357A0F35A05639673633F51624 /* Kingfisher.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Kingfisher.debug.xcconfig; sourceTree = ""; }; - 7006AE0546B679604D685DCA785C542C /* Result.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Result.swift; path = Sources/Utility/Result.swift; sourceTree = ""; }; + 68BB2620AEB63F8E9B3D27DBBCFE300D /* ConstraintConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintConfig.swift; path = Sources/ConstraintConfig.swift; sourceTree = ""; }; + 6B3BE0F5416E121307A80EA7D74C9F1F /* Alamofire-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Alamofire-prefix.pch"; sourceTree = ""; }; + 6BAA0652E6B2091F985485A914ED9663 /* Combine.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Combine.swift; path = Source/Combine.swift; sourceTree = ""; }; + 6CF18FE28A2E0A8843038908E4A7410E /* Runtime.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Runtime.swift; path = Sources/Utility/Runtime.swift; sourceTree = ""; }; + 6EC572DB04F27228F228CA58C17E9016 /* RetryPolicy.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RetryPolicy.swift; path = Source/RetryPolicy.swift; sourceTree = ""; }; + 6FD33114F6764F686C1A80233A8A1D34 /* ExtensionHelpers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExtensionHelpers.swift; path = Sources/Utility/ExtensionHelpers.swift; sourceTree = ""; }; 70D1A3E8DA5E73DFADB87E35404AAA05 /* Pods-CatStaGram.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-CatStaGram.debug.xcconfig"; sourceTree = ""; }; - 715993D57B4975C3CF94C6BC997595C6 /* RetryPolicy.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RetryPolicy.swift; path = Source/RetryPolicy.swift; sourceTree = ""; }; - 72447455A0B451F260AE94E750E590D7 /* AlamofireExtended.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AlamofireExtended.swift; path = Source/AlamofireExtended.swift; sourceTree = ""; }; - 7366FB0DEF319ED32BAF9DE5654C8DB7 /* ImageProgressive.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProgressive.swift; path = Sources/Image/ImageProgressive.swift; sourceTree = ""; }; - 73DC4358317E61D22885E415D01F8DFF /* Kingfisher.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Kingfisher.modulemap; sourceTree = ""; }; - 76CC7FAE70D848424E201CB9C76CABD3 /* Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Alamofire.swift; path = Source/Alamofire.swift; sourceTree = ""; }; + 7389B8989D16FFDD6215BF99EAD4EF83 /* KFAnimatedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFAnimatedImage.swift; path = Sources/SwiftUI/KFAnimatedImage.swift; sourceTree = ""; }; + 74E82B4D26EE55A623511D1FEF6913B1 /* CachedResponseHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CachedResponseHandler.swift; path = Source/CachedResponseHandler.swift; sourceTree = ""; }; + 75701ABD349433AD237CC63DF8CD558F /* Kingfisher.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Kingfisher.debug.xcconfig; sourceTree = ""; }; + 75D24A24D936AA5A0801D5BC8B6EC130 /* RedirectHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedirectHandler.swift; path = Source/RedirectHandler.swift; sourceTree = ""; }; + 77870AC54264011D51C6F35408B2B263 /* Kingfisher-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Kingfisher-umbrella.h"; sourceTree = ""; }; + 787FCAF7200932CE309A97CBF33ACB60 /* SnapKit-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "SnapKit-Info.plist"; sourceTree = ""; }; 792CB57DACF134EC2A6070CDDCF91F56 /* Pods-CatStaGram-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-CatStaGram-frameworks.sh"; sourceTree = ""; }; - 7933A5F1F8BD20A48039AA759ADB465E /* ImageDrawing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDrawing.swift; path = Sources/Image/ImageDrawing.swift; sourceTree = ""; }; - 7B7AD2807A19FC11299041D8A8777267 /* KingfisherManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherManager.swift; path = Sources/General/KingfisherManager.swift; sourceTree = ""; }; - 7C6C8F94FCB9064F02B48B607AB69962 /* Placeholder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Placeholder.swift; path = Sources/Image/Placeholder.swift; sourceTree = ""; }; + 797263D3E571383C88815218DCE6B4CC /* Notifications.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Notifications.swift; path = Source/Notifications.swift; sourceTree = ""; }; + 799E181E909BC0CAE1806256859AB012 /* Delegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Delegate.swift; path = Sources/Utility/Delegate.swift; sourceTree = ""; }; + 7AEA1626758690136CDAF41CB95672E6 /* SessionDataTask.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDataTask.swift; path = Sources/Networking/SessionDataTask.swift; sourceTree = ""; }; + 7CC749BFB582FB54281186F7C212DC04 /* Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Kingfisher.swift; path = Sources/General/Kingfisher.swift; sourceTree = ""; }; + 7CF22E24F8E9F6CCEE9CFA8E515A697C /* KingfisherOptionsInfo.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherOptionsInfo.swift; path = Sources/General/KingfisherOptionsInfo.swift; sourceTree = ""; }; 7DBB0D476F47E7A38CE0EDF85193CF41 /* Pods-CatStaGram-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-CatStaGram-dummy.m"; sourceTree = ""; }; - 7EE41735C363063D401BC89633A455BE /* MultipartFormData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultipartFormData.swift; path = Source/MultipartFormData.swift; sourceTree = ""; }; - 80270ADB4ECE74E17A7DB57236035920 /* AFError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AFError.swift; path = Source/AFError.swift; sourceTree = ""; }; - 8676B059D0C43C5A2FC81E0F55AD63D7 /* FormatIndicatedCacheSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FormatIndicatedCacheSerializer.swift; path = Sources/Cache/FormatIndicatedCacheSerializer.swift; sourceTree = ""; }; - 86DDDE6D887ED6DFE35529D771930C65 /* KFAnimatedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFAnimatedImage.swift; path = Sources/SwiftUI/KFAnimatedImage.swift; sourceTree = ""; }; + 7E785A1D945BC47127CA4CE0DE7A0BA8 /* ImageDownloaderDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDownloaderDelegate.swift; path = Sources/Networking/ImageDownloaderDelegate.swift; sourceTree = ""; }; + 7EC45995F1574DEA557A6E3BC05CFEEF /* Alamofire-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Alamofire-dummy.m"; sourceTree = ""; }; + 7FCA8DB37C4EAD7761A2AD3929FDDDD6 /* KFOptionsSetter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFOptionsSetter.swift; path = Sources/General/KFOptionsSetter.swift; sourceTree = ""; }; + 82800F15627BA03BC43D65EBE817E6BD /* KFImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImage.swift; path = Sources/SwiftUI/KFImage.swift; sourceTree = ""; }; + 83A45C0B52AFEB667ED0777CEBB9BA08 /* Concurrency.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Concurrency.swift; path = Source/Concurrency.swift; sourceTree = ""; }; + 84C9E9C49050587AA0631B8B31BB28C3 /* SnapKit.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SnapKit.release.xcconfig; sourceTree = ""; }; + 85174CF7F69E6FCA3002CE90EB305644 /* Alamofire-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Alamofire-umbrella.h"; sourceTree = ""; }; + 85F4E1D29D79341BA832E746E5BDCFFA /* ConstraintMultiplierTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintMultiplierTarget.swift; path = Sources/ConstraintMultiplierTarget.swift; sourceTree = ""; }; 872D7EFA572ECEE8EF993C27196E16DD /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; }; - 8AE9B6094B04E75ACDBB73C3A194D690 /* ImageCache.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = Sources/Cache/ImageCache.swift; sourceTree = ""; }; - 8BA6A6C6AEAC43638A2B248BBEA90EBD /* MultipartUpload.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultipartUpload.swift; path = Source/MultipartUpload.swift; sourceTree = ""; }; - 932C67B305CF8C8942FEBD2375BE2E1F /* EventMonitor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = EventMonitor.swift; path = Source/EventMonitor.swift; sourceTree = ""; }; - 9566B136FA420BDAAE8B272AE10CFE3E /* KFImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImage.swift; path = Sources/SwiftUI/KFImage.swift; sourceTree = ""; }; - 95BFD9705B9A22E25BBBD0A608C90CCC /* Resource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Resource.swift; path = Sources/General/ImageSource/Resource.swift; sourceTree = ""; }; - 985C3C905A92749F5FB0325692FBF789 /* ImageView+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "ImageView+Kingfisher.swift"; path = "Sources/Extensions/ImageView+Kingfisher.swift"; sourceTree = ""; }; + 8A22A6B725B6E1FF87F3CF9BB6E30DDC /* Kingfisher-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Kingfisher-prefix.pch"; sourceTree = ""; }; + 8A4A070E2855042F335EFD9E5E195C5B /* ConstraintMakerEditable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintMakerEditable.swift; path = Sources/ConstraintMakerEditable.swift; sourceTree = ""; }; + 8B76FA6AD6CFF53A0BE51BE13C1109C2 /* MemoryStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MemoryStorage.swift; path = Sources/Cache/MemoryStorage.swift; sourceTree = ""; }; + 8BFCE61ADED2F9A2CD76A78A188C0CA9 /* Session.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Session.swift; path = Source/Session.swift; sourceTree = ""; }; + 8C54FE1F16AF08A4A5E86EC037CBAC7A /* Placeholder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Placeholder.swift; path = Sources/Image/Placeholder.swift; sourceTree = ""; }; + 8CE918EBDB02B9B27E71402FCB954402 /* ConstraintDirectionalInsets.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintDirectionalInsets.swift; path = Sources/ConstraintDirectionalInsets.swift; sourceTree = ""; }; + 8E1DF45629585C93475F05C4C705B54B /* ConstraintMakerExtendable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintMakerExtendable.swift; path = Sources/ConstraintMakerExtendable.swift; sourceTree = ""; }; + 907DC9525547D14B924F35B4D6792664 /* AlamofireExtended.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AlamofireExtended.swift; path = Source/AlamofireExtended.swift; sourceTree = ""; }; + 91586153A6E31A45B8184E55BDC2F04F /* MultipartUpload.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultipartUpload.swift; path = Source/MultipartUpload.swift; sourceTree = ""; }; + 91B43B2C8204242C97F8659DA9C709B3 /* ImageFormat.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageFormat.swift; path = Sources/Image/ImageFormat.swift; sourceTree = ""; }; + 923866AFC245CAED8B1E67CAE9A8256A /* ConstraintView+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "ConstraintView+Extensions.swift"; path = "Sources/ConstraintView+Extensions.swift"; sourceTree = ""; }; + 936CE1FB37015B4B9EA165AF51FA4947 /* ConstraintMaker.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintMaker.swift; path = Sources/ConstraintMaker.swift; sourceTree = ""; }; + 95405C25A4588F6E55B6AA02260CE374 /* SnapKit.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SnapKit.modulemap; sourceTree = ""; }; + 979486118B3E90C08386079D57962701 /* SnapKit */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = SnapKit; path = SnapKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 987D2724E2957A7A8BB654A970164F95 /* Result.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Result.swift; path = Sources/Utility/Result.swift; sourceTree = ""; }; + 99CBCB77AD5710D6D11812BAC486BD6E /* ImageDataProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDataProcessor.swift; path = Sources/Networking/ImageDataProcessor.swift; sourceTree = ""; }; + 9A4130C4E6473352951BBE52AB721BC7 /* TVMonogramView+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "TVMonogramView+Kingfisher.swift"; path = "Sources/Extensions/TVMonogramView+Kingfisher.swift"; sourceTree = ""; }; + 9B0717827596EDE05BA395607E31C7FA /* KingfisherError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherError.swift; path = Sources/General/KingfisherError.swift; sourceTree = ""; }; + 9BA3F91769ED2D36CBFCA9056FCE2A63 /* KingfisherManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherManager.swift; path = Sources/General/KingfisherManager.swift; sourceTree = ""; }; + 9BA9A2CCA476007FA9B257E213BB1E94 /* Alamofire.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Alamofire.debug.xcconfig; sourceTree = ""; }; + 9CD59BED079FC427AE725FF857613539 /* ParameterEncoding.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ParameterEncoding.swift; path = Source/ParameterEncoding.swift; sourceTree = ""; }; + 9D0238C1747B9EF2D7860DFC44E8C5B6 /* ConstraintPriorityTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintPriorityTarget.swift; path = Sources/ConstraintPriorityTarget.swift; sourceTree = ""; }; 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; - A043F9EF1DF150712ECDD2FB7CD2E46B /* NetworkReachabilityManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkReachabilityManager.swift; path = Source/NetworkReachabilityManager.swift; sourceTree = ""; }; - A0DD802295003561B6C5741521D716BC /* RequestTaskMap.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestTaskMap.swift; path = Source/RequestTaskMap.swift; sourceTree = ""; }; - A1939B0AEE888F2EB258D6832523F67C /* Session.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Session.swift; path = Source/Session.swift; sourceTree = ""; }; - A20EDDB802D0AEBA579A2FAC5B1B6BDE /* OperationQueue+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "OperationQueue+Alamofire.swift"; path = "Source/OperationQueue+Alamofire.swift"; sourceTree = ""; }; + 9DBF96DDFE3AFC890BE19FD063447453 /* ConstraintView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintView.swift; path = Sources/ConstraintView.swift; sourceTree = ""; }; + A0EDA61BA557420CA7C3E24BE64E495E /* ConstraintAttributes.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintAttributes.swift; path = Sources/ConstraintAttributes.swift; sourceTree = ""; }; + A1AD755643055311B24F71017C844D2D /* ConstraintDirectionalInsetTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintDirectionalInsetTarget.swift; path = Sources/ConstraintDirectionalInsetTarget.swift; sourceTree = ""; }; + A8B0124C50FC6B6BBE8A6278B8F6E983 /* ImageProgressive.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProgressive.swift; path = Sources/Image/ImageProgressive.swift; sourceTree = ""; }; A9CCB3A1E65C7F0BD9B4C55FDA273F66 /* Pods-CatStaGram */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-CatStaGram"; path = Pods_CatStaGram.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - AC2601AB15F49167B95C5DDC2FBC7B78 /* MemoryStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MemoryStorage.swift; path = Sources/Cache/MemoryStorage.swift; sourceTree = ""; }; - AFFE766A6E0F3A621DFD9487DA3CAF7E /* Protected.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Protected.swift; path = Source/Protected.swift; sourceTree = ""; }; - B0BAB418BAD6B4B421F289A06BD5909C /* Alamofire-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Alamofire-Info.plist"; sourceTree = ""; }; - B114DBD20B001B2A9D93F8F8C47903B6 /* Combine.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Combine.swift; path = Source/Combine.swift; sourceTree = ""; }; - B42FDB98681E0F2B54A53C6263FC0FB7 /* SessionDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDelegate.swift; path = Source/SessionDelegate.swift; sourceTree = ""; }; - B7407E80368EF1CE2A091B4F0F8F81FF /* URLSessionConfiguration+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URLSessionConfiguration+Alamofire.swift"; path = "Source/URLSessionConfiguration+Alamofire.swift"; sourceTree = ""; }; - B74729EFA464A2B107050ED713601AB8 /* AVAssetImageDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AVAssetImageDataProvider.swift; path = Sources/General/ImageSource/AVAssetImageDataProvider.swift; sourceTree = ""; }; - BA9B50C2ED49A8E73A4D64C205173820 /* UIButton+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIButton+Kingfisher.swift"; path = "Sources/Extensions/UIButton+Kingfisher.swift"; sourceTree = ""; }; - BB099A6B514875F22EF98798B56B4BA5 /* Response.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Response.swift; path = Source/Response.swift; sourceTree = ""; }; - BB31688140758856323E0B3187769487 /* URLRequest+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URLRequest+Alamofire.swift"; path = "Source/URLRequest+Alamofire.swift"; sourceTree = ""; }; - C264879E6B16EA9E07F073E24220A232 /* RequestModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestModifier.swift; path = Sources/Networking/RequestModifier.swift; sourceTree = ""; }; - C26990E9D251C57CAB34BC5C9EC37033 /* KFImageRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageRenderer.swift; path = Sources/SwiftUI/KFImageRenderer.swift; sourceTree = ""; }; - C32A86B99F0C8B9E42B3D9C7F9730889 /* ResponseSerialization.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ResponseSerialization.swift; path = Source/ResponseSerialization.swift; sourceTree = ""; }; - C3CD3E23CCF124B01CA690E27E72D1B9 /* KingfisherError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherError.swift; path = Sources/General/KingfisherError.swift; sourceTree = ""; }; + B465AC0964110BAEAFCF634EF8832801 /* ImageBinder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageBinder.swift; path = Sources/SwiftUI/ImageBinder.swift; sourceTree = ""; }; + B4D4EE71842ECE47B9B60E26EC9158FE /* ImageProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProcessor.swift; path = Sources/Image/ImageProcessor.swift; sourceTree = ""; }; + B93AA7C2C96625A85AEA6FB6E673DDAD /* StringEncoding+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "StringEncoding+Alamofire.swift"; path = "Source/StringEncoding+Alamofire.swift"; sourceTree = ""; }; + BBD249D56D7905164D61C4DAC613E264 /* Storage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Storage.swift; path = Sources/Cache/Storage.swift; sourceTree = ""; }; + BC6A21BE8DA507842B3F12753CDD5716 /* Request.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Request.swift; path = Source/Request.swift; sourceTree = ""; }; + BDE1C754F3F4878CA35D1AAE0FDF70EC /* ConstraintRelatableTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintRelatableTarget.swift; path = Sources/ConstraintRelatableTarget.swift; sourceTree = ""; }; + BEE5E2F810B61452A5C02C51C487AEEF /* Image.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Image.swift; path = Sources/Image/Image.swift; sourceTree = ""; }; + BEE7E843B4099ACD1719D865D4B9F25D /* SnapKit-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SnapKit-prefix.pch"; sourceTree = ""; }; + C138D9C30CB4F78726B11E277DCF1ED4 /* GIFAnimatedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GIFAnimatedImage.swift; path = Sources/Image/GIFAnimatedImage.swift; sourceTree = ""; }; + C13F847746EF4F7AE09B3A747BA544B5 /* ImageDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDataProvider.swift; path = Sources/General/ImageSource/ImageDataProvider.swift; sourceTree = ""; }; + C17E436AB454460A694A91918ABF6C0A /* RequestInterceptor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestInterceptor.swift; path = Source/RequestInterceptor.swift; sourceTree = ""; }; + C26221D803E0608A91ACAA9D354BBECB /* Kingfisher-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Kingfisher-Info.plist"; sourceTree = ""; }; + C2CF2CC40AE2571056B934957097004E /* NetworkReachabilityManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkReachabilityManager.swift; path = Source/NetworkReachabilityManager.swift; sourceTree = ""; }; C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Kingfisher; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C6E93449DD3CC778DD5D443C7780D895 /* NSButton+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSButton+Kingfisher.swift"; path = "Sources/Extensions/NSButton+Kingfisher.swift"; sourceTree = ""; }; - C95F29F7BBB1AA54AD679386E26C7740 /* ImageProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProcessor.swift; path = Sources/Image/ImageProcessor.swift; sourceTree = ""; }; - CE9ECAD406B234DD522B04457BCADA54 /* SessionDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDelegate.swift; path = Sources/Networking/SessionDelegate.swift; sourceTree = ""; }; - D362E01704CD82D45E9C7857FB20AC1E /* DiskStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DiskStorage.swift; path = Sources/Cache/DiskStorage.swift; sourceTree = ""; }; - D370E0D48C1925AC14D155BEDF3DC7D4 /* GraphicsContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GraphicsContext.swift; path = Sources/Image/GraphicsContext.swift; sourceTree = ""; }; - D8CBA959C728F5B6931C11DE61853D88 /* SizeExtensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SizeExtensions.swift; path = Sources/Utility/SizeExtensions.swift; sourceTree = ""; }; - D942B0884573FD857CFB17A94E6A5807 /* StringEncoding+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "StringEncoding+Alamofire.swift"; path = "Source/StringEncoding+Alamofire.swift"; sourceTree = ""; }; - DAE0A9D42161F3A6512AEE4A7F63B4A0 /* ImageBinder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageBinder.swift; path = Sources/SwiftUI/ImageBinder.swift; sourceTree = ""; }; - DB1FAB3233CC80F28F7E66DF4DC9F2FA /* HTTPHeaders.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HTTPHeaders.swift; path = Source/HTTPHeaders.swift; sourceTree = ""; }; - E7A8F9A880D8CF92488BF36640F2DBBE /* Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Kingfisher.swift; path = Sources/General/Kingfisher.swift; sourceTree = ""; }; - EDEF84AAA5BEDA67174B11EECD29C8BD /* Kingfisher.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Kingfisher.release.xcconfig; sourceTree = ""; }; + C549ABE4054C58BADD9499BFD01CB5CC /* CallbackQueue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CallbackQueue.swift; path = Sources/Utility/CallbackQueue.swift; sourceTree = ""; }; + C6B20F22CEC80124AF2B6C9DD78E40F2 /* ConstraintInsets.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintInsets.swift; path = Sources/ConstraintInsets.swift; sourceTree = ""; }; + C81466D995C81B34DEE0A314B083C2D0 /* HTTPHeaders.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HTTPHeaders.swift; path = Source/HTTPHeaders.swift; sourceTree = ""; }; + C8C89C7A9A55466B555B62139EC4D548 /* Alamofire.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Alamofire.modulemap; sourceTree = ""; }; + C8D9CBFE45052D73C5F502FB595489BE /* SessionDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDelegate.swift; path = Source/SessionDelegate.swift; sourceTree = ""; }; + C8E227E931599A9136D2BB9EB051B6D6 /* ImageDrawing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDrawing.swift; path = Sources/Image/ImageDrawing.swift; sourceTree = ""; }; + CA5130A1FDB34E3B4447038BEA687890 /* ImageView+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "ImageView+Kingfisher.swift"; path = "Sources/Extensions/ImageView+Kingfisher.swift"; sourceTree = ""; }; + CB0A90C89BD5F9CC88A1A769CF346458 /* URLRequest+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URLRequest+Alamofire.swift"; path = "Source/URLRequest+Alamofire.swift"; sourceTree = ""; }; + CF26F23415267C306D13B6524C4FF622 /* ConstraintLayoutGuide.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintLayoutGuide.swift; path = Sources/ConstraintLayoutGuide.swift; sourceTree = ""; }; + D178B7CF7375C36FFB823F1493FB21FC /* ParameterEncoder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ParameterEncoder.swift; path = Source/ParameterEncoder.swift; sourceTree = ""; }; + D4C0C598F4D69737485E825CF3EB38E2 /* Resource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Resource.swift; path = Sources/General/ImageSource/Resource.swift; sourceTree = ""; }; + D7041BB19442F3ACDB15B140F458F078 /* ImageTransition.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageTransition.swift; path = Sources/Image/ImageTransition.swift; sourceTree = ""; }; + D8A5C0DFC63B97534F154442D36FA9B8 /* UIButton+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIButton+Kingfisher.swift"; path = "Sources/Extensions/UIButton+Kingfisher.swift"; sourceTree = ""; }; + D9C4DE86B5FA3554D2CE8756E7C62C05 /* ConstraintLayoutGuideDSL.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintLayoutGuideDSL.swift; path = Sources/ConstraintLayoutGuideDSL.swift; sourceTree = ""; }; + DDD3326917FBC47DA75A7A9DF4A8D68B /* AuthenticationChallengeResponsable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthenticationChallengeResponsable.swift; path = Sources/Networking/AuthenticationChallengeResponsable.swift; sourceTree = ""; }; + DEAE448343149DBD139FF6ECDFCB540D /* ConstraintMakerFinalizable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintMakerFinalizable.swift; path = Sources/ConstraintMakerFinalizable.swift; sourceTree = ""; }; + DEBBF83A19240F77CB8282A425738F57 /* Filter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Filter.swift; path = Sources/Image/Filter.swift; sourceTree = ""; }; + E28BDE3A9FE93A1357F5CD74F1552342 /* RedirectHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedirectHandler.swift; path = Sources/Networking/RedirectHandler.swift; sourceTree = ""; }; + E3E472F69AB3AD6146CF5B1687599392 /* AVAssetImageDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AVAssetImageDataProvider.swift; path = Sources/General/ImageSource/AVAssetImageDataProvider.swift; sourceTree = ""; }; + E4022A8C85369D0ACA7E38670253E3D1 /* Indicator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Indicator.swift; path = Sources/Views/Indicator.swift; sourceTree = ""; }; + E4C7082784768E1C6C01AF820DFEFDF1 /* Validation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Validation.swift; path = Source/Validation.swift; sourceTree = ""; }; + E6D33079D4E9609D5B2E73B0524B75E5 /* Source.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Source.swift; path = Sources/General/ImageSource/Source.swift; sourceTree = ""; }; + E972E5301F6A20A1F2F4A3F3FB5189DA /* FormatIndicatedCacheSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FormatIndicatedCacheSerializer.swift; path = Sources/Cache/FormatIndicatedCacheSerializer.swift; sourceTree = ""; }; EE2747566D1A67FB4BBC1883CC9ACE4E /* Pods-CatStaGram-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-CatStaGram-umbrella.h"; sourceTree = ""; }; - EEE76C5003FE080CF8820CA18C434EA3 /* ImageDownloader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDownloader.swift; path = Sources/Networking/ImageDownloader.swift; sourceTree = ""; }; - F1B7F2B2CCF604EF10790364BAB06391 /* AuthenticationInterceptor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthenticationInterceptor.swift; path = Source/AuthenticationInterceptor.swift; sourceTree = ""; }; + F0148907B3A18C2DF9B3D355964E537C /* Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Alamofire.swift; path = Source/Alamofire.swift; sourceTree = ""; }; + F1A28E9E8E3211493299F42561988C41 /* SnapKit-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SnapKit-umbrella.h"; sourceTree = ""; }; + F1C39CAEDC4116F60F5D9A988BBCBA88 /* ConstraintLayoutSupport.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintLayoutSupport.swift; path = Sources/ConstraintLayoutSupport.swift; sourceTree = ""; }; + F22C681FC81C694E8D747D6AECBAEF8E /* ConstraintMakerPrioritizable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintMakerPrioritizable.swift; path = Sources/ConstraintMakerPrioritizable.swift; sourceTree = ""; }; F368C1F054959568F9FC5262C8FD9F8C /* Pods-CatStaGram.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-CatStaGram.release.xcconfig"; sourceTree = ""; }; - F6A1EDFD24518B3F930192EDAE086842 /* ParameterEncoder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ParameterEncoder.swift; path = Source/ParameterEncoder.swift; sourceTree = ""; }; - FAB10145B8616970B7B29BFF058CF910 /* ImageContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageContext.swift; path = Sources/SwiftUI/ImageContext.swift; sourceTree = ""; }; - FB0BD0DA3BA3BF98EFC7BF4305A340BA /* Alamofire.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Alamofire.modulemap; sourceTree = ""; }; - FC710AFB6066D8535108054FD1115E94 /* Notifications.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Notifications.swift; path = Source/Notifications.swift; sourceTree = ""; }; + F3933AD68D7D40D3391FD238893A9E80 /* RequestModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestModifier.swift; path = Sources/Networking/RequestModifier.swift; sourceTree = ""; }; + F448016C8356D578FEDC3FF0468E0949 /* KFImageRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageRenderer.swift; path = Sources/SwiftUI/KFImageRenderer.swift; sourceTree = ""; }; + F4852E3BB7086025361AA0A3FF25346E /* RetryStrategy.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RetryStrategy.swift; path = Sources/Networking/RetryStrategy.swift; sourceTree = ""; }; + F566C7CBE07FDFA59C7103FF1EFF3896 /* ConstraintViewDSL.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintViewDSL.swift; path = Sources/ConstraintViewDSL.swift; sourceTree = ""; }; + F738ECB6F1F16EEF4863E1874544D4A0 /* KFImageProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageProtocol.swift; path = Sources/SwiftUI/KFImageProtocol.swift; sourceTree = ""; }; + F86BD1B17487999591CD15009D638029 /* KF.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KF.swift; path = Sources/General/KF.swift; sourceTree = ""; }; + F8B938A45380E77BF8ACD8EF3BA3276F /* URLSessionConfiguration+Alamofire.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URLSessionConfiguration+Alamofire.swift"; path = "Source/URLSessionConfiguration+Alamofire.swift"; sourceTree = ""; }; + FCA8F67D5E603B9D9888F34599FC6AAA /* Constraint.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Constraint.swift; path = Sources/Constraint.swift; sourceTree = ""; }; + FDDE8D838F7402500B258EECED27B9C2 /* DiskStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DiskStorage.swift; path = Sources/Cache/DiskStorage.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -275,6 +367,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 773F7A3D136BD088B602E619FEB2C970 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D7DC8082CC183FA721961479CA965627 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 81DB1665E1495609510BA493822E5A85 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -285,11 +385,11 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - B23375F7E905D644F561F7559FA187BF /* Frameworks */ = { + E8E4E7CE8309D3EF7B7476AC57986BE9 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C9392394543C174FFBF84B243D562853 /* Foundation.framework in Frameworks */, + 0833E66E7F19849322305D67777B77DB /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -304,74 +404,19 @@ name = Frameworks; sourceTree = ""; }; - 0F4AF1A3447E9E41BB475E0A5B868464 /* Kingfisher */ = { + 16F9A14783155ED2458CE900252543A1 /* Support Files */ = { isa = PBXGroup; children = ( - 06442349E67A8F1944EBFB63B46B406E /* AnimatedImageView.swift */, - 21DBC7F03013C95C65EE295C4DAC2A64 /* AuthenticationChallengeResponsable.swift */, - B74729EFA464A2B107050ED713601AB8 /* AVAssetImageDataProvider.swift */, - 0B7F4B3A6C95F8600D69F851451D63F6 /* Box.swift */, - 0BC3A37B1FA2F4841F5E8CC3FC102A19 /* CacheSerializer.swift */, - 40CCE8AF433C89864A9FE3C1F9C34A96 /* CallbackQueue.swift */, - 142C2CAF3091DF534D3E7B4AAA4DFC0B /* CPListItem+Kingfisher.swift */, - 0B347AF8E0B52CE83F1B9A8F51F1AE31 /* Delegate.swift */, - D362E01704CD82D45E9C7857FB20AC1E /* DiskStorage.swift */, - 365099CC49560F0DAFBF90D1A7FBFD24 /* ExtensionHelpers.swift */, - 6A723238162323CE3B380FA8E894C865 /* Filter.swift */, - 8676B059D0C43C5A2FC81E0F55AD63D7 /* FormatIndicatedCacheSerializer.swift */, - 4EF4F904B2EF1F1E836FADDE8C6B05DF /* GIFAnimatedImage.swift */, - D370E0D48C1925AC14D155BEDF3DC7D4 /* GraphicsContext.swift */, - 16E37EF24D9E916BB47E43D8E954D3AF /* Image.swift */, - DAE0A9D42161F3A6512AEE4A7F63B4A0 /* ImageBinder.swift */, - 8AE9B6094B04E75ACDBB73C3A194D690 /* ImageCache.swift */, - FAB10145B8616970B7B29BFF058CF910 /* ImageContext.swift */, - 37D911DA9899C96EA4408220F4754219 /* ImageDataProcessor.swift */, - 597967B6CDA2E72751463CF56198E927 /* ImageDataProvider.swift */, - EEE76C5003FE080CF8820CA18C434EA3 /* ImageDownloader.swift */, - 077E33C20C9BD4FAF66AE7A6EC1657F4 /* ImageDownloaderDelegate.swift */, - 7933A5F1F8BD20A48039AA759ADB465E /* ImageDrawing.swift */, - 3C65DBD1C381FE1E4AA74F824E85F325 /* ImageFormat.swift */, - 07524CF920EFBC25C9A0C9B3E57AA118 /* ImageModifier.swift */, - 240168000C008585397987F341FC9AFA /* ImagePrefetcher.swift */, - C95F29F7BBB1AA54AD679386E26C7740 /* ImageProcessor.swift */, - 7366FB0DEF319ED32BAF9DE5654C8DB7 /* ImageProgressive.swift */, - 4C1FA3BADCA4576957512EC8D60D2B76 /* ImageTransition.swift */, - 985C3C905A92749F5FB0325692FBF789 /* ImageView+Kingfisher.swift */, - 5723DEFD772E79C36C19B268F7643CE0 /* Indicator.swift */, - 660CF05D9FA7E4371D6E6A45ED8EE01F /* KF.swift */, - 86DDDE6D887ED6DFE35529D771930C65 /* KFAnimatedImage.swift */, - 9566B136FA420BDAAE8B272AE10CFE3E /* KFImage.swift */, - 2BAF59EA7A7DD980421989D2A205E6B0 /* KFImageOptions.swift */, - 292F77B8C48397E58C30BC7A313423E4 /* KFImageProtocol.swift */, - C26990E9D251C57CAB34BC5C9EC37033 /* KFImageRenderer.swift */, - 011604690A5B598427758F666A2F13C8 /* KFOptionsSetter.swift */, - E7A8F9A880D8CF92488BF36640F2DBBE /* Kingfisher.swift */, - C3CD3E23CCF124B01CA690E27E72D1B9 /* KingfisherError.swift */, - 7B7AD2807A19FC11299041D8A8777267 /* KingfisherManager.swift */, - 1B8B2E44706F2E23444FC30F3BAEE3DC /* KingfisherOptionsInfo.swift */, - AC2601AB15F49167B95C5DDC2FBC7B78 /* MemoryStorage.swift */, - C6E93449DD3CC778DD5D443C7780D895 /* NSButton+Kingfisher.swift */, - 1EBE61AE143E2DB816492EB19AA1651F /* NSTextAttachment+Kingfisher.swift */, - 7C6C8F94FCB9064F02B48B607AB69962 /* Placeholder.swift */, - 1C1BAC7555E3D0AC24A397BB028A3CEC /* RedirectHandler.swift */, - C264879E6B16EA9E07F073E24220A232 /* RequestModifier.swift */, - 95BFD9705B9A22E25BBBD0A608C90CCC /* Resource.swift */, - 7006AE0546B679604D685DCA785C542C /* Result.swift */, - 27F40A9A287AE4680FD4A041AAD0C5ED /* RetryStrategy.swift */, - 1CB85CAAF3329112CD2FFF9889E98FF7 /* Runtime.swift */, - 0C323A988742445270C1D6A4DD40ED07 /* SessionDataTask.swift */, - CE9ECAD406B234DD522B04457BCADA54 /* SessionDelegate.swift */, - D8CBA959C728F5B6931C11DE61853D88 /* SizeExtensions.swift */, - 496EC73E3893F724BA57D993B154BF49 /* Source.swift */, - 6BCDFE9C10374511CCC375D2E99C812E /* Storage.swift */, - 23963C2629645E475AB2931F62E451C7 /* String+MD5.swift */, - 4193FEA28CDEB3710C7C41537C8D5E2E /* TVMonogramView+Kingfisher.swift */, - BA9B50C2ED49A8E73A4D64C205173820 /* UIButton+Kingfisher.swift */, - 1C444AA66DFF06EB1D1D714FE5939A17 /* WKInterfaceImage+Kingfisher.swift */, - 729C05219705FA5F82BEFFE650B8F5E9 /* Support Files */, + C8C89C7A9A55466B555B62139EC4D548 /* Alamofire.modulemap */, + 7EC45995F1574DEA557A6E3BC05CFEEF /* Alamofire-dummy.m */, + 4E0BE569D743ECEC0C714CAAB568C48F /* Alamofire-Info.plist */, + 6B3BE0F5416E121307A80EA7D74C9F1F /* Alamofire-prefix.pch */, + 85174CF7F69E6FCA3002CE90EB305644 /* Alamofire-umbrella.h */, + 9BA9A2CCA476007FA9B257E213BB1E94 /* Alamofire.debug.xcconfig */, + 3F571B67CBFAF3C7031418A7801B513A /* Alamofire.release.xcconfig */, ); - name = Kingfisher; - path = Kingfisher; + name = "Support Files"; + path = "../Target Support Files/Alamofire"; sourceTree = ""; }; 1A54762D1F676258FBB651CD2FE8D36D /* Targets Support Files */ = { @@ -382,95 +427,166 @@ name = "Targets Support Files"; sourceTree = ""; }; - 1E75F81CC79CF69CB4EA4C194E904C70 /* Pods */ = { + 27E7C7DB2156391D82F573B5005BEFA0 /* Kingfisher */ = { isa = PBXGroup; children = ( - 78C3F1E5DD9988656EC031F46E619FBD /* Alamofire */, - 0F4AF1A3447E9E41BB475E0A5B868464 /* Kingfisher */, + 46CBB1E6E5F76A4B848491596108E707 /* AnimatedImageView.swift */, + DDD3326917FBC47DA75A7A9DF4A8D68B /* AuthenticationChallengeResponsable.swift */, + E3E472F69AB3AD6146CF5B1687599392 /* AVAssetImageDataProvider.swift */, + 2B0C052B339AA2BDD72BBE9C7CEDFA3C /* Box.swift */, + 1F1EE76507C32C6532481BFCF619D224 /* CacheSerializer.swift */, + C549ABE4054C58BADD9499BFD01CB5CC /* CallbackQueue.swift */, + 5B109BB831EAE85D48A06C6D4D8E375F /* CPListItem+Kingfisher.swift */, + 799E181E909BC0CAE1806256859AB012 /* Delegate.swift */, + FDDE8D838F7402500B258EECED27B9C2 /* DiskStorage.swift */, + 6FD33114F6764F686C1A80233A8A1D34 /* ExtensionHelpers.swift */, + DEBBF83A19240F77CB8282A425738F57 /* Filter.swift */, + E972E5301F6A20A1F2F4A3F3FB5189DA /* FormatIndicatedCacheSerializer.swift */, + C138D9C30CB4F78726B11E277DCF1ED4 /* GIFAnimatedImage.swift */, + 5BF6D1092A6AEA823ADE61284D6A02F5 /* GraphicsContext.swift */, + BEE5E2F810B61452A5C02C51C487AEEF /* Image.swift */, + B465AC0964110BAEAFCF634EF8832801 /* ImageBinder.swift */, + 5FF1A9204468240BA1A125C9445A0047 /* ImageCache.swift */, + 31E64283506C8735B64BD9C80EAC66F4 /* ImageContext.swift */, + 99CBCB77AD5710D6D11812BAC486BD6E /* ImageDataProcessor.swift */, + C13F847746EF4F7AE09B3A747BA544B5 /* ImageDataProvider.swift */, + 610292CBAE491B4D17CA51F66D6861EF /* ImageDownloader.swift */, + 7E785A1D945BC47127CA4CE0DE7A0BA8 /* ImageDownloaderDelegate.swift */, + C8E227E931599A9136D2BB9EB051B6D6 /* ImageDrawing.swift */, + 91B43B2C8204242C97F8659DA9C709B3 /* ImageFormat.swift */, + 34CDE0FD157ECBE5A3270DB4E5818E36 /* ImageModifier.swift */, + 285C69FC27CB96D64460B0BD1DCFFFDC /* ImagePrefetcher.swift */, + B4D4EE71842ECE47B9B60E26EC9158FE /* ImageProcessor.swift */, + A8B0124C50FC6B6BBE8A6278B8F6E983 /* ImageProgressive.swift */, + D7041BB19442F3ACDB15B140F458F078 /* ImageTransition.swift */, + CA5130A1FDB34E3B4447038BEA687890 /* ImageView+Kingfisher.swift */, + E4022A8C85369D0ACA7E38670253E3D1 /* Indicator.swift */, + F86BD1B17487999591CD15009D638029 /* KF.swift */, + 7389B8989D16FFDD6215BF99EAD4EF83 /* KFAnimatedImage.swift */, + 82800F15627BA03BC43D65EBE817E6BD /* KFImage.swift */, + 5E1D44D618B316B6D82C72F94A26E240 /* KFImageOptions.swift */, + F738ECB6F1F16EEF4863E1874544D4A0 /* KFImageProtocol.swift */, + F448016C8356D578FEDC3FF0468E0949 /* KFImageRenderer.swift */, + 7FCA8DB37C4EAD7761A2AD3929FDDDD6 /* KFOptionsSetter.swift */, + 7CC749BFB582FB54281186F7C212DC04 /* Kingfisher.swift */, + 9B0717827596EDE05BA395607E31C7FA /* KingfisherError.swift */, + 9BA3F91769ED2D36CBFCA9056FCE2A63 /* KingfisherManager.swift */, + 7CF22E24F8E9F6CCEE9CFA8E515A697C /* KingfisherOptionsInfo.swift */, + 8B76FA6AD6CFF53A0BE51BE13C1109C2 /* MemoryStorage.swift */, + 40B6FD901E48869144B91F5E03417982 /* NSButton+Kingfisher.swift */, + 0026A67CFFD17285AF03B23D2DD28B2A /* NSTextAttachment+Kingfisher.swift */, + 8C54FE1F16AF08A4A5E86EC037CBAC7A /* Placeholder.swift */, + E28BDE3A9FE93A1357F5CD74F1552342 /* RedirectHandler.swift */, + F3933AD68D7D40D3391FD238893A9E80 /* RequestModifier.swift */, + D4C0C598F4D69737485E825CF3EB38E2 /* Resource.swift */, + 987D2724E2957A7A8BB654A970164F95 /* Result.swift */, + F4852E3BB7086025361AA0A3FF25346E /* RetryStrategy.swift */, + 6CF18FE28A2E0A8843038908E4A7410E /* Runtime.swift */, + 7AEA1626758690136CDAF41CB95672E6 /* SessionDataTask.swift */, + 26CA42D477EB2ADDC226A4709E7B38A7 /* SessionDelegate.swift */, + 4E0CC80E337D75B5AB065E2F5AE4ECA1 /* SizeExtensions.swift */, + E6D33079D4E9609D5B2E73B0524B75E5 /* Source.swift */, + BBD249D56D7905164D61C4DAC613E264 /* Storage.swift */, + 4545CD1066BB1CDD9E98D073C6339E4B /* String+MD5.swift */, + 9A4130C4E6473352951BBE52AB721BC7 /* TVMonogramView+Kingfisher.swift */, + D8A5C0DFC63B97534F154442D36FA9B8 /* UIButton+Kingfisher.swift */, + 11B2551386363716C6DB6FD94B490B81 /* WKInterfaceImage+Kingfisher.swift */, + 2C01D6AB4D07F185B63D01CAC39F0ED7 /* Support Files */, ); - name = Pods; + name = Kingfisher; + path = Kingfisher; sourceTree = ""; }; - 5EE21CBB8EFB615AB450D209E054FAE6 /* iOS */ = { + 2C01D6AB4D07F185B63D01CAC39F0ED7 /* Support Files */ = { isa = PBXGroup; children = ( - 52554E1C9731D5352FDE9E63F8C5466B /* Accelerate.framework */, - 872D7EFA572ECEE8EF993C27196E16DD /* CFNetwork.framework */, - 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */, + 540265494A78FD92012AD9D9D20AD5F2 /* Kingfisher.modulemap */, + 67466EFA454A89EA2AFFBB49504EB9FC /* Kingfisher-dummy.m */, + C26221D803E0608A91ACAA9D354BBECB /* Kingfisher-Info.plist */, + 8A22A6B725B6E1FF87F3CF9BB6E30DDC /* Kingfisher-prefix.pch */, + 77870AC54264011D51C6F35408B2B263 /* Kingfisher-umbrella.h */, + 75701ABD349433AD237CC63DF8CD558F /* Kingfisher.debug.xcconfig */, + 27F553BE51F7C505891FF4C14E5C9FBB /* Kingfisher.release.xcconfig */, ); - name = iOS; + name = "Support Files"; + path = "../Target Support Files/Kingfisher"; sourceTree = ""; }; - 6F1D967767CECA40AC890F7DC03EA2B8 /* Support Files */ = { + 35A4FE241239ED465FC75D066725D275 /* Pods */ = { isa = PBXGroup; children = ( - FB0BD0DA3BA3BF98EFC7BF4305A340BA /* Alamofire.modulemap */, - 3D13E09327BDC573DE5AAFE079172AB9 /* Alamofire-dummy.m */, - B0BAB418BAD6B4B421F289A06BD5909C /* Alamofire-Info.plist */, - 24A6C9EC04946183C998CEC9FFF72EFD /* Alamofire-prefix.pch */, - 0D167A016457E1A5BD5B9A73C4FF9434 /* Alamofire-umbrella.h */, - 630CD17355E726BB7AF8B6C6A581C2FE /* Alamofire.debug.xcconfig */, - 56B4F426A74516AD9DF8A17EA00CE61E /* Alamofire.release.xcconfig */, + 80A57F333FA3B247AE468D88E796459C /* Alamofire */, + 27E7C7DB2156391D82F573B5005BEFA0 /* Kingfisher */, + B6EFC7C1EEF39EDCC1C705256AFA3075 /* SnapKit */, ); - name = "Support Files"; - path = "../Target Support Files/Alamofire"; + name = Pods; sourceTree = ""; }; - 729C05219705FA5F82BEFFE650B8F5E9 /* Support Files */ = { + 3F3E091557E2F7203F7A73FE6BCA1673 /* Support Files */ = { isa = PBXGroup; children = ( - 73DC4358317E61D22885E415D01F8DFF /* Kingfisher.modulemap */, - 6835FBF931D5C31EFFF83ED559436CDE /* Kingfisher-dummy.m */, - 1986AFB6D3CAD62304F3FA85BE29D575 /* Kingfisher-Info.plist */, - 148FA9BD1508B6E7E220C9D9BF1928F8 /* Kingfisher-prefix.pch */, - 6C321B1DFC61DFB5E1EB0BF63C4E7B05 /* Kingfisher-umbrella.h */, - 6E5C70357A0F35A05639673633F51624 /* Kingfisher.debug.xcconfig */, - EDEF84AAA5BEDA67174B11EECD29C8BD /* Kingfisher.release.xcconfig */, + 95405C25A4588F6E55B6AA02260CE374 /* SnapKit.modulemap */, + 14CEEDA7A6DF1930A777B65955863011 /* SnapKit-dummy.m */, + 787FCAF7200932CE309A97CBF33ACB60 /* SnapKit-Info.plist */, + BEE7E843B4099ACD1719D865D4B9F25D /* SnapKit-prefix.pch */, + F1A28E9E8E3211493299F42561988C41 /* SnapKit-umbrella.h */, + 0CA5626C621090BAC155CA46CA4B4699 /* SnapKit.debug.xcconfig */, + 84C9E9C49050587AA0631B8B31BB28C3 /* SnapKit.release.xcconfig */, ); name = "Support Files"; - path = "../Target Support Files/Kingfisher"; + path = "../Target Support Files/SnapKit"; + sourceTree = ""; + }; + 5EE21CBB8EFB615AB450D209E054FAE6 /* iOS */ = { + isa = PBXGroup; + children = ( + 52554E1C9731D5352FDE9E63F8C5466B /* Accelerate.framework */, + 872D7EFA572ECEE8EF993C27196E16DD /* CFNetwork.framework */, + 4207BEE6DFA63E5CF69828DD467E9674 /* Foundation.framework */, + ); + name = iOS; sourceTree = ""; }; - 78C3F1E5DD9988656EC031F46E619FBD /* Alamofire */ = { + 80A57F333FA3B247AE468D88E796459C /* Alamofire */ = { isa = PBXGroup; children = ( - 80270ADB4ECE74E17A7DB57236035920 /* AFError.swift */, - 76CC7FAE70D848424E201CB9C76CABD3 /* Alamofire.swift */, - 72447455A0B451F260AE94E750E590D7 /* AlamofireExtended.swift */, - F1B7F2B2CCF604EF10790364BAB06391 /* AuthenticationInterceptor.swift */, - 44A117735F022F30BB9DC641136E51C9 /* CachedResponseHandler.swift */, - B114DBD20B001B2A9D93F8F8C47903B6 /* Combine.swift */, - 1529C2E8B334710FA50E4C9A7153006A /* Concurrency.swift */, - 48BAA15044E6AD772F77A856FA5107F2 /* DispatchQueue+Alamofire.swift */, - 932C67B305CF8C8942FEBD2375BE2E1F /* EventMonitor.swift */, - DB1FAB3233CC80F28F7E66DF4DC9F2FA /* HTTPHeaders.swift */, - 5B56F1736D1127A32614C8CBBEF3E5F6 /* HTTPMethod.swift */, - 7EE41735C363063D401BC89633A455BE /* MultipartFormData.swift */, - 8BA6A6C6AEAC43638A2B248BBEA90EBD /* MultipartUpload.swift */, - A043F9EF1DF150712ECDD2FB7CD2E46B /* NetworkReachabilityManager.swift */, - FC710AFB6066D8535108054FD1115E94 /* Notifications.swift */, - A20EDDB802D0AEBA579A2FAC5B1B6BDE /* OperationQueue+Alamofire.swift */, - F6A1EDFD24518B3F930192EDAE086842 /* ParameterEncoder.swift */, - 249A902C9828CB270329EFFAFAC1843A /* ParameterEncoding.swift */, - AFFE766A6E0F3A621DFD9487DA3CAF7E /* Protected.swift */, - 499955C35F6D4A8F96E940FF129FF498 /* RedirectHandler.swift */, - 0341B04B64485596391C33DD9360D74E /* Request.swift */, - 6599B0D2C1F4B263D4D859853F0F348A /* RequestInterceptor.swift */, - A0DD802295003561B6C5741521D716BC /* RequestTaskMap.swift */, - BB099A6B514875F22EF98798B56B4BA5 /* Response.swift */, - C32A86B99F0C8B9E42B3D9C7F9730889 /* ResponseSerialization.swift */, - 2F3F986AF94568CEEB8AF631C72F03B2 /* Result+Alamofire.swift */, - 715993D57B4975C3CF94C6BC997595C6 /* RetryPolicy.swift */, - 04303942CB47C4C340CF398F8653BB98 /* ServerTrustEvaluation.swift */, - A1939B0AEE888F2EB258D6832523F67C /* Session.swift */, - B42FDB98681E0F2B54A53C6263FC0FB7 /* SessionDelegate.swift */, - D942B0884573FD857CFB17A94E6A5807 /* StringEncoding+Alamofire.swift */, - 404743B9B61FE5357078AFD049FC6B45 /* URLConvertible+URLRequestConvertible.swift */, - 072684B1154141462994E3AD2ACD3606 /* URLEncodedFormEncoder.swift */, - BB31688140758856323E0B3187769487 /* URLRequest+Alamofire.swift */, - B7407E80368EF1CE2A091B4F0F8F81FF /* URLSessionConfiguration+Alamofire.swift */, - 67030433EFF4B0333EC8811BC7EC47D6 /* Validation.swift */, - 6F1D967767CECA40AC890F7DC03EA2B8 /* Support Files */, + 2AE64C5352AFEE75EB677A7284ABDE59 /* AFError.swift */, + F0148907B3A18C2DF9B3D355964E537C /* Alamofire.swift */, + 907DC9525547D14B924F35B4D6792664 /* AlamofireExtended.swift */, + 2DF4497480929BC383C9404C43E30479 /* AuthenticationInterceptor.swift */, + 74E82B4D26EE55A623511D1FEF6913B1 /* CachedResponseHandler.swift */, + 6BAA0652E6B2091F985485A914ED9663 /* Combine.swift */, + 83A45C0B52AFEB667ED0777CEBB9BA08 /* Concurrency.swift */, + 5937AE3145C880EAA9D9560C432F6CC0 /* DispatchQueue+Alamofire.swift */, + 11E540405E3D1158FAFBAE4F9BDC7EE9 /* EventMonitor.swift */, + C81466D995C81B34DEE0A314B083C2D0 /* HTTPHeaders.swift */, + 32F44A08A5F580F49D5291C4A2BA8CCC /* HTTPMethod.swift */, + 667CD2987F5017A87603DE7297D94774 /* MultipartFormData.swift */, + 91586153A6E31A45B8184E55BDC2F04F /* MultipartUpload.swift */, + C2CF2CC40AE2571056B934957097004E /* NetworkReachabilityManager.swift */, + 797263D3E571383C88815218DCE6B4CC /* Notifications.swift */, + 268F34234A1960AFA99595BB83B710CE /* OperationQueue+Alamofire.swift */, + D178B7CF7375C36FFB823F1493FB21FC /* ParameterEncoder.swift */, + 9CD59BED079FC427AE725FF857613539 /* ParameterEncoding.swift */, + 25707AE1D4270D1D1FE989EF8068C8A7 /* Protected.swift */, + 75D24A24D936AA5A0801D5BC8B6EC130 /* RedirectHandler.swift */, + BC6A21BE8DA507842B3F12753CDD5716 /* Request.swift */, + C17E436AB454460A694A91918ABF6C0A /* RequestInterceptor.swift */, + 27FBDA82588FFADCB73A08FDC1C0EED8 /* RequestTaskMap.swift */, + 64F6FA35F52DD4E40C180C9B3FF358E0 /* Response.swift */, + 24E7079555F2E462A564E0B121EE2DFB /* ResponseSerialization.swift */, + 1A35F58FA9F1861DD89BD76A1FB6912D /* Result+Alamofire.swift */, + 6EC572DB04F27228F228CA58C17E9016 /* RetryPolicy.swift */, + 4A3083E1E5289D29F023A8D6809C9837 /* ServerTrustEvaluation.swift */, + 8BFCE61ADED2F9A2CD76A78A188C0CA9 /* Session.swift */, + C8D9CBFE45052D73C5F502FB595489BE /* SessionDelegate.swift */, + B93AA7C2C96625A85AEA6FB6E673DDAD /* StringEncoding+Alamofire.swift */, + 054BA59643B2127733DA8F8AAD1E4F5E /* URLConvertible+URLRequestConvertible.swift */, + 21674FEAC995EECAEE8116E61002B452 /* URLEncodedFormEncoder.swift */, + CB0A90C89BD5F9CC88A1A769CF346458 /* URLRequest+Alamofire.swift */, + F8B938A45380E77BF8ACD8EF3BA3276F /* URLSessionConfiguration+Alamofire.swift */, + E4C7082784768E1C6C01AF820DFEFDF1 /* Validation.swift */, + 16F9A14783155ED2458CE900252543A1 /* Support Files */, ); name = Alamofire; path = Alamofire; @@ -493,35 +609,82 @@ path = "Target Support Files/Pods-CatStaGram"; sourceTree = ""; }; - CF1408CF629C7361332E53B88F7BD30C = { + A76DE9D2D14A32CEC3D2C78ECC8954A8 /* Products */ = { isa = PBXGroup; children = ( - 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, - 03C5C200A0787E300053CFA8F53CA094 /* Frameworks */, - 1E75F81CC79CF69CB4EA4C194E904C70 /* Pods */, - E258A569C65C0563D771E2C2FC871EC4 /* Products */, - 1A54762D1F676258FBB651CD2FE8D36D /* Targets Support Files */, + 5D797E9A5C5782CE845840781FA1CC81 /* Alamofire */, + C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */, + A9CCB3A1E65C7F0BD9B4C55FDA273F66 /* Pods-CatStaGram */, + 979486118B3E90C08386079D57962701 /* SnapKit */, ); + name = Products; sourceTree = ""; }; - E258A569C65C0563D771E2C2FC871EC4 /* Products */ = { + B6EFC7C1EEF39EDCC1C705256AFA3075 /* SnapKit */ = { isa = PBXGroup; children = ( - 5D797E9A5C5782CE845840781FA1CC81 /* Alamofire */, - C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */, - A9CCB3A1E65C7F0BD9B4C55FDA273F66 /* Pods-CatStaGram */, + FCA8F67D5E603B9D9888F34599FC6AAA /* Constraint.swift */, + A0EDA61BA557420CA7C3E24BE64E495E /* ConstraintAttributes.swift */, + 68BB2620AEB63F8E9B3D27DBBCFE300D /* ConstraintConfig.swift */, + 03EED79B9CB8F66CBC3776943530D0C1 /* ConstraintConstantTarget.swift */, + 3C9910954DCC3CF80C42993711A0B19F /* ConstraintDescription.swift */, + 8CE918EBDB02B9B27E71402FCB954402 /* ConstraintDirectionalInsets.swift */, + A1AD755643055311B24F71017C844D2D /* ConstraintDirectionalInsetTarget.swift */, + 43B784610AC76ECDEBDCB3AE749016F5 /* ConstraintDSL.swift */, + C6B20F22CEC80124AF2B6C9DD78E40F2 /* ConstraintInsets.swift */, + 238EDDD638F2EDB57E54DDDB3B8BEACF /* ConstraintInsetTarget.swift */, + 364987DCEA6E58F38028166AB008F9CD /* ConstraintItem.swift */, + CF26F23415267C306D13B6524C4FF622 /* ConstraintLayoutGuide.swift */, + 5397B5B4479B64B79C83FC512332BB86 /* ConstraintLayoutGuide+Extensions.swift */, + D9C4DE86B5FA3554D2CE8756E7C62C05 /* ConstraintLayoutGuideDSL.swift */, + F1C39CAEDC4116F60F5D9A988BBCBA88 /* ConstraintLayoutSupport.swift */, + 23350F61A142EDC6837D0E4A8A5FC8EF /* ConstraintLayoutSupportDSL.swift */, + 936CE1FB37015B4B9EA165AF51FA4947 /* ConstraintMaker.swift */, + 8A4A070E2855042F335EFD9E5E195C5B /* ConstraintMakerEditable.swift */, + 8E1DF45629585C93475F05C4C705B54B /* ConstraintMakerExtendable.swift */, + DEAE448343149DBD139FF6ECDFCB540D /* ConstraintMakerFinalizable.swift */, + F22C681FC81C694E8D747D6AECBAEF8E /* ConstraintMakerPrioritizable.swift */, + 23A8F7936CB5C19BE80BD3356E614D52 /* ConstraintMakerRelatable.swift */, + 2643C189D74D592B92995979CC1BEA9D /* ConstraintMakerRelatable+Extensions.swift */, + 85F4E1D29D79341BA832E746E5BDCFFA /* ConstraintMultiplierTarget.swift */, + 1D722BE2B78702E0D89F4AD6CB7A08D9 /* ConstraintOffsetTarget.swift */, + 178259530DD4732FDC78DC27F9D1452E /* ConstraintPriority.swift */, + 9D0238C1747B9EF2D7860DFC44E8C5B6 /* ConstraintPriorityTarget.swift */, + BDE1C754F3F4878CA35D1AAE0FDF70EC /* ConstraintRelatableTarget.swift */, + 31CB56705A8EE8E7C074CD7E3D8F671B /* ConstraintRelation.swift */, + 9DBF96DDFE3AFC890BE19FD063447453 /* ConstraintView.swift */, + 923866AFC245CAED8B1E67CAE9A8256A /* ConstraintView+Extensions.swift */, + F566C7CBE07FDFA59C7103FF1EFF3896 /* ConstraintViewDSL.swift */, + 22CE6618974753838E7BAB54EBCE7DBF /* Debugging.swift */, + 4F42A45BF9A88CF234C5C8B1645A4BEB /* LayoutConstraint.swift */, + 403E973BCBF657DC44AA8E268567E39C /* LayoutConstraintItem.swift */, + 24E66FE8F126D7F6A70D6214DD496549 /* Typealiases.swift */, + 5A1F0BB5484DE62961655DC057183C5F /* UILayoutSupport+Extensions.swift */, + 3F3E091557E2F7203F7A73FE6BCA1673 /* Support Files */, + ); + name = SnapKit; + path = SnapKit; + sourceTree = ""; + }; + CF1408CF629C7361332E53B88F7BD30C = { + isa = PBXGroup; + children = ( + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, + 03C5C200A0787E300053CFA8F53CA094 /* Frameworks */, + 35A4FE241239ED465FC75D066725D275 /* Pods */, + A76DE9D2D14A32CEC3D2C78ECC8954A8 /* Products */, + 1A54762D1F676258FBB651CD2FE8D36D /* Targets Support Files */, ); - name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ - 43CCF09A05DFAC4EB3150A6E0E759A87 /* Headers */ = { + 10B6983DB05C146A460F34BEBEE83A25 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 18E839CC4FD7F1CD252F4BCA4C70BED1 /* Pods-CatStaGram-umbrella.h in Headers */, + 712E2B43577DA59F7504EA4478C346D0 /* Pods-CatStaGram-umbrella.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -533,6 +696,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5FA4790A32ECD84F2CEA55509107BD0A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A3E3AB7C765C21AE17B1E6EE6560040C /* SnapKit-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 7AE52B176E9872452FD890FC7F460CE1 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -544,6 +715,24 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 19622742EBA51E823D6DAE3F8CDBFAD4 /* SnapKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58EAA7FF29EA237538F747F2AF468203 /* Build configuration list for PBXNativeTarget "SnapKit" */; + buildPhases = ( + 5FA4790A32ECD84F2CEA55509107BD0A /* Headers */, + 7B7E69B2799BABEB7764F85296B591B7 /* Sources */, + E8E4E7CE8309D3EF7B7476AC57986BE9 /* Frameworks */, + 820C67429A90336F5AF1C850DF03B1B9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SnapKit; + productName = SnapKit; + productReference = 979486118B3E90C08386079D57962701 /* SnapKit */; + productType = "com.apple.product-type.framework"; + }; E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */ = { isa = PBXNativeTarget; buildConfigurationList = 69ACD8654734266A348C6FF68E734010 /* Build configuration list for PBXNativeTarget "Kingfisher" */; @@ -582,18 +771,19 @@ }; F393950DF5E71F5A62A43D9F28802975 /* Pods-CatStaGram */ = { isa = PBXNativeTarget; - buildConfigurationList = 8D021AE99749D2A1BE107C5220D8DAE8 /* Build configuration list for PBXNativeTarget "Pods-CatStaGram" */; + buildConfigurationList = 0BA200DFC0434A43B0FFB5411B473A44 /* Build configuration list for PBXNativeTarget "Pods-CatStaGram" */; buildPhases = ( - 43CCF09A05DFAC4EB3150A6E0E759A87 /* Headers */, - 33C417A8C3A976B1FCC021782A062736 /* Sources */, - B23375F7E905D644F561F7559FA187BF /* Frameworks */, - 0C3EF2A38ACDF47EE76DDF4D565771CC /* Resources */, + 10B6983DB05C146A460F34BEBEE83A25 /* Headers */, + 850DF93C8755B2BE0B2EB2520273E114 /* Sources */, + 773F7A3D136BD088B602E619FEB2C970 /* Frameworks */, + B8DF3DBEAF278A72C5B2553D609AA457 /* Resources */, ); buildRules = ( ); dependencies = ( - 683A175A1094AF133A14F0CFE57BDAEA /* PBXTargetDependency */, - F5BC4F5CBDFA1DEFFA581E75C37BB059 /* PBXTargetDependency */, + 3AF8FAB9E11CF1FBABF3DFA6BF943B54 /* PBXTargetDependency */, + 3E8D736638CEFCE74367FE3478BF21A9 /* PBXTargetDependency */, + 72DF9E6E45DC0D18FF7E2A85302F2189 /* PBXTargetDependency */, ); name = "Pods-CatStaGram"; productName = Pods_CatStaGram; @@ -618,50 +808,50 @@ en, ); mainGroup = CF1408CF629C7361332E53B88F7BD30C; - productRefGroup = E258A569C65C0563D771E2C2FC871EC4 /* Products */; + productRefGroup = A76DE9D2D14A32CEC3D2C78ECC8954A8 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( EAAA1AD3A8A1B59AB91319EE40752C6D /* Alamofire */, E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */, F393950DF5E71F5A62A43D9F28802975 /* Pods-CatStaGram */, + 19622742EBA51E823D6DAE3F8CDBFAD4 /* SnapKit */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 0C3EF2A38ACDF47EE76DDF4D565771CC /* Resources */ = { + 820C67429A90336F5AF1C850DF03B1B9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - CFEB3E9FD20A01120B40D65B82D8F26F /* Resources */ = { + B8DF3DBEAF278A72C5B2553D609AA457 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - E9D4145FA41F60FFAB33A07796D9ED97 /* Resources */ = { + CFEB3E9FD20A01120B40D65B82D8F26F /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 33C417A8C3A976B1FCC021782A062736 /* Sources */ = { - isa = PBXSourcesBuildPhase; + E9D4145FA41F60FFAB33A07796D9ED97 /* Resources */ = { + isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - B6C05F9D9AF254B332EAAB3064F7F147 /* Pods-CatStaGram-dummy.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ 3C1DA515D615F8CE75565ACE14378882 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -731,6 +921,59 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7B7E69B2799BABEB7764F85296B591B7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6EAC8B6FA2D115DE708A58C601DD25E1 /* Constraint.swift in Sources */, + 0B994CDC79B1AD3A7BE62490D27C60C8 /* ConstraintAttributes.swift in Sources */, + 3660B4F629053ABC3C1DF69366770289 /* ConstraintConfig.swift in Sources */, + 9C9548E8B4675954566202F7B87FF0E6 /* ConstraintConstantTarget.swift in Sources */, + 5380454C48A12A6E376122ABD8096968 /* ConstraintDescription.swift in Sources */, + E98DC8FB2578E76A812E0C94BA10E1B1 /* ConstraintDirectionalInsets.swift in Sources */, + 04637C70546B34F93C3A1D79C3F78B37 /* ConstraintDirectionalInsetTarget.swift in Sources */, + 56AC6DC6459AE47E7BC4AF06E5B148D2 /* ConstraintDSL.swift in Sources */, + 604D3C93C17978C23600EC415949AB64 /* ConstraintInsets.swift in Sources */, + 9E02CFFEFE1BEC978B512286F03D31DA /* ConstraintInsetTarget.swift in Sources */, + 69DE75BA185BBAB5FDAA28321F3C849F /* ConstraintItem.swift in Sources */, + 611CEF7FF1EF9EA1BCEF6C73EEE3ACCF /* ConstraintLayoutGuide.swift in Sources */, + 6AC1B881BB319C89AD023A02CDC8FC3D /* ConstraintLayoutGuide+Extensions.swift in Sources */, + EF21586EC3DFF6097A58EEC87376A0C1 /* ConstraintLayoutGuideDSL.swift in Sources */, + 00A2749F46C967ED4725A32357E3FB1B /* ConstraintLayoutSupport.swift in Sources */, + 56A72F6D13D930C4A2568F24DEA33C8D /* ConstraintLayoutSupportDSL.swift in Sources */, + FAB03959C2357E325B19E08BC4775DAB /* ConstraintMaker.swift in Sources */, + 220D6AC9B1B1EC8EFFB204F9C1EAC842 /* ConstraintMakerEditable.swift in Sources */, + A5A8BF973BFE9C9304372A26C9F2E35B /* ConstraintMakerExtendable.swift in Sources */, + 42B6ACFCF650183030867CB3EF345E95 /* ConstraintMakerFinalizable.swift in Sources */, + B43017FEC99D227D4CDD81DD2C27D9D9 /* ConstraintMakerPrioritizable.swift in Sources */, + 183D8B3E057B885EA7DF9A8CDCCE9029 /* ConstraintMakerRelatable.swift in Sources */, + 95E194A3AABB5407231E898B6686F73E /* ConstraintMakerRelatable+Extensions.swift in Sources */, + F7C0960CEEB8F0C18F4503B405EAC08F /* ConstraintMultiplierTarget.swift in Sources */, + A03FDA8BC5741880B1EF11AFD248EF48 /* ConstraintOffsetTarget.swift in Sources */, + 06E43B4751069B47B3BD4AFD936A57E3 /* ConstraintPriority.swift in Sources */, + 4DA72FD7F1FB2C0449EDEF4B8A579807 /* ConstraintPriorityTarget.swift in Sources */, + 55E51F45F1E157D3B4942BA7252C277E /* ConstraintRelatableTarget.swift in Sources */, + 4BAD99B7394E225CEDBF94B8100BCC7F /* ConstraintRelation.swift in Sources */, + B752F7C4BECB65894B1F49421049CE5F /* ConstraintView.swift in Sources */, + F953AA9104BFE0C2DAD639EA60104A75 /* ConstraintView+Extensions.swift in Sources */, + 256558233B40ACA6818F143BBC5B8017 /* ConstraintViewDSL.swift in Sources */, + 5D36B99F3CAB1FC7337082D9581FECA6 /* Debugging.swift in Sources */, + 0591132B5EA1BE4DDA268D8A9C3D0421 /* LayoutConstraint.swift in Sources */, + 98F570DA48370453D648BD526FDAAEAA /* LayoutConstraintItem.swift in Sources */, + BD47CB74CD9B2B4D2D942C9B65748DFB /* SnapKit-dummy.m in Sources */, + B03B5579590D528CBA6D11FF638BC2AF /* Typealiases.swift in Sources */, + 38B9D58E94D9C1CECD7E381C64A2329D /* UILayoutSupport+Extensions.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 850DF93C8755B2BE0B2EB2520273E114 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AC93F7D982E8056E781FDCFB59C24E11 /* Pods-CatStaGram-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F5D2A45FBA06D86A537CB441D5BF4FF4 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -778,22 +1021,28 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 683A175A1094AF133A14F0CFE57BDAEA /* PBXTargetDependency */ = { + 3AF8FAB9E11CF1FBABF3DFA6BF943B54 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = Alamofire; target = EAAA1AD3A8A1B59AB91319EE40752C6D /* Alamofire */; - targetProxy = BADC5D608C035FEA12332200B1BEA3AA /* PBXContainerItemProxy */; + targetProxy = 33A8CD687A4E5E0DF9CC5D157A31D598 /* PBXContainerItemProxy */; }; - F5BC4F5CBDFA1DEFFA581E75C37BB059 /* PBXTargetDependency */ = { + 3E8D736638CEFCE74367FE3478BF21A9 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = Kingfisher; target = E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */; - targetProxy = 86507F3308AD3FD588A9636FBE2FE558 /* PBXContainerItemProxy */; + targetProxy = 5DA80A8427E962423623642D1CE4C8A9 /* PBXContainerItemProxy */; + }; + 72DF9E6E45DC0D18FF7E2A85302F2189 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = SnapKit; + target = 19622742EBA51E823D6DAE3F8CDBFAD4 /* SnapKit */; + targetProxy = 950E4A232F336281C8F57BF7EE77C93D /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 08ABDF3E2A01819F2628F573E591761D /* Debug */ = { + 071958470D06ED04439130B07B7F73A6 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 70D1A3E8DA5E73DFADB87E35404AAA05 /* Pods-CatStaGram.debug.xcconfig */; buildSettings = { @@ -830,6 +1079,44 @@ }; name = Debug; }; + 240E3598F0805B86C76A925F911BDA56 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F368C1F054959568F9FC5262C8FD9F8C /* Pods-CatStaGram.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-CatStaGram/Pods-CatStaGram-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-CatStaGram/Pods-CatStaGram.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; 4214E6946993DF02ED98D89047C64016 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -896,9 +1183,45 @@ }; name = Debug; }; + 8C925430B65BE03F92ABCD1B2005EE0C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 84C9E9C49050587AA0631B8B31BB28C3 /* SnapKit.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/SnapKit/SnapKit-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/SnapKit/SnapKit-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/SnapKit/SnapKit.modulemap"; + PRODUCT_MODULE_NAME = SnapKit; + PRODUCT_NAME = SnapKit; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; 90A4588B06F8745E7FCD1B00204D6241 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 56B4F426A74516AD9DF8A17EA00CE61E /* Alamofire.release.xcconfig */; + baseConfigurationReference = 3F571B67CBFAF3C7031418A7801B513A /* Alamofire.release.xcconfig */; buildSettings = { CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; @@ -934,7 +1257,7 @@ }; 980A58862D8A5086E2825CF9017AC8DD /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6E5C70357A0F35A05639673633F51624 /* Kingfisher.debug.xcconfig */; + baseConfigurationReference = 75701ABD349433AD237CC63DF8CD558F /* Kingfisher.debug.xcconfig */; buildSettings = { CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; @@ -969,7 +1292,7 @@ }; 9E98C04A5FA16D8AD5D48C1861179497 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 630CD17355E726BB7AF8B6C6A581C2FE /* Alamofire.debug.xcconfig */; + baseConfigurationReference = 9BA9A2CCA476007FA9B257E213BB1E94 /* Alamofire.debug.xcconfig */; buildSettings = { CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; @@ -1002,9 +1325,9 @@ }; name = Debug; }; - BFD9E4B58F44191AF73A3434AAF6831F /* Release */ = { + A6F0173BAEB974532D93E05E5A94E50A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = EDEF84AAA5BEDA67174B11EECD29C8BD /* Kingfisher.release.xcconfig */; + baseConfigurationReference = 0CA5626C621090BAC155CA46CA4B4699 /* SnapKit.debug.xcconfig */; buildSettings = { CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; @@ -1015,34 +1338,32 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_PREFIX_HEADER = "Target Support Files/Kingfisher/Kingfisher-prefix.pch"; - INFOPLIST_FILE = "Target Support Files/Kingfisher/Kingfisher-Info.plist"; + GCC_PREFIX_HEADER = "Target Support Files/SnapKit/SnapKit-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/SnapKit/SnapKit-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MODULEMAP_FILE = "Target Support Files/Kingfisher/Kingfisher.modulemap"; - PRODUCT_MODULE_NAME = Kingfisher; - PRODUCT_NAME = Kingfisher; + MODULEMAP_FILE = "Target Support Files/SnapKit/SnapKit.modulemap"; + PRODUCT_MODULE_NAME = SnapKit; + PRODUCT_NAME = SnapKit; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; - name = Release; + name = Debug; }; - C5B5D0306C480B3EDBBDE65FF216B6ED /* Release */ = { + BFD9E4B58F44191AF73A3434AAF6831F /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F368C1F054959568F9FC5262C8FD9F8C /* Pods-CatStaGram.release.xcconfig */; + baseConfigurationReference = 27F553BE51F7C505891FF4C14E5C9FBB /* Kingfisher.release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; @@ -1052,23 +1373,22 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "Target Support Files/Pods-CatStaGram/Pods-CatStaGram-Info.plist"; + GCC_PREFIX_HEADER = "Target Support Files/Kingfisher/Kingfisher-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/Kingfisher/Kingfisher-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MACH_O_TYPE = staticlib; - MODULEMAP_FILE = "Target Support Files/Pods-CatStaGram/Pods-CatStaGram.modulemap"; - OTHER_LDFLAGS = ""; - OTHER_LIBTOOLFLAGS = ""; - PODS_ROOT = "$(SRCROOT)"; - PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + MODULEMAP_FILE = "Target Support Files/Kingfisher/Kingfisher.modulemap"; + PRODUCT_MODULE_NAME = Kingfisher; + PRODUCT_NAME = Kingfisher; SDKROOT = iphoneos; SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -1141,6 +1461,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 0BA200DFC0434A43B0FFB5411B473A44 /* Build configuration list for PBXNativeTarget "Pods-CatStaGram" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 071958470D06ED04439130B07B7F73A6 /* Debug */, + 240E3598F0805B86C76A925F911BDA56 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1150,29 +1479,29 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 69ACD8654734266A348C6FF68E734010 /* Build configuration list for PBXNativeTarget "Kingfisher" */ = { + 58EAA7FF29EA237538F747F2AF468203 /* Build configuration list for PBXNativeTarget "SnapKit" */ = { isa = XCConfigurationList; buildConfigurations = ( - 980A58862D8A5086E2825CF9017AC8DD /* Debug */, - BFD9E4B58F44191AF73A3434AAF6831F /* Release */, + A6F0173BAEB974532D93E05E5A94E50A /* Debug */, + 8C925430B65BE03F92ABCD1B2005EE0C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 8A212264186B8822192F9C369D7DE4BB /* Build configuration list for PBXNativeTarget "Alamofire" */ = { + 69ACD8654734266A348C6FF68E734010 /* Build configuration list for PBXNativeTarget "Kingfisher" */ = { isa = XCConfigurationList; buildConfigurations = ( - 9E98C04A5FA16D8AD5D48C1861179497 /* Debug */, - 90A4588B06F8745E7FCD1B00204D6241 /* Release */, + 980A58862D8A5086E2825CF9017AC8DD /* Debug */, + BFD9E4B58F44191AF73A3434AAF6831F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 8D021AE99749D2A1BE107C5220D8DAE8 /* Build configuration list for PBXNativeTarget "Pods-CatStaGram" */ = { + 8A212264186B8822192F9C369D7DE4BB /* Build configuration list for PBXNativeTarget "Alamofire" */ = { isa = XCConfigurationList; buildConfigurations = ( - 08ABDF3E2A01819F2628F573E591761D /* Debug */, - C5B5D0306C480B3EDBBDE65FF216B6ED /* Release */, + 9E98C04A5FA16D8AD5D48C1861179497 /* Debug */, + 90A4588B06F8745E7FCD1B00204D6241 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/SnapKit.xcscheme b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/SnapKit.xcscheme new file mode 100644 index 0000000..ee0a410 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/SnapKit.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist index f8edfc0..41a6dcf 100644 --- a/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -25,6 +25,13 @@ orderHint 2 + SnapKit.xcscheme + + isShown + + orderHint + 3 + SuppressBuildableAutocreation diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/LICENSE b/jaem/week7/CatStaGram/Pods/SnapKit/LICENSE new file mode 100644 index 0000000..a18ccfb --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/README.md b/jaem/week7/CatStaGram/Pods/SnapKit/README.md new file mode 100644 index 0000000..b36df7a --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/README.md @@ -0,0 +1,155 @@ + + +SnapKit is a DSL to make Auto Layout easy on both iOS and OS X. + +[![Build Status](https://travis-ci.org/SnapKit/SnapKit.svg)](https://travis-ci.org/SnapKit/SnapKit) +[![Platform](https://img.shields.io/cocoapods/p/SnapKit.svg?style=flat)](https://github.com/SnapKit/SnapKit) +[![Cocoapods Compatible](https://img.shields.io/cocoapods/v/SnapKit.svg)](https://cocoapods.org/pods/SnapKit) +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) + +#### ⚠️ **To use with Swift 4.x please ensure you are using >= 4.0.0** ⚠️ +#### ⚠️ **To use with Swift 5.x please ensure you are using >= 5.0.0** ⚠️ + +## Contents + +- [Requirements](#requirements) +- [Migration Guides](#migration-guides) +- [Communication](#communication) +- [Installation](#installation) +- [Usage](#usage) +- [Credits](#credits) +- [License](#license) + +## Requirements + +- iOS 10.0+ / Mac OS X 10.12+ / tvOS 10.0+ +- Xcode 10.0+ +- Swift 4.0+ + +## Migration Guides + +- [SnapKit 3.0 Migration Guide](Documentation/SnapKit%203.0%20Migration%20Guide.md) + +## Communication + +- If you **need help**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/snapkit). (Tag 'snapkit') +- If you'd like to **ask a general question**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/snapkit). +- If you **found a bug**, open an issue. +- If you **have a feature request**, open an issue. +- If you **want to contribute**, submit a pull request. + + +## Installation + +### CocoaPods + +[CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command: + +```bash +$ gem install cocoapods +``` + +> CocoaPods 1.1.0+ is required to build SnapKit 4.0.0+. + +To integrate SnapKit into your Xcode project using CocoaPods, specify it in your `Podfile`: + +```ruby +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '10.0' +use_frameworks! + +target '' do + pod 'SnapKit', '~> 5.6.0' +end +``` + +Then, run the following command: + +```bash +$ pod install +``` + +### Carthage + +[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. + +You can install Carthage with [Homebrew](http://brew.sh/) using the following command: + +```bash +$ brew update +$ brew install carthage +``` + +To integrate SnapKit into your Xcode project using Carthage, specify it in your `Cartfile`: + +```ogdl +github "SnapKit/SnapKit" ~> 5.0.0 +``` + +Run `carthage update` to build the framework and drag the built `SnapKit.framework` into your Xcode project. + +### Swift Package Manager + +[Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies. + +> Xcode 11+ is required to build SnapKit using Swift Package Manager. + +To integrate SnapKit into your Xcode project using Swift Package Manager, add it to the dependencies value of your `Package.swift`: + +```swift +dependencies: [ + .package(url: "https://github.com/SnapKit/SnapKit.git", .upToNextMajor(from: "5.0.1")) +] +``` + +### Manually + +If you prefer not to use either of the aforementioned dependency managers, you can integrate SnapKit into your project manually. + +--- + +## Usage + +### Quick Start + +```swift +import SnapKit + +class MyViewController: UIViewController { + + lazy var box = UIView() + + override func viewDidLoad() { + super.viewDidLoad() + + self.view.addSubview(box) + box.backgroundColor = .green + box.snp.makeConstraints { (make) -> Void in + make.width.height.equalTo(50) + make.center.equalTo(self.view) + } + } + +} +``` + +### Playground +You can try SnapKit in Playground. + +**Note:** + +> To try SnapKit in playground, open `SnapKit.xcworkspace` and build SnapKit.framework for any simulator first. + +### Resources + +- [Documentation](https://snapkit.github.io/SnapKit/docs/) +- [F.A.Q.](https://snapkit.github.io/SnapKit/faq/) + +## Credits + +- Robert Payne ([@robertjpayne](https://twitter.com/robertjpayne)) +- Many other contributors + +## License + +SnapKit is released under the MIT license. See LICENSE for details. diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/Constraint.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/Constraint.swift new file mode 100644 index 0000000..37409b3 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/Constraint.swift @@ -0,0 +1,341 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + +public final class Constraint { + + internal let sourceLocation: (String, UInt) + internal let label: String? + + private let from: ConstraintItem + private let to: ConstraintItem + private let relation: ConstraintRelation + private let multiplier: ConstraintMultiplierTarget + private var constant: ConstraintConstantTarget { + didSet { + self.updateConstantAndPriorityIfNeeded() + } + } + private var priority: ConstraintPriorityTarget { + didSet { + self.updateConstantAndPriorityIfNeeded() + } + } + public var layoutConstraints: [LayoutConstraint] + + public var isActive: Bool { + set { + if newValue { + activate() + } + else { + deactivate() + } + } + + get { + for layoutConstraint in self.layoutConstraints { + if layoutConstraint.isActive { + return true + } + } + return false + } + } + + // MARK: Initialization + + internal init(from: ConstraintItem, + to: ConstraintItem, + relation: ConstraintRelation, + sourceLocation: (String, UInt), + label: String?, + multiplier: ConstraintMultiplierTarget, + constant: ConstraintConstantTarget, + priority: ConstraintPriorityTarget) { + self.from = from + self.to = to + self.relation = relation + self.sourceLocation = sourceLocation + self.label = label + self.multiplier = multiplier + self.constant = constant + self.priority = priority + self.layoutConstraints = [] + + // get attributes + let layoutFromAttributes = self.from.attributes.layoutAttributes + let layoutToAttributes = self.to.attributes.layoutAttributes + + // get layout from + let layoutFrom = self.from.layoutConstraintItem! + + // get relation + let layoutRelation = self.relation.layoutRelation + + for layoutFromAttribute in layoutFromAttributes { + // get layout to attribute + let layoutToAttribute: LayoutAttribute + #if os(iOS) || os(tvOS) + if layoutToAttributes.count > 0 { + if self.from.attributes == .edges && self.to.attributes == .margins { + switch layoutFromAttribute { + case .left: + layoutToAttribute = .leftMargin + case .right: + layoutToAttribute = .rightMargin + case .top: + layoutToAttribute = .topMargin + case .bottom: + layoutToAttribute = .bottomMargin + default: + fatalError() + } + } else if self.from.attributes == .margins && self.to.attributes == .edges { + switch layoutFromAttribute { + case .leftMargin: + layoutToAttribute = .left + case .rightMargin: + layoutToAttribute = .right + case .topMargin: + layoutToAttribute = .top + case .bottomMargin: + layoutToAttribute = .bottom + default: + fatalError() + } + } else if self.from.attributes == .directionalEdges && self.to.attributes == .directionalMargins { + switch layoutFromAttribute { + case .leading: + layoutToAttribute = .leadingMargin + case .trailing: + layoutToAttribute = .trailingMargin + case .top: + layoutToAttribute = .topMargin + case .bottom: + layoutToAttribute = .bottomMargin + default: + fatalError() + } + } else if self.from.attributes == .directionalMargins && self.to.attributes == .directionalEdges { + switch layoutFromAttribute { + case .leadingMargin: + layoutToAttribute = .leading + case .trailingMargin: + layoutToAttribute = .trailing + case .topMargin: + layoutToAttribute = .top + case .bottomMargin: + layoutToAttribute = .bottom + default: + fatalError() + } + } else if self.from.attributes == self.to.attributes { + layoutToAttribute = layoutFromAttribute + } else { + layoutToAttribute = layoutToAttributes[0] + } + } else { + if self.to.target == nil && (layoutFromAttribute == .centerX || layoutFromAttribute == .centerY) { + layoutToAttribute = layoutFromAttribute == .centerX ? .left : .top + } else { + layoutToAttribute = layoutFromAttribute + } + } + #else + if self.from.attributes == self.to.attributes { + layoutToAttribute = layoutFromAttribute + } else if layoutToAttributes.count > 0 { + layoutToAttribute = layoutToAttributes[0] + } else { + layoutToAttribute = layoutFromAttribute + } + #endif + + // get layout constant + let layoutConstant: CGFloat = self.constant.constraintConstantTargetValueFor(layoutAttribute: layoutToAttribute) + + // get layout to + var layoutTo: AnyObject? = self.to.target + + // use superview if possible + if layoutTo == nil && layoutToAttribute != .width && layoutToAttribute != .height { + layoutTo = layoutFrom.superview + } + + // create layout constraint + let layoutConstraint = LayoutConstraint( + item: layoutFrom, + attribute: layoutFromAttribute, + relatedBy: layoutRelation, + toItem: layoutTo, + attribute: layoutToAttribute, + multiplier: self.multiplier.constraintMultiplierTargetValue, + constant: layoutConstant + ) + + // set label + layoutConstraint.label = self.label + + // set priority + layoutConstraint.priority = LayoutPriority(rawValue: self.priority.constraintPriorityTargetValue) + + // set constraint + layoutConstraint.constraint = self + + // append + self.layoutConstraints.append(layoutConstraint) + } + } + + // MARK: Public + + @available(*, deprecated, renamed:"activate()") + public func install() { + self.activate() + } + + @available(*, deprecated, renamed:"deactivate()") + public func uninstall() { + self.deactivate() + } + + public func activate() { + self.activateIfNeeded() + } + + public func deactivate() { + self.deactivateIfNeeded() + } + + @discardableResult + public func update(offset: ConstraintOffsetTarget) -> Constraint { + self.constant = offset.constraintOffsetTargetValue + return self + } + + @discardableResult + public func update(inset: ConstraintInsetTarget) -> Constraint { + self.constant = inset.constraintInsetTargetValue + return self + } + + #if os(iOS) || os(tvOS) + @discardableResult + @available(iOS 11.0, tvOS 11.0, *) + public func update(inset: ConstraintDirectionalInsetTarget) -> Constraint { + self.constant = inset.constraintDirectionalInsetTargetValue + return self + } + #endif + + @discardableResult + public func update(priority: ConstraintPriorityTarget) -> Constraint { + self.priority = priority.constraintPriorityTargetValue + return self + } + + @discardableResult + public func update(priority: ConstraintPriority) -> Constraint { + self.priority = priority.value + return self + } + + @available(*, deprecated, renamed:"update(offset:)") + public func updateOffset(amount: ConstraintOffsetTarget) -> Void { self.update(offset: amount) } + + @available(*, deprecated, renamed:"update(inset:)") + public func updateInsets(amount: ConstraintInsetTarget) -> Void { self.update(inset: amount) } + + @available(*, deprecated, renamed:"update(priority:)") + public func updatePriority(amount: ConstraintPriorityTarget) -> Void { self.update(priority: amount) } + + @available(*, deprecated, message:"Use update(priority: ConstraintPriorityTarget) instead.") + public func updatePriorityRequired() -> Void {} + + @available(*, deprecated, message:"Use update(priority: ConstraintPriorityTarget) instead.") + public func updatePriorityHigh() -> Void { fatalError("Must be implemented by Concrete subclass.") } + + @available(*, deprecated, message:"Use update(priority: ConstraintPriorityTarget) instead.") + public func updatePriorityMedium() -> Void { fatalError("Must be implemented by Concrete subclass.") } + + @available(*, deprecated, message:"Use update(priority: ConstraintPriorityTarget) instead.") + public func updatePriorityLow() -> Void { fatalError("Must be implemented by Concrete subclass.") } + + // MARK: Internal + + internal func updateConstantAndPriorityIfNeeded() { + for layoutConstraint in self.layoutConstraints { + let attribute = (layoutConstraint.secondAttribute == .notAnAttribute) ? layoutConstraint.firstAttribute : layoutConstraint.secondAttribute + layoutConstraint.constant = self.constant.constraintConstantTargetValueFor(layoutAttribute: attribute) + + let requiredPriority = ConstraintPriority.required.value + if (layoutConstraint.priority.rawValue < requiredPriority), (self.priority.constraintPriorityTargetValue != requiredPriority) { + layoutConstraint.priority = LayoutPriority(rawValue: self.priority.constraintPriorityTargetValue) + } + } + } + + internal func activateIfNeeded(updatingExisting: Bool = false) { + guard let item = self.from.layoutConstraintItem else { + print("WARNING: SnapKit failed to get from item from constraint. Activate will be a no-op.") + return + } + let layoutConstraints = self.layoutConstraints + + if updatingExisting { + var existingLayoutConstraints: [LayoutConstraint] = [] + for constraint in item.constraints { + existingLayoutConstraints += constraint.layoutConstraints + } + + for layoutConstraint in layoutConstraints { + let existingLayoutConstraint = existingLayoutConstraints.first { $0 == layoutConstraint } + guard let updateLayoutConstraint = existingLayoutConstraint else { + fatalError("Updated constraint could not find existing matching constraint to update: \(layoutConstraint)") + } + + let updateLayoutAttribute = (updateLayoutConstraint.secondAttribute == .notAnAttribute) ? updateLayoutConstraint.firstAttribute : updateLayoutConstraint.secondAttribute + updateLayoutConstraint.constant = self.constant.constraintConstantTargetValueFor(layoutAttribute: updateLayoutAttribute) + } + } else { + NSLayoutConstraint.activate(layoutConstraints) + item.add(constraints: [self]) + } + } + + internal func deactivateIfNeeded() { + guard let item = self.from.layoutConstraintItem else { + print("WARNING: SnapKit failed to get from item from constraint. Deactivate will be a no-op.") + return + } + let layoutConstraints = self.layoutConstraints + NSLayoutConstraint.deactivate(layoutConstraints) + item.remove(constraints: [self]) + } +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintAttributes.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintAttributes.swift new file mode 100644 index 0000000..408308b --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintAttributes.swift @@ -0,0 +1,203 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +internal struct ConstraintAttributes : OptionSet, ExpressibleByIntegerLiteral { + + typealias IntegerLiteralType = UInt + + internal init(rawValue: UInt) { + self.rawValue = rawValue + } + internal init(_ rawValue: UInt) { + self.init(rawValue: rawValue) + } + internal init(nilLiteral: ()) { + self.rawValue = 0 + } + internal init(integerLiteral rawValue: IntegerLiteralType) { + self.init(rawValue: rawValue) + } + + internal private(set) var rawValue: UInt + internal static var allZeros: ConstraintAttributes { return 0 } + internal static func convertFromNilLiteral() -> ConstraintAttributes { return 0 } + internal var boolValue: Bool { return self.rawValue != 0 } + + internal func toRaw() -> UInt { return self.rawValue } + internal static func fromRaw(_ raw: UInt) -> ConstraintAttributes? { return self.init(raw) } + internal static func fromMask(_ raw: UInt) -> ConstraintAttributes { return self.init(raw) } + + // normal + + internal static let none: ConstraintAttributes = 0 + internal static let left: ConstraintAttributes = ConstraintAttributes(UInt(1) << 0) + internal static let top: ConstraintAttributes = ConstraintAttributes(UInt(1) << 1) + internal static let right: ConstraintAttributes = ConstraintAttributes(UInt(1) << 2) + internal static let bottom: ConstraintAttributes = ConstraintAttributes(UInt(1) << 3) + internal static let leading: ConstraintAttributes = ConstraintAttributes(UInt(1) << 4) + internal static let trailing: ConstraintAttributes = ConstraintAttributes(UInt(1) << 5) + internal static let width: ConstraintAttributes = ConstraintAttributes(UInt(1) << 6) + internal static let height: ConstraintAttributes = ConstraintAttributes(UInt(1) << 7) + internal static let centerX: ConstraintAttributes = ConstraintAttributes(UInt(1) << 8) + internal static let centerY: ConstraintAttributes = ConstraintAttributes(UInt(1) << 9) + internal static let lastBaseline: ConstraintAttributes = ConstraintAttributes(UInt(1) << 10) + + @available(iOS 8.0, OSX 10.11, *) + internal static let firstBaseline: ConstraintAttributes = ConstraintAttributes(UInt(1) << 11) + + @available(iOS 8.0, *) + internal static let leftMargin: ConstraintAttributes = ConstraintAttributes(UInt(1) << 12) + + @available(iOS 8.0, *) + internal static let rightMargin: ConstraintAttributes = ConstraintAttributes(UInt(1) << 13) + + @available(iOS 8.0, *) + internal static let topMargin: ConstraintAttributes = ConstraintAttributes(UInt(1) << 14) + + @available(iOS 8.0, *) + internal static let bottomMargin: ConstraintAttributes = ConstraintAttributes(UInt(1) << 15) + + @available(iOS 8.0, *) + internal static let leadingMargin: ConstraintAttributes = ConstraintAttributes(UInt(1) << 16) + + @available(iOS 8.0, *) + internal static let trailingMargin: ConstraintAttributes = ConstraintAttributes(UInt(1) << 17) + + @available(iOS 8.0, *) + internal static let centerXWithinMargins: ConstraintAttributes = ConstraintAttributes(UInt(1) << 18) + + @available(iOS 8.0, *) + internal static let centerYWithinMargins: ConstraintAttributes = ConstraintAttributes(UInt(1) << 19) + + // aggregates + + internal static let edges: ConstraintAttributes = [.horizontalEdges, .verticalEdges] + internal static let horizontalEdges: ConstraintAttributes = [.left, .right] + internal static let verticalEdges: ConstraintAttributes = [.top, .bottom] + internal static let directionalEdges: ConstraintAttributes = [.directionalHorizontalEdges, .directionalVerticalEdges] + internal static let directionalHorizontalEdges: ConstraintAttributes = [.leading, .trailing] + internal static let directionalVerticalEdges: ConstraintAttributes = [.top, .bottom] + internal static let size: ConstraintAttributes = [.width, .height] + internal static let center: ConstraintAttributes = [.centerX, .centerY] + + @available(iOS 8.0, *) + internal static let margins: ConstraintAttributes = [.leftMargin, .topMargin, .rightMargin, .bottomMargin] + + @available(iOS 8.0, *) + internal static let directionalMargins: ConstraintAttributes = [.leadingMargin, .topMargin, .trailingMargin, .bottomMargin] + + @available(iOS 8.0, *) + internal static let centerWithinMargins: ConstraintAttributes = [.centerXWithinMargins, .centerYWithinMargins] + + internal var layoutAttributes:[LayoutAttribute] { + var attrs = [LayoutAttribute]() + if (self.contains(ConstraintAttributes.left)) { + attrs.append(.left) + } + if (self.contains(ConstraintAttributes.top)) { + attrs.append(.top) + } + if (self.contains(ConstraintAttributes.right)) { + attrs.append(.right) + } + if (self.contains(ConstraintAttributes.bottom)) { + attrs.append(.bottom) + } + if (self.contains(ConstraintAttributes.leading)) { + attrs.append(.leading) + } + if (self.contains(ConstraintAttributes.trailing)) { + attrs.append(.trailing) + } + if (self.contains(ConstraintAttributes.width)) { + attrs.append(.width) + } + if (self.contains(ConstraintAttributes.height)) { + attrs.append(.height) + } + if (self.contains(ConstraintAttributes.centerX)) { + attrs.append(.centerX) + } + if (self.contains(ConstraintAttributes.centerY)) { + attrs.append(.centerY) + } + if (self.contains(ConstraintAttributes.lastBaseline)) { + attrs.append(.lastBaseline) + } + + #if os(iOS) || os(tvOS) + if (self.contains(ConstraintAttributes.firstBaseline)) { + attrs.append(.firstBaseline) + } + if (self.contains(ConstraintAttributes.leftMargin)) { + attrs.append(.leftMargin) + } + if (self.contains(ConstraintAttributes.rightMargin)) { + attrs.append(.rightMargin) + } + if (self.contains(ConstraintAttributes.topMargin)) { + attrs.append(.topMargin) + } + if (self.contains(ConstraintAttributes.bottomMargin)) { + attrs.append(.bottomMargin) + } + if (self.contains(ConstraintAttributes.leadingMargin)) { + attrs.append(.leadingMargin) + } + if (self.contains(ConstraintAttributes.trailingMargin)) { + attrs.append(.trailingMargin) + } + if (self.contains(ConstraintAttributes.centerXWithinMargins)) { + attrs.append(.centerXWithinMargins) + } + if (self.contains(ConstraintAttributes.centerYWithinMargins)) { + attrs.append(.centerYWithinMargins) + } + #endif + + return attrs + } +} + +internal func + (left: ConstraintAttributes, right: ConstraintAttributes) -> ConstraintAttributes { + return left.union(right) +} + +internal func +=(left: inout ConstraintAttributes, right: ConstraintAttributes) { + left.formUnion(right) +} + +internal func -=(left: inout ConstraintAttributes, right: ConstraintAttributes) { + left.subtract(right) +} + +internal func ==(left: ConstraintAttributes, right: ConstraintAttributes) -> Bool { + return left.rawValue == right.rawValue +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintConfig.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintConfig.swift new file mode 100644 index 0000000..2746b7d --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintConfig.swift @@ -0,0 +1,37 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit + public typealias ConstraintInterfaceLayoutDirection = UIUserInterfaceLayoutDirection +#else + import AppKit + public typealias ConstraintInterfaceLayoutDirection = NSUserInterfaceLayoutDirection +#endif + + +public struct ConstraintConfig { + + public static var interfaceLayoutDirection: ConstraintInterfaceLayoutDirection = .leftToRight + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintConstantTarget.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintConstantTarget.swift new file mode 100644 index 0000000..7f54907 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintConstantTarget.swift @@ -0,0 +1,213 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public protocol ConstraintConstantTarget { +} + +extension CGPoint: ConstraintConstantTarget { +} + +extension CGSize: ConstraintConstantTarget { +} + +extension ConstraintInsets: ConstraintConstantTarget { +} + +#if os(iOS) || os(tvOS) +@available(iOS 11.0, tvOS 11.0, *) +extension ConstraintDirectionalInsets: ConstraintConstantTarget { +} +#endif + +extension ConstraintConstantTarget { + + internal func constraintConstantTargetValueFor(layoutAttribute: LayoutAttribute) -> CGFloat { + if let value = self as? CGFloat { + return value + } + + if let value = self as? Float { + return CGFloat(value) + } + + if let value = self as? Double { + return CGFloat(value) + } + + if let value = self as? Int { + return CGFloat(value) + } + + if let value = self as? UInt { + return CGFloat(value) + } + + if let value = self as? CGSize { + if layoutAttribute == .width { + return value.width + } else if layoutAttribute == .height { + return value.height + } else { + return 0.0 + } + } + + if let value = self as? CGPoint { + #if os(iOS) || os(tvOS) + switch layoutAttribute { + case .left, .right, .leading, .trailing, .centerX, .leftMargin, .rightMargin, .leadingMargin, .trailingMargin, .centerXWithinMargins: + return value.x + case .top, .bottom, .centerY, .topMargin, .bottomMargin, .centerYWithinMargins, .lastBaseline, .firstBaseline: + return value.y + case .width, .height, .notAnAttribute: + return 0.0 + #if swift(>=5.0) + @unknown default: + return 0.0 + #endif + } + #else + switch layoutAttribute { + case .left, .right, .leading, .trailing, .centerX: + return value.x + case .top, .bottom, .centerY, .lastBaseline, .firstBaseline: + return value.y + case .width, .height, .notAnAttribute: + return 0.0 + #if swift(>=5.0) + @unknown default: + return 0.0 + #endif + } + #endif + } + + if let value = self as? ConstraintInsets { + #if os(iOS) || os(tvOS) + switch layoutAttribute { + case .left, .leftMargin: + return value.left + case .top, .topMargin, .firstBaseline: + return value.top + case .right, .rightMargin: + return -value.right + case .bottom, .bottomMargin, .lastBaseline: + return -value.bottom + case .leading, .leadingMargin: + return (ConstraintConfig.interfaceLayoutDirection == .leftToRight) ? value.left : value.right + case .trailing, .trailingMargin: + return (ConstraintConfig.interfaceLayoutDirection == .leftToRight) ? -value.right : -value.left + case .centerX, .centerXWithinMargins: + return (value.left - value.right) / 2 + case .centerY, .centerYWithinMargins: + return (value.top - value.bottom) / 2 + case .width: + return -(value.left + value.right) + case .height: + return -(value.top + value.bottom) + case .notAnAttribute: + return 0.0 + #if swift(>=5.0) + @unknown default: + return 0.0 + #endif + } + #else + switch layoutAttribute { + case .left: + return value.left + case .top, .firstBaseline: + return value.top + case .right: + return -value.right + case .bottom, .lastBaseline: + return -value.bottom + case .leading: + return (ConstraintConfig.interfaceLayoutDirection == .leftToRight) ? value.left : value.right + case .trailing: + return (ConstraintConfig.interfaceLayoutDirection == .leftToRight) ? -value.right : -value.left + case .centerX: + return (value.left - value.right) / 2 + case .centerY: + return (value.top - value.bottom) / 2 + case .width: + return -(value.left + value.right) + case .height: + return -(value.top + value.bottom) + case .notAnAttribute: + return 0.0 + #if swift(>=5.0) + @unknown default: + return 0.0 + #endif + } + #endif + } + + #if os(iOS) || os(tvOS) + if #available(iOS 11.0, tvOS 11.0, *), let value = self as? ConstraintDirectionalInsets { + switch layoutAttribute { + case .left, .leftMargin: + return (ConstraintConfig.interfaceLayoutDirection == .leftToRight) ? value.leading : value.trailing + case .top, .topMargin, .firstBaseline: + return value.top + case .right, .rightMargin: + return (ConstraintConfig.interfaceLayoutDirection == .leftToRight) ? -value.trailing : -value.leading + case .bottom, .bottomMargin, .lastBaseline: + return -value.bottom + case .leading, .leadingMargin: + return value.leading + case .trailing, .trailingMargin: + return -value.trailing + case .centerX, .centerXWithinMargins: + return (value.leading - value.trailing) / 2 + case .centerY, .centerYWithinMargins: + return (value.top - value.bottom) / 2 + case .width: + return -(value.leading + value.trailing) + case .height: + return -(value.top + value.bottom) + case .notAnAttribute: + return 0.0 + #if swift(>=5.0) + @unknown default: + return 0.0 + #else + default: + return 0.0 + #endif + } + } + #endif + + return 0.0 + } + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDSL.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDSL.swift new file mode 100644 index 0000000..20f153d --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDSL.swift @@ -0,0 +1,209 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public protocol ConstraintDSL { + + var target: AnyObject? { get } + + func setLabel(_ value: String?) + func label() -> String? + +} +extension ConstraintDSL { + + public func setLabel(_ value: String?) { + objc_setAssociatedObject(self.target as Any, &labelKey, value, .OBJC_ASSOCIATION_COPY_NONATOMIC) + } + public func label() -> String? { + return objc_getAssociatedObject(self.target as Any, &labelKey) as? String + } + +} +private var labelKey: UInt8 = 0 + + +public protocol ConstraintBasicAttributesDSL : ConstraintDSL { +} +extension ConstraintBasicAttributesDSL { + + // MARK: Basics + + public var left: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.left) + } + + public var top: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.top) + } + + public var right: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.right) + } + + public var bottom: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.bottom) + } + + public var leading: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.leading) + } + + public var trailing: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.trailing) + } + + public var width: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.width) + } + + public var height: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.height) + } + + public var centerX: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.centerX) + } + + public var centerY: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.centerY) + } + + public var edges: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.edges) + } + + public var directionalEdges: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.directionalEdges) + } + + public var horizontalEdges: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.horizontalEdges) + } + + public var verticalEdges: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.verticalEdges) + } + + public var directionalHorizontalEdges: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.directionalHorizontalEdges) + } + + public var directionalVerticalEdges: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.directionalVerticalEdges) + } + + public var size: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.size) + } + + public var center: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.center) + } + +} + +public protocol ConstraintAttributesDSL : ConstraintBasicAttributesDSL { +} +extension ConstraintAttributesDSL { + + // MARK: Baselines + @available(*, deprecated, renamed:"lastBaseline") + public var baseline: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.lastBaseline) + } + + @available(iOS 8.0, OSX 10.11, *) + public var lastBaseline: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.lastBaseline) + } + + @available(iOS 8.0, OSX 10.11, *) + public var firstBaseline: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.firstBaseline) + } + + // MARK: Margins + + @available(iOS 8.0, *) + public var leftMargin: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.leftMargin) + } + + @available(iOS 8.0, *) + public var topMargin: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.topMargin) + } + + @available(iOS 8.0, *) + public var rightMargin: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.rightMargin) + } + + @available(iOS 8.0, *) + public var bottomMargin: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.bottomMargin) + } + + @available(iOS 8.0, *) + public var leadingMargin: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.leadingMargin) + } + + @available(iOS 8.0, *) + public var trailingMargin: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.trailingMargin) + } + + @available(iOS 8.0, *) + public var centerXWithinMargins: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.centerXWithinMargins) + } + + @available(iOS 8.0, *) + public var centerYWithinMargins: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.centerYWithinMargins) + } + + @available(iOS 8.0, *) + public var margins: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.margins) + } + + @available(iOS 8.0, *) + public var directionalMargins: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.directionalMargins) + } + + @available(iOS 8.0, *) + public var centerWithinMargins: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.centerWithinMargins) + } + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDescription.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDescription.swift new file mode 100644 index 0000000..3521f9f --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDescription.swift @@ -0,0 +1,69 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public class ConstraintDescription { + + internal let item: LayoutConstraintItem + internal var attributes: ConstraintAttributes + internal var relation: ConstraintRelation? = nil + internal var sourceLocation: (String, UInt)? = nil + internal var label: String? = nil + internal var related: ConstraintItem? = nil + internal var multiplier: ConstraintMultiplierTarget = 1.0 + internal var constant: ConstraintConstantTarget = 0.0 + internal var priority: ConstraintPriorityTarget = 1000.0 + internal lazy var constraint: Constraint? = { + guard let relation = self.relation, + let related = self.related, + let sourceLocation = self.sourceLocation else { + return nil + } + let from = ConstraintItem(target: self.item, attributes: self.attributes) + + return Constraint( + from: from, + to: related, + relation: relation, + sourceLocation: sourceLocation, + label: self.label, + multiplier: self.multiplier, + constant: self.constant, + priority: self.priority + ) + }() + + // MARK: Initialization + + internal init(item: LayoutConstraintItem, attributes: ConstraintAttributes) { + self.item = item + self.attributes = attributes + } + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDirectionalInsetTarget.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDirectionalInsetTarget.swift new file mode 100644 index 0000000..955aec3 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDirectionalInsetTarget.swift @@ -0,0 +1,49 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) +import UIKit +#else +import AppKit +#endif + +#if os(iOS) || os(tvOS) +public protocol ConstraintDirectionalInsetTarget: ConstraintConstantTarget { +} + +@available(iOS 11.0, tvOS 11.0, *) +extension ConstraintDirectionalInsets: ConstraintDirectionalInsetTarget { +} + +extension ConstraintDirectionalInsetTarget { + + @available(iOS 11.0, tvOS 11.0, *) + internal var constraintDirectionalInsetTargetValue: ConstraintDirectionalInsets { + if let amount = self as? ConstraintDirectionalInsets { + return amount + } else { + return ConstraintDirectionalInsets(top: 0, leading: 0, bottom: 0, trailing: 0) + } + } +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDirectionalInsets.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDirectionalInsets.swift new file mode 100644 index 0000000..ada8ed5 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintDirectionalInsets.swift @@ -0,0 +1,34 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +#if os(iOS) || os(tvOS) + @available(iOS 11.0, tvOS 11.0, *) + public typealias ConstraintDirectionalInsets = NSDirectionalEdgeInsets +#endif diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintInsetTarget.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintInsetTarget.swift new file mode 100644 index 0000000..ba8a0f3 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintInsetTarget.swift @@ -0,0 +1,72 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public protocol ConstraintInsetTarget: ConstraintConstantTarget { +} + +extension Int: ConstraintInsetTarget { +} + +extension UInt: ConstraintInsetTarget { +} + +extension Float: ConstraintInsetTarget { +} + +extension Double: ConstraintInsetTarget { +} + +extension CGFloat: ConstraintInsetTarget { +} + +extension ConstraintInsets: ConstraintInsetTarget { +} + +extension ConstraintInsetTarget { + + internal var constraintInsetTargetValue: ConstraintInsets { + if let amount = self as? ConstraintInsets { + return amount + } else if let amount = self as? Float { + return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount)) + } else if let amount = self as? Double { + return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount)) + } else if let amount = self as? CGFloat { + return ConstraintInsets(top: amount, left: amount, bottom: amount, right: amount) + } else if let amount = self as? Int { + return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount)) + } else if let amount = self as? UInt { + return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount)) + } else { + return ConstraintInsets(top: 0, left: 0, bottom: 0, right: 0) + } + } + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintInsets.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintInsets.swift new file mode 100644 index 0000000..738ca05 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintInsets.swift @@ -0,0 +1,35 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +#if os(iOS) || os(tvOS) + public typealias ConstraintInsets = UIEdgeInsets +#else + public typealias ConstraintInsets = NSEdgeInsets +#endif diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintItem.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintItem.swift new file mode 100644 index 0000000..a342c1d --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintItem.swift @@ -0,0 +1,61 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public final class ConstraintItem { + + internal weak var target: AnyObject? + internal let attributes: ConstraintAttributes + + internal init(target: AnyObject?, attributes: ConstraintAttributes) { + self.target = target + self.attributes = attributes + } + + internal var layoutConstraintItem: LayoutConstraintItem? { + return self.target as? LayoutConstraintItem + } + +} + +public func ==(lhs: ConstraintItem, rhs: ConstraintItem) -> Bool { + // pointer equality + guard lhs !== rhs else { + return true + } + + // must both have valid targets and identical attributes + guard let target1 = lhs.target, + let target2 = rhs.target, + target1 === target2 && lhs.attributes == rhs.attributes else { + return false + } + + return true +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutGuide+Extensions.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutGuide+Extensions.swift new file mode 100644 index 0000000..d429e0c --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutGuide+Extensions.swift @@ -0,0 +1,36 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#endif + + +@available(iOS 9.0, OSX 10.11, *) +public extension ConstraintLayoutGuide { + + var snp: ConstraintLayoutGuideDSL { + return ConstraintLayoutGuideDSL(guide: self) + } + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutGuide.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutGuide.swift new file mode 100644 index 0000000..e3e50c8 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutGuide.swift @@ -0,0 +1,37 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +#if os(iOS) || os(tvOS) + @available(iOS 9.0, *) + public typealias ConstraintLayoutGuide = UILayoutGuide +#else + @available(OSX 10.11, *) + public typealias ConstraintLayoutGuide = NSLayoutGuide +#endif diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutGuideDSL.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutGuideDSL.swift new file mode 100644 index 0000000..0007819 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutGuideDSL.swift @@ -0,0 +1,66 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +@available(iOS 9.0, OSX 10.11, *) +public struct ConstraintLayoutGuideDSL: ConstraintAttributesDSL { + + @discardableResult + public func prepareConstraints(_ closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] { + return ConstraintMaker.prepareConstraints(item: self.guide, closure: closure) + } + + public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) { + ConstraintMaker.makeConstraints(item: self.guide, closure: closure) + } + + public func remakeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) { + ConstraintMaker.remakeConstraints(item: self.guide, closure: closure) + } + + public func updateConstraints(_ closure: (_ make: ConstraintMaker) -> Void) { + ConstraintMaker.updateConstraints(item: self.guide, closure: closure) + } + + public func removeConstraints() { + ConstraintMaker.removeConstraints(item: self.guide) + } + + public var target: AnyObject? { + return self.guide + } + + internal let guide: ConstraintLayoutGuide + + internal init(guide: ConstraintLayoutGuide) { + self.guide = guide + + } + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutSupport.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutSupport.swift new file mode 100644 index 0000000..e92e9fb --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutSupport.swift @@ -0,0 +1,36 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +#if os(iOS) || os(tvOS) + @available(iOS 8.0, *) + public typealias ConstraintLayoutSupport = UILayoutSupport +#else + public class ConstraintLayoutSupport {} +#endif diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutSupportDSL.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutSupportDSL.swift new file mode 100644 index 0000000..5d6ae89 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintLayoutSupportDSL.swift @@ -0,0 +1,56 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +@available(iOS 8.0, *) +public struct ConstraintLayoutSupportDSL: ConstraintDSL { + + public var target: AnyObject? { + return self.support + } + + internal let support: ConstraintLayoutSupport + + internal init(support: ConstraintLayoutSupport) { + self.support = support + + } + + public var top: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.top) + } + + public var bottom: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.bottom) + } + + public var height: ConstraintItem { + return ConstraintItem(target: self.target, attributes: ConstraintAttributes.height) + } +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMaker.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMaker.swift new file mode 100644 index 0000000..50d7402 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMaker.swift @@ -0,0 +1,224 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + +public class ConstraintMaker { + + public var left: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.left) + } + + public var top: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.top) + } + + public var bottom: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.bottom) + } + + public var right: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.right) + } + + public var leading: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.leading) + } + + public var trailing: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.trailing) + } + + public var width: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.width) + } + + public var height: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.height) + } + + public var centerX: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.centerX) + } + + public var centerY: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.centerY) + } + + @available(*, deprecated, renamed:"lastBaseline") + public var baseline: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.lastBaseline) + } + + public var lastBaseline: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.lastBaseline) + } + + @available(iOS 8.0, OSX 10.11, *) + public var firstBaseline: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.firstBaseline) + } + + @available(iOS 8.0, *) + public var leftMargin: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.leftMargin) + } + + @available(iOS 8.0, *) + public var rightMargin: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.rightMargin) + } + + @available(iOS 8.0, *) + public var topMargin: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.topMargin) + } + + @available(iOS 8.0, *) + public var bottomMargin: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.bottomMargin) + } + + @available(iOS 8.0, *) + public var leadingMargin: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.leadingMargin) + } + + @available(iOS 8.0, *) + public var trailingMargin: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.trailingMargin) + } + + @available(iOS 8.0, *) + public var centerXWithinMargins: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.centerXWithinMargins) + } + + @available(iOS 8.0, *) + public var centerYWithinMargins: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.centerYWithinMargins) + } + + public var edges: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.edges) + } + public var horizontalEdges: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.horizontalEdges) + } + public var verticalEdges: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.verticalEdges) + } + public var directionalEdges: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.directionalEdges) + } + public var directionalHorizontalEdges: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.directionalHorizontalEdges) + } + public var directionalVerticalEdges: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.directionalVerticalEdges) + } + public var size: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.size) + } + public var center: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.center) + } + + @available(iOS 8.0, *) + public var margins: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.margins) + } + + @available(iOS 8.0, *) + public var directionalMargins: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.directionalMargins) + } + + @available(iOS 8.0, *) + public var centerWithinMargins: ConstraintMakerExtendable { + return self.makeExtendableWithAttributes(.centerWithinMargins) + } + + public let item: LayoutConstraintItem + private var descriptions = [ConstraintDescription]() + + internal init(item: LayoutConstraintItem) { + self.item = item + self.item.prepare() + } + + internal func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable { + let description = ConstraintDescription(item: self.item, attributes: attributes) + self.descriptions.append(description) + return ConstraintMakerExtendable(description) + } + + internal static func prepareConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] { + let maker = ConstraintMaker(item: item) + closure(maker) + var constraints: [Constraint] = [] + for description in maker.descriptions { + guard let constraint = description.constraint else { + continue + } + constraints.append(constraint) + } + return constraints + } + + internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) { + let constraints = prepareConstraints(item: item, closure: closure) + for constraint in constraints { + constraint.activateIfNeeded(updatingExisting: false) + } + } + + internal static func remakeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) { + self.removeConstraints(item: item) + self.makeConstraints(item: item, closure: closure) + } + + internal static func updateConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) { + guard item.constraints.count > 0 else { + self.makeConstraints(item: item, closure: closure) + return + } + + let constraints = prepareConstraints(item: item, closure: closure) + for constraint in constraints { + constraint.activateIfNeeded(updatingExisting: true) + } + } + + internal static func removeConstraints(item: LayoutConstraintItem) { + let constraints = item.constraints + for constraint in constraints { + constraint.deactivateIfNeeded() + } + } + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerEditable.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerEditable.swift new file mode 100644 index 0000000..4869bc2 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerEditable.swift @@ -0,0 +1,64 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public class ConstraintMakerEditable: ConstraintMakerPrioritizable { + + @discardableResult + public func multipliedBy(_ amount: ConstraintMultiplierTarget) -> ConstraintMakerEditable { + self.description.multiplier = amount + return self + } + + @discardableResult + public func dividedBy(_ amount: ConstraintMultiplierTarget) -> ConstraintMakerEditable { + return self.multipliedBy(1.0 / amount.constraintMultiplierTargetValue) + } + + @discardableResult + public func offset(_ amount: ConstraintOffsetTarget) -> ConstraintMakerEditable { + self.description.constant = amount.constraintOffsetTargetValue + return self + } + + @discardableResult + public func inset(_ amount: ConstraintInsetTarget) -> ConstraintMakerEditable { + self.description.constant = amount.constraintInsetTargetValue + return self + } + + #if os(iOS) || os(tvOS) + @discardableResult + @available(iOS 11.0, tvOS 11.0, *) + public func inset(_ amount: ConstraintDirectionalInsetTarget) -> ConstraintMakerEditable { + self.description.constant = amount.constraintDirectionalInsetTargetValue + return self + } + #endif +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerExtendable.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerExtendable.swift new file mode 100644 index 0000000..d834649 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerExtendable.swift @@ -0,0 +1,195 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public class ConstraintMakerExtendable: ConstraintMakerRelatable { + + public var left: ConstraintMakerExtendable { + self.description.attributes += .left + return self + } + + public var top: ConstraintMakerExtendable { + self.description.attributes += .top + return self + } + + public var bottom: ConstraintMakerExtendable { + self.description.attributes += .bottom + return self + } + + public var right: ConstraintMakerExtendable { + self.description.attributes += .right + return self + } + + public var leading: ConstraintMakerExtendable { + self.description.attributes += .leading + return self + } + + public var trailing: ConstraintMakerExtendable { + self.description.attributes += .trailing + return self + } + + public var width: ConstraintMakerExtendable { + self.description.attributes += .width + return self + } + + public var height: ConstraintMakerExtendable { + self.description.attributes += .height + return self + } + + public var centerX: ConstraintMakerExtendable { + self.description.attributes += .centerX + return self + } + + public var centerY: ConstraintMakerExtendable { + self.description.attributes += .centerY + return self + } + + @available(*, deprecated, renamed:"lastBaseline") + public var baseline: ConstraintMakerExtendable { + self.description.attributes += .lastBaseline + return self + } + + public var lastBaseline: ConstraintMakerExtendable { + self.description.attributes += .lastBaseline + return self + } + + @available(iOS 8.0, OSX 10.11, *) + public var firstBaseline: ConstraintMakerExtendable { + self.description.attributes += .firstBaseline + return self + } + + @available(iOS 8.0, *) + public var leftMargin: ConstraintMakerExtendable { + self.description.attributes += .leftMargin + return self + } + + @available(iOS 8.0, *) + public var rightMargin: ConstraintMakerExtendable { + self.description.attributes += .rightMargin + return self + } + + @available(iOS 8.0, *) + public var topMargin: ConstraintMakerExtendable { + self.description.attributes += .topMargin + return self + } + + @available(iOS 8.0, *) + public var bottomMargin: ConstraintMakerExtendable { + self.description.attributes += .bottomMargin + return self + } + + @available(iOS 8.0, *) + public var leadingMargin: ConstraintMakerExtendable { + self.description.attributes += .leadingMargin + return self + } + + @available(iOS 8.0, *) + public var trailingMargin: ConstraintMakerExtendable { + self.description.attributes += .trailingMargin + return self + } + + @available(iOS 8.0, *) + public var centerXWithinMargins: ConstraintMakerExtendable { + self.description.attributes += .centerXWithinMargins + return self + } + + @available(iOS 8.0, *) + public var centerYWithinMargins: ConstraintMakerExtendable { + self.description.attributes += .centerYWithinMargins + return self + } + + public var edges: ConstraintMakerExtendable { + self.description.attributes += .edges + return self + } + public var horizontalEdges: ConstraintMakerExtendable { + self.description.attributes += .horizontalEdges + return self + } + public var verticalEdges: ConstraintMakerExtendable { + self.description.attributes += .verticalEdges + return self + } + public var directionalEdges: ConstraintMakerExtendable { + self.description.attributes += .directionalEdges + return self + } + public var directionalHorizontalEdges: ConstraintMakerExtendable { + self.description.attributes += .directionalHorizontalEdges + return self + } + public var directionalVerticalEdges: ConstraintMakerExtendable { + self.description.attributes += .directionalVerticalEdges + return self + } + public var size: ConstraintMakerExtendable { + self.description.attributes += .size + return self + } + + @available(iOS 8.0, *) + public var margins: ConstraintMakerExtendable { + self.description.attributes += .margins + return self + } + + @available(iOS 8.0, *) + public var directionalMargins: ConstraintMakerExtendable { + self.description.attributes += .directionalMargins + return self + } + + @available(iOS 8.0, *) + public var centerWithinMargins: ConstraintMakerExtendable { + self.description.attributes += .centerWithinMargins + return self + } + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerFinalizable.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerFinalizable.swift new file mode 100644 index 0000000..4e1379e --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerFinalizable.swift @@ -0,0 +1,49 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public class ConstraintMakerFinalizable { + + internal let description: ConstraintDescription + + internal init(_ description: ConstraintDescription) { + self.description = description + } + + @discardableResult + public func labeled(_ label: String) -> ConstraintMakerFinalizable { + self.description.label = label + return self + } + + public var constraint: Constraint { + return self.description.constraint! + } + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerPrioritizable.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerPrioritizable.swift new file mode 100644 index 0000000..5af5303 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerPrioritizable.swift @@ -0,0 +1,70 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + +@available(*, deprecated, message:"Use ConstraintMakerPrioritizable instead.") +public typealias ConstraintMakerPriortizable = ConstraintMakerPrioritizable + +public class ConstraintMakerPrioritizable: ConstraintMakerFinalizable { + + @discardableResult + public func priority(_ amount: ConstraintPriority) -> ConstraintMakerFinalizable { + self.description.priority = amount.value + return self + } + + @discardableResult + public func priority(_ amount: ConstraintPriorityTarget) -> ConstraintMakerFinalizable { + self.description.priority = amount + return self + } + + @available(*, deprecated, message:"Use priority(.required) instead.") + @discardableResult + public func priorityRequired() -> ConstraintMakerFinalizable { + return self.priority(.required) + } + + @available(*, deprecated, message:"Use priority(.high) instead.") + @discardableResult + public func priorityHigh() -> ConstraintMakerFinalizable { + return self.priority(.high) + } + + @available(*, deprecated, message:"Use priority(.medium) instead.") + @discardableResult + public func priorityMedium() -> ConstraintMakerFinalizable { + return self.priority(.medium) + } + + @available(*, deprecated, message:"Use priority(.low) instead.") + @discardableResult + public func priorityLow() -> ConstraintMakerFinalizable { + return self.priority(.low) + } +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerRelatable+Extensions.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerRelatable+Extensions.swift new file mode 100644 index 0000000..63100d7 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerRelatable+Extensions.swift @@ -0,0 +1,57 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +extension ConstraintMakerRelatable { + + @discardableResult + public func equalToSuperview(_ closure: (ConstraintView) -> T, _ file: String = #file, line: UInt = #line) -> ConstraintMakerEditable { + guard let other = self.description.item.superview else { + fatalError("Expected superview but found nil when attempting make constraint `equalToSuperview`.") + } + return self.relatedTo(closure(other), relation: .equal, file: file, line: line) + } + + @discardableResult + public func lessThanOrEqualToSuperview(_ closure: (ConstraintView) -> T, _ file: String = #file, line: UInt = #line) -> ConstraintMakerEditable { + guard let other = self.description.item.superview else { + fatalError("Expected superview but found nil when attempting make constraint `lessThanOrEqualToSuperview`.") + } + return self.relatedTo(closure(other), relation: .lessThanOrEqual, file: file, line: line) + } + + @discardableResult + public func greaterThanOrEqualTo(_ closure: (ConstraintView) -> T, _ file: String = #file, line: UInt = #line) -> ConstraintMakerEditable { + guard let other = self.description.item.superview else { + fatalError("Expected superview but found nil when attempting make constraint `greaterThanOrEqualToSuperview`.") + } + return self.relatedTo(closure(other), relation: .greaterThanOrEqual, file: file, line: line) + } + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerRelatable.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerRelatable.swift new file mode 100644 index 0000000..7889532 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMakerRelatable.swift @@ -0,0 +1,115 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public class ConstraintMakerRelatable { + + internal let description: ConstraintDescription + + internal init(_ description: ConstraintDescription) { + self.description = description + } + + internal func relatedTo(_ other: ConstraintRelatableTarget, relation: ConstraintRelation, file: String, line: UInt) -> ConstraintMakerEditable { + let related: ConstraintItem + let constant: ConstraintConstantTarget + + if let other = other as? ConstraintItem { + guard other.attributes == ConstraintAttributes.none || + other.attributes.layoutAttributes.count <= 1 || + other.attributes.layoutAttributes == self.description.attributes.layoutAttributes || + other.attributes == .edges && self.description.attributes == .margins || + other.attributes == .margins && self.description.attributes == .edges || + other.attributes == .directionalEdges && self.description.attributes == .directionalMargins || + other.attributes == .directionalMargins && self.description.attributes == .directionalEdges else { + fatalError("Cannot constraint to multiple non identical attributes. (\(file), \(line))"); + } + + related = other + constant = 0.0 + } else if let other = other as? ConstraintView { + related = ConstraintItem(target: other, attributes: ConstraintAttributes.none) + constant = 0.0 + } else if let other = other as? ConstraintConstantTarget { + related = ConstraintItem(target: nil, attributes: ConstraintAttributes.none) + constant = other + } else if #available(iOS 9.0, OSX 10.11, *), let other = other as? ConstraintLayoutGuide { + related = ConstraintItem(target: other, attributes: ConstraintAttributes.none) + constant = 0.0 + } else { + fatalError("Invalid constraint. (\(file), \(line))") + } + + let editable = ConstraintMakerEditable(self.description) + editable.description.sourceLocation = (file, line) + editable.description.relation = relation + editable.description.related = related + editable.description.constant = constant + return editable + } + + @discardableResult + public func equalTo(_ other: ConstraintRelatableTarget, _ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable { + return self.relatedTo(other, relation: .equal, file: file, line: line) + } + + @discardableResult + public func equalToSuperview(_ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable { + guard let other = self.description.item.superview else { + fatalError("Expected superview but found nil when attempting make constraint `equalToSuperview`.") + } + return self.relatedTo(other, relation: .equal, file: file, line: line) + } + + @discardableResult + public func lessThanOrEqualTo(_ other: ConstraintRelatableTarget, _ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable { + return self.relatedTo(other, relation: .lessThanOrEqual, file: file, line: line) + } + + @discardableResult + public func lessThanOrEqualToSuperview(_ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable { + guard let other = self.description.item.superview else { + fatalError("Expected superview but found nil when attempting make constraint `lessThanOrEqualToSuperview`.") + } + return self.relatedTo(other, relation: .lessThanOrEqual, file: file, line: line) + } + + @discardableResult + public func greaterThanOrEqualTo(_ other: ConstraintRelatableTarget, _ file: String = #file, line: UInt = #line) -> ConstraintMakerEditable { + return self.relatedTo(other, relation: .greaterThanOrEqual, file: file, line: line) + } + + @discardableResult + public func greaterThanOrEqualToSuperview(_ file: String = #file, line: UInt = #line) -> ConstraintMakerEditable { + guard let other = self.description.item.superview else { + fatalError("Expected superview but found nil when attempting make constraint `greaterThanOrEqualToSuperview`.") + } + return self.relatedTo(other, relation: .greaterThanOrEqual, file: file, line: line) + } +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMultiplierTarget.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMultiplierTarget.swift new file mode 100644 index 0000000..6fecd33 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintMultiplierTarget.swift @@ -0,0 +1,75 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public protocol ConstraintMultiplierTarget { + + var constraintMultiplierTargetValue: CGFloat { get } + +} + +extension Int: ConstraintMultiplierTarget { + + public var constraintMultiplierTargetValue: CGFloat { + return CGFloat(self) + } + +} + +extension UInt: ConstraintMultiplierTarget { + + public var constraintMultiplierTargetValue: CGFloat { + return CGFloat(self) + } + +} + +extension Float: ConstraintMultiplierTarget { + + public var constraintMultiplierTargetValue: CGFloat { + return CGFloat(self) + } + +} + +extension Double: ConstraintMultiplierTarget { + + public var constraintMultiplierTargetValue: CGFloat { + return CGFloat(self) + } + +} + +extension CGFloat: ConstraintMultiplierTarget { + + public var constraintMultiplierTargetValue: CGFloat { + return self + } + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintOffsetTarget.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintOffsetTarget.swift new file mode 100644 index 0000000..bd9e0a1 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintOffsetTarget.swift @@ -0,0 +1,69 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public protocol ConstraintOffsetTarget: ConstraintConstantTarget { +} + +extension Int: ConstraintOffsetTarget { +} + +extension UInt: ConstraintOffsetTarget { +} + +extension Float: ConstraintOffsetTarget { +} + +extension Double: ConstraintOffsetTarget { +} + +extension CGFloat: ConstraintOffsetTarget { +} + +extension ConstraintOffsetTarget { + + internal var constraintOffsetTargetValue: CGFloat { + let offset: CGFloat + if let amount = self as? Float { + offset = CGFloat(amount) + } else if let amount = self as? Double { + offset = CGFloat(amount) + } else if let amount = self as? CGFloat { + offset = CGFloat(amount) + } else if let amount = self as? Int { + offset = CGFloat(amount) + } else if let amount = self as? UInt { + offset = CGFloat(amount) + } else { + offset = 0.0 + } + return offset + } + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintPriority.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintPriority.swift new file mode 100644 index 0000000..f9dab16 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintPriority.swift @@ -0,0 +1,77 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + +public struct ConstraintPriority : ExpressibleByFloatLiteral, Equatable, Strideable { + public typealias FloatLiteralType = Float + + public let value: Float + + public init(floatLiteral value: Float) { + self.value = value + } + + public init(_ value: Float) { + self.value = value + } + + public static var required: ConstraintPriority { + return 1000.0 + } + + public static var high: ConstraintPriority { + return 750.0 + } + + public static var medium: ConstraintPriority { + #if os(OSX) + return 501.0 + #else + return 500.0 + #endif + + } + + public static var low: ConstraintPriority { + return 250.0 + } + + public static func ==(lhs: ConstraintPriority, rhs: ConstraintPriority) -> Bool { + return lhs.value == rhs.value + } + + // MARK: Strideable + + public func advanced(by n: FloatLiteralType) -> ConstraintPriority { + return ConstraintPriority(floatLiteral: value + n) + } + + public func distance(to other: ConstraintPriority) -> FloatLiteralType { + return other.value - value + } +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintPriorityTarget.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintPriorityTarget.swift new file mode 100644 index 0000000..064f750 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintPriorityTarget.swift @@ -0,0 +1,85 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public protocol ConstraintPriorityTarget { + + var constraintPriorityTargetValue: Float { get } + +} + +extension Int: ConstraintPriorityTarget { + + public var constraintPriorityTargetValue: Float { + return Float(self) + } + +} + +extension UInt: ConstraintPriorityTarget { + + public var constraintPriorityTargetValue: Float { + return Float(self) + } + +} + +extension Float: ConstraintPriorityTarget { + + public var constraintPriorityTargetValue: Float { + return self + } + +} + +extension Double: ConstraintPriorityTarget { + + public var constraintPriorityTargetValue: Float { + return Float(self) + } + +} + +extension CGFloat: ConstraintPriorityTarget { + + public var constraintPriorityTargetValue: Float { + return Float(self) + } + +} + +#if os(iOS) || os(tvOS) +extension UILayoutPriority: ConstraintPriorityTarget { + + public var constraintPriorityTargetValue: Float { + return self.rawValue + } + +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintRelatableTarget.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintRelatableTarget.swift new file mode 100644 index 0000000..d517a61 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintRelatableTarget.swift @@ -0,0 +1,72 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public protocol ConstraintRelatableTarget { +} + +extension Int: ConstraintRelatableTarget { +} + +extension UInt: ConstraintRelatableTarget { +} + +extension Float: ConstraintRelatableTarget { +} + +extension Double: ConstraintRelatableTarget { +} + +extension CGFloat: ConstraintRelatableTarget { +} + +extension CGSize: ConstraintRelatableTarget { +} + +extension CGPoint: ConstraintRelatableTarget { +} + +extension ConstraintInsets: ConstraintRelatableTarget { +} + +#if os(iOS) || os(tvOS) +@available(iOS 11.0, tvOS 11.0, *) +extension ConstraintDirectionalInsets: ConstraintRelatableTarget { +} +#endif + +extension ConstraintItem: ConstraintRelatableTarget { +} + +extension ConstraintView: ConstraintRelatableTarget { +} + +@available(iOS 9.0, OSX 10.11, *) +extension ConstraintLayoutGuide: ConstraintRelatableTarget { +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintRelation.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintRelation.swift new file mode 100644 index 0000000..446aaf7 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintRelation.swift @@ -0,0 +1,48 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +internal enum ConstraintRelation : Int { + case equal = 1 + case lessThanOrEqual + case greaterThanOrEqual + + internal var layoutRelation: LayoutRelation { + get { + switch(self) { + case .equal: + return .equal + case .lessThanOrEqual: + return .lessThanOrEqual + case .greaterThanOrEqual: + return .greaterThanOrEqual + } + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintView+Extensions.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintView+Extensions.swift new file mode 100644 index 0000000..e0f71c1 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintView+Extensions.swift @@ -0,0 +1,152 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public extension ConstraintView { + + @available(*, deprecated, renamed:"snp.left") + var snp_left: ConstraintItem { return self.snp.left } + + @available(*, deprecated, renamed:"snp.top") + var snp_top: ConstraintItem { return self.snp.top } + + @available(*, deprecated, renamed:"snp.right") + var snp_right: ConstraintItem { return self.snp.right } + + @available(*, deprecated, renamed:"snp.bottom") + var snp_bottom: ConstraintItem { return self.snp.bottom } + + @available(*, deprecated, renamed:"snp.leading") + var snp_leading: ConstraintItem { return self.snp.leading } + + @available(*, deprecated, renamed:"snp.trailing") + var snp_trailing: ConstraintItem { return self.snp.trailing } + + @available(*, deprecated, renamed:"snp.width") + var snp_width: ConstraintItem { return self.snp.width } + + @available(*, deprecated, renamed:"snp.height") + var snp_height: ConstraintItem { return self.snp.height } + + @available(*, deprecated, renamed:"snp.centerX") + var snp_centerX: ConstraintItem { return self.snp.centerX } + + @available(*, deprecated, renamed:"snp.centerY") + var snp_centerY: ConstraintItem { return self.snp.centerY } + + @available(*, deprecated, renamed:"snp.baseline") + var snp_baseline: ConstraintItem { return self.snp.baseline } + + @available(*, deprecated, renamed:"snp.lastBaseline") + @available(iOS 8.0, OSX 10.11, *) + var snp_lastBaseline: ConstraintItem { return self.snp.lastBaseline } + + @available(iOS, deprecated, renamed:"snp.firstBaseline") + @available(iOS 8.0, OSX 10.11, *) + var snp_firstBaseline: ConstraintItem { return self.snp.firstBaseline } + + @available(iOS, deprecated, renamed:"snp.leftMargin") + @available(iOS 8.0, *) + var snp_leftMargin: ConstraintItem { return self.snp.leftMargin } + + @available(iOS, deprecated, renamed:"snp.topMargin") + @available(iOS 8.0, *) + var snp_topMargin: ConstraintItem { return self.snp.topMargin } + + @available(iOS, deprecated, renamed:"snp.rightMargin") + @available(iOS 8.0, *) + var snp_rightMargin: ConstraintItem { return self.snp.rightMargin } + + @available(iOS, deprecated, renamed:"snp.bottomMargin") + @available(iOS 8.0, *) + var snp_bottomMargin: ConstraintItem { return self.snp.bottomMargin } + + @available(iOS, deprecated, renamed:"snp.leadingMargin") + @available(iOS 8.0, *) + var snp_leadingMargin: ConstraintItem { return self.snp.leadingMargin } + + @available(iOS, deprecated, renamed:"snp.trailingMargin") + @available(iOS 8.0, *) + var snp_trailingMargin: ConstraintItem { return self.snp.trailingMargin } + + @available(iOS, deprecated, renamed:"snp.centerXWithinMargins") + @available(iOS 8.0, *) + var snp_centerXWithinMargins: ConstraintItem { return self.snp.centerXWithinMargins } + + @available(iOS, deprecated, renamed:"snp.centerYWithinMargins") + @available(iOS 8.0, *) + var snp_centerYWithinMargins: ConstraintItem { return self.snp.centerYWithinMargins } + + @available(*, deprecated, renamed:"snp.edges") + var snp_edges: ConstraintItem { return self.snp.edges } + + @available(*, deprecated, renamed:"snp.size") + var snp_size: ConstraintItem { return self.snp.size } + + @available(*, deprecated, renamed:"snp.center") + var snp_center: ConstraintItem { return self.snp.center } + + @available(iOS, deprecated, renamed:"snp.margins") + @available(iOS 8.0, *) + var snp_margins: ConstraintItem { return self.snp.margins } + + @available(iOS, deprecated, renamed:"snp.centerWithinMargins") + @available(iOS 8.0, *) + var snp_centerWithinMargins: ConstraintItem { return self.snp.centerWithinMargins } + + @available(*, deprecated, renamed:"snp.prepareConstraints(_:)") + func snp_prepareConstraints(_ closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] { + return self.snp.prepareConstraints(closure) + } + + @available(*, deprecated, renamed:"snp.makeConstraints(_:)") + func snp_makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) { + self.snp.makeConstraints(closure) + } + + @available(*, deprecated, renamed:"snp.remakeConstraints(_:)") + func snp_remakeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) { + self.snp.remakeConstraints(closure) + } + + @available(*, deprecated, renamed:"snp.updateConstraints(_:)") + func snp_updateConstraints(_ closure: (_ make: ConstraintMaker) -> Void) { + self.snp.updateConstraints(closure) + } + + @available(*, deprecated, renamed:"snp.removeConstraints()") + func snp_removeConstraints() { + self.snp.removeConstraints() + } + + var snp: ConstraintViewDSL { + return ConstraintViewDSL(view: self) + } + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintView.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintView.swift new file mode 100644 index 0000000..6ff8a76 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintView.swift @@ -0,0 +1,35 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +#if os(iOS) || os(tvOS) + public typealias ConstraintView = UIView +#else + public typealias ConstraintView = NSView +#endif diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintViewDSL.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintViewDSL.swift new file mode 100644 index 0000000..a0187f9 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/ConstraintViewDSL.swift @@ -0,0 +1,101 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public struct ConstraintViewDSL: ConstraintAttributesDSL { + + @discardableResult + public func prepareConstraints(_ closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] { + return ConstraintMaker.prepareConstraints(item: self.view, closure: closure) + } + + public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) { + ConstraintMaker.makeConstraints(item: self.view, closure: closure) + } + + public func remakeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) { + ConstraintMaker.remakeConstraints(item: self.view, closure: closure) + } + + public func updateConstraints(_ closure: (_ make: ConstraintMaker) -> Void) { + ConstraintMaker.updateConstraints(item: self.view, closure: closure) + } + + public func removeConstraints() { + ConstraintMaker.removeConstraints(item: self.view) + } + + public var contentHuggingHorizontalPriority: Float { + get { + return self.view.contentHuggingPriority(for: .horizontal).rawValue + } + nonmutating set { + self.view.setContentHuggingPriority(LayoutPriority(rawValue: newValue), for: .horizontal) + } + } + + public var contentHuggingVerticalPriority: Float { + get { + return self.view.contentHuggingPriority(for: .vertical).rawValue + } + nonmutating set { + self.view.setContentHuggingPriority(LayoutPriority(rawValue: newValue), for: .vertical) + } + } + + public var contentCompressionResistanceHorizontalPriority: Float { + get { + return self.view.contentCompressionResistancePriority(for: .horizontal).rawValue + } + nonmutating set { + self.view.setContentCompressionResistancePriority(LayoutPriority(rawValue: newValue), for: .horizontal) + } + } + + public var contentCompressionResistanceVerticalPriority: Float { + get { + return self.view.contentCompressionResistancePriority(for: .vertical).rawValue + } + nonmutating set { + self.view.setContentCompressionResistancePriority(LayoutPriority(rawValue: newValue), for: .vertical) + } + } + + public var target: AnyObject? { + return self.view + } + + internal let view: ConstraintView + + internal init(view: ConstraintView) { + self.view = view + + } + +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/Debugging.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/Debugging.swift new file mode 100644 index 0000000..a78579a --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/Debugging.swift @@ -0,0 +1,169 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + +public extension LayoutConstraint { + + override var description: String { + var description = "<" + + description += descriptionForObject(self) + + if let firstItem = conditionalOptional(from: self.firstItem) { + description += " \(descriptionForObject(firstItem))" + } + + if self.firstAttribute != .notAnAttribute { + description += ".\(descriptionForAttribute(self.firstAttribute))" + } + + description += " \(descriptionForRelation(self.relation))" + + if let secondItem = self.secondItem { + description += " \(descriptionForObject(secondItem))" + } + + if self.secondAttribute != .notAnAttribute { + description += ".\(descriptionForAttribute(self.secondAttribute))" + } + + if self.multiplier != 1.0 { + description += " * \(self.multiplier)" + } + + if self.secondAttribute == .notAnAttribute { + description += " \(self.constant)" + } else { + if self.constant > 0.0 { + description += " + \(self.constant)" + } else if self.constant < 0.0 { + description += " - \(abs(self.constant))" + } + } + + if self.priority.rawValue != 1000.0 { + description += " ^\(self.priority)" + } + + description += ">" + + return description + } + +} + +private func descriptionForRelation(_ relation: LayoutRelation) -> String { + switch relation { + case .equal: return "==" + case .greaterThanOrEqual: return ">=" + case .lessThanOrEqual: return "<=" + #if swift(>=5.0) + @unknown default: return "unknown" + #endif + } +} + +private func descriptionForAttribute(_ attribute: LayoutAttribute) -> String { + #if os(iOS) || os(tvOS) + switch attribute { + case .notAnAttribute: return "notAnAttribute" + case .top: return "top" + case .left: return "left" + case .bottom: return "bottom" + case .right: return "right" + case .leading: return "leading" + case .trailing: return "trailing" + case .width: return "width" + case .height: return "height" + case .centerX: return "centerX" + case .centerY: return "centerY" + case .lastBaseline: return "lastBaseline" + case .firstBaseline: return "firstBaseline" + case .topMargin: return "topMargin" + case .leftMargin: return "leftMargin" + case .bottomMargin: return "bottomMargin" + case .rightMargin: return "rightMargin" + case .leadingMargin: return "leadingMargin" + case .trailingMargin: return "trailingMargin" + case .centerXWithinMargins: return "centerXWithinMargins" + case .centerYWithinMargins: return "centerYWithinMargins" + #if swift(>=5.0) + @unknown default: return "unknown" + #endif + } + #else + switch attribute { + case .notAnAttribute: return "notAnAttribute" + case .top: return "top" + case .left: return "left" + case .bottom: return "bottom" + case .right: return "right" + case .leading: return "leading" + case .trailing: return "trailing" + case .width: return "width" + case .height: return "height" + case .centerX: return "centerX" + case .centerY: return "centerY" + case .lastBaseline: return "lastBaseline" + case .firstBaseline: return "firstBaseline" + #if swift(>=5.0) + @unknown default: return "unknown" + #endif + } + #endif +} + +private func conditionalOptional(from object: Optional) -> Optional { + return object +} + +private func conditionalOptional(from object: T) -> Optional { + return Optional.some(object) +} + +private func descriptionForObject(_ object: AnyObject) -> String { + let pointerDescription = String(format: "%p", UInt(bitPattern: ObjectIdentifier(object))) + var desc = "" + + desc += type(of: object).description() + + if let object = object as? ConstraintView { + desc += ":\(object.snp.label() ?? pointerDescription)" + } else if let object = object as? LayoutConstraint { + desc += ":\(object.label ?? pointerDescription)" + } else { + desc += ":\(pointerDescription)" + } + + if let object = object as? LayoutConstraint, let file = object.constraint?.sourceLocation.0, let line = object.constraint?.sourceLocation.1 { + desc += "@\((file as NSString).lastPathComponent)#\(line)" + } + + desc += "" + return desc +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/LayoutConstraint.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/LayoutConstraint.swift new file mode 100644 index 0000000..5425ea8 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/LayoutConstraint.swift @@ -0,0 +1,61 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public class LayoutConstraint : NSLayoutConstraint { + + public var label: String? { + get { + return self.identifier + } + set { + self.identifier = newValue + } + } + + internal weak var constraint: Constraint? = nil + +} + +internal func ==(lhs: LayoutConstraint, rhs: LayoutConstraint) -> Bool { + // If firstItem or secondItem on either constraint has a dangling pointer + // this comparison can cause a crash. The solution for this is to ensure + // your layout code hold strong references to things like Views, LayoutGuides + // and LayoutAnchors as SnapKit will not keep strong references to any of these. + guard lhs.firstAttribute == rhs.firstAttribute && + lhs.secondAttribute == rhs.secondAttribute && + lhs.relation == rhs.relation && + lhs.priority == rhs.priority && + lhs.multiplier == rhs.multiplier && + lhs.secondItem === rhs.secondItem && + lhs.firstItem === rhs.firstItem else { + return false + } + return true +} diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/LayoutConstraintItem.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/LayoutConstraintItem.swift new file mode 100644 index 0000000..9b61d18 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/LayoutConstraintItem.swift @@ -0,0 +1,93 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +public protocol LayoutConstraintItem: AnyObject { +} + +@available(iOS 9.0, OSX 10.11, *) +extension ConstraintLayoutGuide : LayoutConstraintItem { +} + +extension ConstraintView : LayoutConstraintItem { +} + + +extension LayoutConstraintItem { + + internal func prepare() { + if let view = self as? ConstraintView { + view.translatesAutoresizingMaskIntoConstraints = false + } + } + + internal var superview: ConstraintView? { + if let view = self as? ConstraintView { + return view.superview + } + + if #available(iOS 9.0, OSX 10.11, *), let guide = self as? ConstraintLayoutGuide { + return guide.owningView + } + + return nil + } + internal var constraints: [Constraint] { + return self.constraintsSet.allObjects as! [Constraint] + } + + internal func add(constraints: [Constraint]) { + let constraintsSet = self.constraintsSet + for constraint in constraints { + constraintsSet.add(constraint) + } + } + + internal func remove(constraints: [Constraint]) { + let constraintsSet = self.constraintsSet + for constraint in constraints { + constraintsSet.remove(constraint) + } + } + + private var constraintsSet: NSMutableSet { + let constraintsSet: NSMutableSet + + if let existing = objc_getAssociatedObject(self, &constraintsKey) as? NSMutableSet { + constraintsSet = existing + } else { + constraintsSet = NSMutableSet() + objc_setAssociatedObject(self, &constraintsKey, constraintsSet, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + return constraintsSet + + } + +} +private var constraintsKey: UInt8 = 0 diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/Typealiases.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/Typealiases.swift new file mode 100644 index 0000000..ded96cc --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/Typealiases.swift @@ -0,0 +1,42 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +#if os(iOS) || os(tvOS) + import UIKit +#if swift(>=4.2) + typealias LayoutRelation = NSLayoutConstraint.Relation + typealias LayoutAttribute = NSLayoutConstraint.Attribute +#else + typealias LayoutRelation = NSLayoutRelation + typealias LayoutAttribute = NSLayoutAttribute +#endif + typealias LayoutPriority = UILayoutPriority +#else + import AppKit + typealias LayoutRelation = NSLayoutConstraint.Relation + typealias LayoutAttribute = NSLayoutConstraint.Attribute + typealias LayoutPriority = NSLayoutConstraint.Priority +#endif + diff --git a/jaem/week7/CatStaGram/Pods/SnapKit/Sources/UILayoutSupport+Extensions.swift b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/UILayoutSupport+Extensions.swift new file mode 100644 index 0000000..8e7644c --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/SnapKit/Sources/UILayoutSupport+Extensions.swift @@ -0,0 +1,36 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#endif + + +@available(iOS 8.0, *) +public extension ConstraintLayoutSupport { + + var snp: ConstraintLayoutSupportDSL { + return ConstraintLayoutSupportDSL(support: self) + } + +} diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.markdown b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.markdown index 115a439..865b23f 100644 --- a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.markdown +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.markdown @@ -49,4 +49,27 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## SnapKit + +Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + Generated by CocoaPods - https://cocoapods.org diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.plist b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.plist index f5305a5..bb20e79 100644 --- a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.plist +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.plist @@ -73,6 +73,35 @@ SOFTWARE. Type PSGroupSpecifier + + FooterText + Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + License + MIT + Title + SnapKit + Type + PSGroupSpecifier + FooterText Generated by CocoaPods - https://cocoapods.org diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-input-files.xcfilelist b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-input-files.xcfilelist index f5030f5..9b017b1 100644 --- a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-input-files.xcfilelist +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-input-files.xcfilelist @@ -1,3 +1,4 @@ ${PODS_ROOT}/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks.sh ${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework -${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework \ No newline at end of file +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework +${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework \ No newline at end of file diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-output-files.xcfilelist b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-output-files.xcfilelist index cfe803f..2c11a72 100644 --- a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-output-files.xcfilelist +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-output-files.xcfilelist @@ -1,2 +1,3 @@ ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework -${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework \ No newline at end of file +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework \ No newline at end of file diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-input-files.xcfilelist b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-input-files.xcfilelist index f5030f5..9b017b1 100644 --- a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-input-files.xcfilelist +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-input-files.xcfilelist @@ -1,3 +1,4 @@ ${PODS_ROOT}/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks.sh ${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework -${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework \ No newline at end of file +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework +${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework \ No newline at end of file diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-output-files.xcfilelist b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-output-files.xcfilelist index cfe803f..2c11a72 100644 --- a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-output-files.xcfilelist +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-output-files.xcfilelist @@ -1,2 +1,3 @@ ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework -${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework \ No newline at end of file +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework \ No newline at end of file diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks.sh b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks.sh index 21fad2e..eee3e67 100755 --- a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks.sh +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks.sh @@ -178,10 +178,12 @@ code_sign_if_enabled() { if [[ "$CONFIGURATION" == "Debug" ]]; then install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework" install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework" + install_framework "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework" fi if [[ "$CONFIGURATION" == "Release" ]]; then install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework" install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework" + install_framework "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework" fi if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then wait diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.debug.xcconfig b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.debug.xcconfig index 6282fcf..1e051f0 100644 --- a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.debug.xcconfig +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.debug.xcconfig @@ -1,11 +1,11 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO -FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 -HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift -OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "Alamofire" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "Alamofire" -framework "CFNetwork" -framework "Kingfisher" -framework "SnapKit" -weak_framework "Combine" -weak_framework "SwiftUI" OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.release.xcconfig b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.release.xcconfig index 6282fcf..1e051f0 100644 --- a/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.release.xcconfig +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.release.xcconfig @@ -1,11 +1,11 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO -FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 -HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift -OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "Alamofire" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "Alamofire" -framework "CFNetwork" -framework "Kingfisher" -framework "SnapKit" -weak_framework "Combine" -weak_framework "SwiftUI" OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-Info.plist b/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-Info.plist new file mode 100644 index 0000000..5d3ab08 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 5.6.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-dummy.m b/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-dummy.m new file mode 100644 index 0000000..b44e8e5 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_SnapKit : NSObject +@end +@implementation PodsDummy_SnapKit +@end diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-prefix.pch b/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-umbrella.h b/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-umbrella.h new file mode 100644 index 0000000..1b1be64 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double SnapKitVersionNumber; +FOUNDATION_EXPORT const unsigned char SnapKitVersionString[]; + diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit.debug.xcconfig b/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit.debug.xcconfig new file mode 100644 index 0000000..03fb3c1 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit.debug.xcconfig @@ -0,0 +1,13 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SnapKit +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/SnapKit +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit.modulemap b/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit.modulemap new file mode 100644 index 0000000..4b3e47b --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit.modulemap @@ -0,0 +1,6 @@ +framework module SnapKit { + umbrella header "SnapKit-umbrella.h" + + export * + module * { export * } +} diff --git a/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit.release.xcconfig b/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit.release.xcconfig new file mode 100644 index 0000000..03fb3c1 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Target Support Files/SnapKit/SnapKit.release.xcconfig @@ -0,0 +1,13 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SnapKit +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/SnapKit +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate index 39bbadf8eeca761515708b3b29dbe9064631efa2..d85218b9600787cc5a6807810a8d03a67e380f2a 100644 GIT binary patch literal 39430 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%0DuY%XY4iJKt?pb^arr_g#g* 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 N?aO=8|7Y=>{{TfZejoq< literal 37742 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┃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 + + + + diff --git a/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..4980697c82700baf962a023f83522800395b7e10 GIT binary patch literal 41849 zcmeEv2V7HE+xR`_-rU^Ws025l$P!RUNJvNsvUdao31LeJ5EW&~aMaq~(_yW4PrC)J zw%XRZYVERi*=g%&Yp1n#?_u})KPNX~sBP{0Z{P3zzTc}qAi4LP^PKUVXP@UZbhI>g zb?fzq7{p+PWjIE`@QjiXD+LcVlZId7t@!`HK0PInI2;e9L^te9!#A{D>HYkP3-NjV7Z=6osPE6f_mZpjf0u zIuwWWC;?ef5}Jn6P!_VIGUPzzr~*}@DpZXcP$OzW^U+dt0a}4BL)V~d(GBPpv<9t1 zx1#lEBie*Eqr1^obT8V49z=W4Bj^eAGJ>&RIJ51Y{2o@h%Gn~TX6=?#925S=ipqNhx2hE zcHmmvfM?@5crI?i9e4rm!3*&jcqzUbUxTm3tMPUCdVB-E5#NMw#<$=#cpct^@4$EB zZFoC=06&Ny!n^Uq_(}W}ei}c6U&Jrrm+@iz3Vs8>i{HcV;}7t0{0;sVe}})vKj0tn zPxxp23;q@V#tN(-JC+S(gV;%I2phphvr||tt7GHYLbiyV!4|V6>`b`HbOdl7pXyPCa@y_H?hZeVX?Z)Z2Mo7nB_ z{p=&`6YO608TMKB1@;j8BKr#aD*Gn;J^KUuBl{EkGy4nsEBhPAaEQYk%PF}LoF6xa z8_NZAleiErnw!EIITM$_nYkn`lgr|Yxe{(BSIXJBGS0zOakIGDTr1bZE#c1K&f?DD zmUAn)RotcA4cv{~P2A1gE!+m~Htu%re(nM8LGB@LH}^2NhkJy3hTF$I&mG_naxZZ& zbFXr*ac^=*xVO1u+&kO{+=tvp+*jP!+;Q$3?py9V?kDbN1*=di{1h6+XvKKN1VxA< zR54i*qli@)6w?&RiWEhvB2AI5n64;L6e=neRf=jwjiOdDSJAAPrvTd_s4Rk2O6U9m%Pk7B1{kKz%h_ zTlpkDlh5L_`5eB8pTW=O=kRm+W_}(&pKsw?`8K|t@8G-nr99!!;aBpj_>1_f_^bKN z{N4N(ek;F?-_Gyg@8Ngy_wu{=`}l|W$N6XYXZb_?i~M2!75*6i4*xFy9{)c7G5<`kI)yHwTj&uM3TFsJI7e6^ zTqs;FTp?U3+$7vA^a&efdy6}eZrf@`fOE@aLEgTcx5k3$;7rqg` z6@C?dQz=z~Dohowicn2fMXI7y(W)t`sj3)NtV*x4s8Up^s$5l`YNo1GRj#U2IaTvj zU8-)?C8|qRm#HpSU7@;Cb(QLB)itVXRjXAus@AG*S8Y^nRc%u}sCr2Ci0Wz8Gpd(W zM^taA-cx;~`dIai>RZ)!s_#`li&$huP8=o<7e|PGqDCAejuivN2r*imB5FmQm?TaU zlf@JIh#C)+@tPyL)Sz?{&6zfHoI8SU9+r$N8r`RJd6qkr+h|9#YL?WIeo-1A; zt`IL2FBh*6uN1EmuNJQn`^2^4I`LL+S;@ip;v@rd}Acuagpd|&)P{8;=%{8ao>{7U><{9gP){87!ThpC6FN2nv!QR-;* z6!lbfj5=1WRqNDoYQ5T^j#nGiCUt_^thT7L)YjfU)#@5`t$LQa zPVH1TtLLd()yvdpsn1qRYN9?zyKoLzsqawVsotXAs=i0PQ+>Z9 zqOGT;(#MoL&>`Oh3 zQ7O`?i7+K37;>!UT&*=RAzN!oG-qfN_10W%ZbnvyHPeutYe~p;YDU|#b1K?9=XZ6u z8r|9LjXkaIwr(HfKqictx`7E|CNLA3U}h2%!h}i+iIjy+|c5#XzreC zZ*H5@;)XX)P4wV?$xS$C*h}4A-R+$&DDIIUvR&OSr)Clq&1mcf5u|@0&_q^yTX$!B zON+bHsYxB&*wA-IOJasWpAEBQ$ueq9nU(}?rYR#wtIsth#v5}ob4>|WrzT*a+wJWw z4X#d`uiH+|_d~3 zYleN$vOq7&n!8(IdHi9He@AVA$f4dd^3)@Du6l&LYXT3wvpl9-x5qqxLgQCU^*YH&Bto8KnyKBp!~4@(z9 z*NrY)h#vmSUn~hB4(M}IV!I}aDPr6phPE@EOfz!^e9mKB@ZHUXgp7vmQUu?cK}gjx zT}-#9)Dorv{<@%ilebI`d}@ZbZl=kp895kL<)tWbYR14*-{u@Z?1P4Qqbb4cr300V zJR8lH4V$`y%I00&u$661@T+-tGt7xfvqloiX~zxb1JA!o(hV8ed8eRFeVJL3TJr15$(2S* zW2JzDw&{j~!Xnr$u=I_cZg-oE3J!-Y+uq#?`yPfq^pzb@L8d1_%yJT}xn>xxHYdxR zt)Y)?9AGw25UU*F@ru)zYGRAvlOsmFtEOvDYPGDjv}B> z{?Z6&*J#O3Of<%4XydcZX01LxD?^)U&9!I~bB+28qu!7iZ`9X#_pVj1Uv{Xv#!x#e zyv_+^$A@zANCpG)5<8elc?IPXCu4WO&;oK{Pi1MeG)5Zb)cg(wacU+k_4Z0@fYDO= z;vIWJ!ZJGe#-{!)Aw%zLO!0Jnc6i-fX*{$(?hjfY;~`vfyE5If$j!F2*3PhJHv_e8 zZ*z4n>1#=;(JwZ{n+&rpMvESFJAWn+7{V0L-Ykp_G_*6AbC_$GUgmbtzjlGPb(r~> z`3{W&-N}SfP$8OuN>HgsTcWyB6{Xx!FGCdg}Xo}_>vnA62Ct;ii_n6K(=>s4cvT?=-askAj$WE^uC?j$$0)@ zB>#m?JmequsN{Emu`)@_H0G2$3utapPdDXVlx?krVkt~2&{FzsU`Kg*emXOqnR+{u z!DKR7Og59l=-MJgLMqJ9r$h`(3u%bF;l|Kgl~4>y+Kl*lrNP^ zcF6(XDkUutX`mtYx$TQ)xE40gaRD2ZAN7CD=yU@!^@mc9?&g-}?q+vaMRQa4T)CL7 zt*Zmn`u5I&V*NEe#cIo&-HWo_4Lx(_P?kHY$lcoBxg@XC)iHPAsjmN=p34NN1`Bu$VeO2N{kb<*lqF@;VTDvu`lN$)3Yxoh z{e9B)GYr@(zf;81FBr1j-2y{%H}zKrvqfHKdryb_0#Lon*Rc+!hjwfMkZ&O9Qm7Or zg-a3Zn1#$DW-+r+nk+?0(eN8OHafStrQ6-<5mYjl6hS9}cLKCRS66pGB>EWkp`&f~ zDGQI6+i*8IY(v}e^+#d|Gj`1yDN;tg^O*C8?oZ}CCRG+uhJduU!ve$p^)<7CS;>s; zWiFJa^fIfYsZ@TXV$CV*901YfQsy$|a;GK`rk)~n2{jUxx;r}EUBGQTtXqneVnC{S zZ>Ut$-Ltf+x!c|984Ms6$OzpTjg9WEE_V~`19x3lBbC`o+PeV%+S_Jzc~`~;b-Ffd zCh#LhjSC76i;9Uenyo1rIkv*$((>v$m%F*8v$8?|wHx@*jMY*N)d@yU2-ICJb;(A$QzTs6bJD8#e*59H?MpLToUOV5Y)rjJ}(FnQ>N z9VnWSe;RWIl}m=Noq0lM4%o{af1wK(Ln{?y*H%uh8agD3<}wb{)XqBX_98x-0_{16 zudP3E;Tjs7PPdiE-@8x+t;`;~cFyFvLwZ$~sn&+e@jTp) z&%u|2G4>X`7T*oR&TbHNj^dB-=lE-uVHF_aM6t1~o=ssh!CczJwt|4OkUf{Zn7sl_ zqTAUAJ!a5@>|yp}_D3*t3fwR*08E_WVAjmxrgKGHHP-<~%oW_V+!`=e-ob6=?gf+O zW88C|(KG#`&zbUf-*0Z8%zdt6t_BT1XK_c1tfLqAbhk9OxheX|=6}j7yqs(;NQKu@ z-gKJfotg$`S$AGmxxt)WUN!&&n0~x@;OiieWlK8TT^27mXSv;JGz^sUP;@&;LTz&f zG$f}yjO&=|J&LDKTI-i~*D}{pe(GbIH^PcfUC-Ra+|1mB(szOl2|6Sipb0ry3ja3tG$(SP0D1-T`fHz3ycH4>k&-KIw|u3 ze(LEfC}!ibGxPd6GSvd%oh(04UQI>2;+(OmK$mJ<~Fx9K|`LI01XxDc-{>a+yJ>epbvPgW{)zD_cD)3$x_M?sv$4&lgv{f zCwUjx?(Sa8JP8{&)dBjVySpmh?QWcHu^RMpOV2U;)=24c#TS_UpjUY-W^{JCmaJu7 zU=A>;paXlpY8a6G2DGPmQ#=@etntg1ora|9oBWr7d`#^FRp}KeN6PI3iMIiy-HCLv zC!boB^)M_QDAGNR-94RvMWKIG&NFx6=^g2w4eW#ZHuDZMwU;?2*?O6GrRfw}Xit~B zv!s)%{+;foED+GeGCNeQ|2?~bLI=FKq{1qq#07N zR3gok(w!Q!w?`D|W`nIUvj^7H-ARS^e%-`%-ubY*{*%6ge-*tW2;L6lr{QF`Ge50psDMjfB7gD_G6 z8X^{PQn^&o2Wou;P%jVt8q;6B(CwPtJN^WO+Yrih$?GYM=#xqkl zq2Xu*@BAm|Y~$`Buc7*H|oU}f#LQT?C!BZo)OIF5#xy#S;L2Cti26G3ja+Y_JKvyytV`?ZTDF8jv!=xqxQunkpQPeI0 z$4sevK@advz!~Z+88oOGR7ZDAEt(~DN!@+O$*h-p06_s2N7CVEba_E~S)|5?pYYN% zfAVCw(Y$qF{+a^@ux4qY^pNy3L2=UZ!>9$dqBhhHE7gG(pib0<92rr)98tEyhk}X0V|t>3k*>>QbUhKp{JLmh#(;ef2t}L6T;y%(#Pa?_-cmR< zz8*MA3!%2}+W^lPutz^2T_#;FEs`FT9*{1P?tclLiI$q6at&?ax=G5sW_w7tEXQbGAS)NBP8#XxcD%b~bkJlogiz&q&zX^FH_ zx>#B)WjHl){iJrtWXcT=n3wvQuDo^^qItKXm1q^Z2wjXWk5sG&@Z?M|1yPR*tPbF1L%XZ9DI_}6)#aWd@3htEIX-A~9WVn7O!1WdCMSW06l8AJsY%Wpw-fZwxKbKY? zB4PR7-{Em;LFxe7(nsZ{gJ>Jf$#%2@-Gg=xMxLtf=2qBbuGS7|g><0=V$~TG zfs^9m;wW|QiUV3bXP(R5+T0eG?Oy0^Y430@qK?csM^RQ>ygoi&Yj8Ff+v8}NxJ7RF zd~4ibo59d8l$yrTnbx&*bhginJ7G5D>Aw%%53~c+40=x(B9NZfp_kCh=rDRkxo?Gw(lyf6(zVhJlqd)I=r14!cnt^kB=BmlRTx-(NoV`QW)M}WN|QMN zvw!1L-0{kZ9&htlTV|H4Z6Wn+4}Kxjhj-DuH9!lzT<`<*F=ZGZqK~B2(sh046OhlY zmnAgKAXV0DhaWxYk*$kzBhZb)(-%KHSdK?uqo2TOkB*~n(6{J2^ga3k{V3fi-6Y*C z-6E}#dZoS%=x6i``W5|#sY)Lqt(CS&TcvH%c4>z!-uZ#D2%L@fzoWggyQ{joy}dQp zMa}=y@qva%9k$Hn#?+@IUyn5oDiQF zp8)PrF&;p7Q9xx&ms*X9Iuk6EKGA532M=X} zfdY!R>UCzbS#PkIU^cBrqsK8C4-;xJ7+~(q=6H(!vI1nPM=)fWRT100;*$ zLO2P;4m=Gf;}o2V)1-T(ozlJ1F6lmLdG+FjW^L9IZO-(L>XwRD?HN^C^Mao02K`K} z&7p0|U0j`6RjoB9YPQA<`t6 zPb+-Vq_kIh*eB%4b2kgkTZ`+k6W2?7q{pPk`$aW?(FDTJBhsTF;^eoty34b4U5lD$ zcWXGGkTXaK@;<_?9=YdP{ z^0@ne<$?rz@=M@%Ma^xXC}~x5Pq`SAr##;2dqGoPFcp*wonc@10Hj7D=nS? zrG9M%%7mZXipNI@T4T3c*FJl8mmB!l$=#)bSZ;eu6R_x00g@e^awF78eyS$AV8j_c z-E-SJp&vAIPM#(($SnlUOidErkMeU*W8joMsl$uF310{>WNRJnMe}-bpLAGSF5~~L zpl;&z5JLe!(ks%do}cBK7)_)P8ScV&gL@cnmR|40TckIrdpI!L-O}xn0n0{&{z>!I z+JWz7#`fWR@J{JX=|~^mgfT;_&o%nF{*a*@GXW z?L2}Xm5xd8^r6@AE@V1de7n)n5kBbk-VZ*L zgVIOR$I=fpIx*8bf%Zlh4S&=0KL7!!8Qcfd*6b_e@!Pkz0n-5s4;7E7OcS2=h1^+r zDqp2j`ZL@Z0#9^tto}biNUs-(9mB+rjler^grX767ti zdtLF2S_BR= z9+wfsNKX@_l75jMBuGb)a02UKRjlY^9R%^xuS0kTtAV}4`m-b1QS4~xH-f-!!4kyv zvk;b{+<-#yX!m|a?zSGfaa&}f>*X2j1ZEvAg~3mBrtGf&eFno$mYY=k9*1G4QVv6q zQf4t7{~(L;3Dq`=4VoY$eb_wu+#!UOvOt(!DZ^t)o8#jU#Bh{99VC8Lt`c!w@$+2Y3xT zo1nm6b}m6dr^js|q^}(aE!&C#^CtkiVLQ-k1Wh~zzwu|e@ZDar?Ss0AJ;TFe*d@|2 zfqX>$oGxn#9guR}<UBmXWee7C-rVuohpcsN; zVG{lm8O2ChUZmtFx|tevPet(W0D^xPyBP@n7IrHo_(n?blL^vOiti+7+KCi@2Yb%| z#kUe<7)tUVplJLc`w+XEeVCwlf=mRN39<~(e2?Xm0+LOmZ9U~hL&^Pi<@?Vlkev(Y`7yN@HzrVX4vOh4g0h<4kO!Jd`JLL!qMy^flTkKKx zZT1-Z4*M?q9{WD~0sA2f`y_=Rp!{hBr4y7vP$ofH1Z5MHLr^Y3d7D_UiBYYB{Q_wI zS00-G4W;?{KAKy`5XsSic=6|^U{1yfX%|G@CN!rz=ij!=NC}? zkzVZJG~7s__Z)Ef!d?!ze9`IAdu|*T2-J@oPwD*(p!Zx5rT4|BqW7rH2P~9}@Q`^f zoRayOK<2q1O6H4ykIZvZIUSIBE(QX_wFKD-0t(^)GSBHb110n21l9e0WS+Co2}~rY zqMyuj)40^ZWS&doV5d|PRCQWpp3CNJry}zZ?^VDRazzAH6I4S`Edi(H{~9vy;fY*1 zIE}dqf?N=t!&NhWTn$$Xu~m(7(2=Y-405;<)IiWxC#eL~i^|n=E}s__ypfj?)Jjm} zDLs?i9B%FaduSo3X$X7Z+Q1SA=LS09pw`&=X3AsN!SUP%9(Z-hH6LpuL=#`O0&N4VEzNEc8@ z|D<<>drJm%p?66C2KNZ}F8AL5uX}|1m=fhrxKFvyxX-yS2m+G?(D=&j}Dnpc@Igsh`g%hRGb@x_^|desn{wxUQ;3QSfpLs6_KQOqRh zPJ-?tXfr`~uT$6+WeSG^1WRB+_Yib1LAxYvWubev$01);N^PpN!_~6go$8J^*T7FR92V~yS$zsBhge8zRP}@~ z&_IK(Qr9AnI~z>mfkWSVk7(H&+dH5iAwyo$ZrZ`Zo9zZeRC5Q!4?z8~LyCA>ry*&+ zzP2jbDPDlsv9lNA=RDDK{t#c$)!yQUKo*D{pd|cWL^zM)$XmOF^vQyQ+9O_?5(8IL; z9w~BQ<-*g?jrKGEae?w8I_dJ3wR_gqH?->%AmQAuxL$FC;zq?yiklU;DAp)?6(H|C zM$qE~Jwec3f}SJ@csQ6eo+0R2I1H+|Rk2>NL2;YncEv`;CdC~T_;Z9UB5Vm^8wk6M zuxAlgA}paZH98ZpbAAU6#_00oPynYEoI(fhjwgszrb`~jQWsd&Augw@R!+5GZ=CC< z2^~huU*XgNtT7xV@VFHqXq$RVXi#m78yXx4iIuC-K&vKC#pdoM5M2Zq0(-`1Zxi$#!D@m{1m_W4PjDB(7ZBV>@D76a zN!mf)WW`b0+H=sUY551OPT*bwha>oYz~}05!4K}jdvX_EbZWZ(*)DX&*}?JW{{5bO zEcfK)f3_zc$39TjfuOUK=j02yC$IdoVUuuJa&o-hbNb}Zrsf;DL$5hC^sMtg703A} z>+_@Bi8q{@f6_z>$l4Y6hZy^t+>awp&7yyBGUy3QkChg}%Vh^CeR2}6@SNP6qfSlx zKiC^83!H?pavUVD4U-tI>N!ZGt zg_7`7z+A~sCFnD+y_eU5y%*NO^B*Q&{_hKS%BZk>RtH|g-!t3Rcd@c_bP>3Jl^QqDITQK_aY1D%C_3s&d`C`7B zqGJg^lP~4%d>QZH%lQhvlCL7@dxCx-=tqKnBIsv=ej(^rf_@{IAsB7sYrN>_^rB-U zpd-d!bi|ww9kJ@a=;+D%p&Jt8^#6;F3n)5v63qJ0vF8ub@l5_KijK<&R`l{`6U?6` zI-bifrwFx^KVLdRu#zIw3P30-ih_8~K~~oB3P#HGD6@!w4Qu@Cbta2-XnnPw+^BM-e=l;4uV`-Ndi;qT&WX z#b^(0yo0}!;Bh`=1jF8O_)MuIoG-D+A=lB<7#C@J3?u0&mKYxJWi3A6qgt>CR zUhywlPJ1VMBeqs zgUKEFfKJed1W)ewi}9cEpTl7IPx;RXjwCp$kN<-Il3<{>Q~u@XI{rKUXYiKs-}684 zKk`2jJeA-Wf@2BRuH%2mD%Hvn9%7^%PKlB6j}XCE zWwde<@I>VpYj3??{%U^BrMf)fe05}ZWvG=h@}P9ZpTqcTJ$ zM9K)6Cn}>UPfYWXA@KRjA{x$tNB_rY(Eq%MrZmIOQNq%sdr6BDjtcy~h^EX?W&vqY zW)hs)tIQ@i>-2b{GGAE$7^SpVMAXlxI<71WC*8L&kFe z8BY}-;zN=Pm8&Q^t|ZvetGtNd^3z1e%am7AbiACRV}&O=gx?Qg9>DBQ;&eVx*C}s0 zC2LdOqFh5+TNT08l6ELxQm&%|x|QIXe!irDefvR&sl)e9cc#HB0CD;!)D)%a%R6eDATKSCfS>+cV;ku| zfT~k@ND1;#i*F-+@`qIYYvng|8yzROtylRi!R@EHjeb=ALNRKm@@FbAcKkVw7FdDz z;Gm$OI0)y_e^)p1Vy!St7zZdQ3>QWSeu75u7e)%BgwetnVJyMj1osfUkl;lGFD7^i z!DkS>l;AT7Uba!7dJRMd3KL`$6hP1*_$(g^l719C|Gy~s_bc0i4p2}4IrwZZ3JURn zf`XA*Pk#uOqzun5i1qWO^%RnXWI#g!gu`=s1rQF;Jv}rOGK6eEDj}0n^Yc9RA4<)? z{sU?*Oc#n|JS?PmxSUG3X&zemyH+2;E;vEF5XuCHP%cyml|q$JEz}6L{2HN-;1vX4 zNH8#>RRjYgx|rZg2)>ly%RJ)6<(q_hFE%#$#0#NWm`CsxK754p9sT0P^#nuelmChr zf3J8UETSk0bOJ&;JZht`l%nLBxQYG{e3kU5=NF*lFdy|fS2&-dN(<>||_}bG% z$(6#z6eU+tlw1ucDO>_5iLW~qO8TRle9*2ER-aPF5Uv+)pfbh{1mAd~j3L}YN417v z8r31&%Z0VVdVoGi#t=3Lw-J0Z!MB{2j3L}1Z26aD4B=kketBQrNB31P>?;BG0hkc3 z`#r%**dshg_thi9qrzjtmf&>+g9rsQV*|l3irWd^Nbn}l zzPe+Ru+O`%4gi(++OdSg1mEe~R}eHY*p9XBzxxU@=>Dx@y8uZmdWH80zRS0-KKvtT zL-<1YitZ^e#@yX20F~ZyntSRy;YW&FJB1&pw6XOxtP(0l#d^?Dg(*62Kb)lw$pnE-O=|Fyo=!b2)>_SFv~qi@IxMS+`UOPRz^ov zAiji&#&F14HAw|_FbMLHwa7={m~$XK=1lMt1V8&m0C3RQLP!&_^4))q7Po(ZDI~`3K!B6#5cU6U|8Xym%?y4FUsERf~!qqQj4<+URzA?li{Sqat)SEmSR1Emkd2ouOK)I#abwbr$RF5`2i@7YTle;FmqS>F_4iIo{p0+$S`tR;fU`dd0V!z|Ge$G{Gvo``_*Kw{NHG zC<1~&`l@d`-2~g|)aNvPh`vq*;zzFvnD*u@TY54U#h-R0h9lX;O_~BtO`y|?2sTgZw#R~ zOw<>b=S!UiVScA7?n!;n-qTg)>YU^5mYc|;2@@a@#wl8N%FhOu_9y_VA1EgLNbu*< z@B={?WL0tKQ;P3uTlNt_r$F0RPRTq0_S5$}soM#bv zQAsdN=kY#KC5i-pL-4nfwp^Zpa(RFdHG1-RhveY%M5c@WB4nPQIApp`oc^`qC~-86 zQUBv&r_OD6%6|C9GfspQuQb7@??I3V4y3hci8xUVmOy9tiQr$>iXmdC7)J1KgpDL@ z;Sf9*qhvh)*{SLI=kPp^2BV%j>))UlLortF%dbui`3L(lIG*kg`Xy_DVw`A#sBTd& z8pL?fNLYrjh_INjES%I5jiN~O-4*dUgQcCk!!h~;7hoYZ3d2s?qW6A2qk*hz#9`AG-0O@ z77~WOCf+RGBCeqnGlsCSgw_AsDJH%BA#)Jr8wi*64d6C$6Q!lMiyH~6C9Do;sR))Z z!p8j-VhXwc-}0P4@in_myq{9l?cxsc9&x95ueeLRkFW;9#uFA8i;1uagf$b^vO#=6 zd{BHy+$}yV?x9pQk+3<0%_VFeVe<)V`;Vup&r+)ToVZVXUVK5^FCHMQm9WzYn?l$$ z!UBuR8lb8#%6lluyIHlS1f4bBU`T|-ps=5;kb@KMRki4>7QNX37g9kIP<^~77bN7k z)FmVuOnN=!@idvtddQ+_PBiEeA*-ha@^B_Xx=+a6=u2)Xz9Iuk_HI@H1j$AdAipOh zd`vVZSQBX4NfS*0YBE?&CKDungp{QQn%;6KD1AJnxBMeWZ^)2Ry^tWKs4fxGhnkHE zM#v2cIVAy**`%`?t$IjrnP4)Q42cF$3eur~;9Ar_fpk=clRE1Qc2D8}+MXivQ6>_pBKvq{+YG`O6 z-KqGY3@Y0P3Noqc^cKho8=nZXXM}96bfK(t0uxOpqY3hh(j1l42bza{K+ z!WIA>ou^-@OB9q>FYNc8Qb^$d=V>5cyAdNv<4SK;J%?mGQ zz8<2a_-gs7A(O{OwMOl)9;qIs9<3gu9;+Uw9`cOz64p-GGQv6tTTa*t!d4Q< zE5%lCR0pXiQ2UX3k~%~kst!|!g9DhYA#5!*9}#vIfkacRld$!Kb-|;^D!B6w)Jc1G z0SKP*XF1PZ31p&%Yam*yo4ayan&;5_h~b>#;1V{t3=tAm!xc6loc1^4Imt-#7!PU9 zr`v*lB$xE0((Z;sVV=8Lyboo_xvtjc7P#1B%wS-ibU-wj0YjsUl(cEgKN%^fr! zUsrQ$4|Kl0(*VQ)PQ{(@^i)q$n-+GdhIjaWZofb4~FMS7)o)R4%6jtd{>6;$scIuH2p~nHe_Yu>JjfTD`r&rU`1feItD(>)J;-WvP zmiHtgy|<&MmEN==qodtDhn_0za(FJIDK{9ifG}u2@|X_P1OysqdIn+V6LtY% zyZcX!s%NSlpveWrB=se!9o~@n(HZ``B zuw9b2kqMcMnGnxi)#~|-rP!Z6H1sw{UMc>ocU12eRArrTQ-g#yYXa26Mva~@F)BJ1 zQY?pAQ*z+qJzKH8tg^;?54wDjHQbTv9hQ7oFkWjfphv;otGRHC){DWDLPAG)?!|^D^a3G(G&{G&HK&W-rP+US z8D3IC7M>m{PpJP24cslwd+yQI!D%nJSY-)Zx!mfo(N;4dHR*v7aLY6}`DHi*Ee-1S zSV^E=)f$4h|Ji|Q49Ab_z@45vi8Ymdv|x??ADp+?V*fb-#Ymv*!OU4S;C$I;VpWx_u60^DNo zDtaBgiH@RU=w0+aR=}-*p*S2zVGYdK)&z{^`+{|!G2Gl^{THR?AhL1j?`B}!YWV6 zbh+kr-kaqhgS+}hG;bqX0y$~z>{fNJx{sdXR^O^#52jK`N(<+;K}R`4dv!I$>>xoH#n^AGmlVj-Kv3JyWXs zZn>Z5{|_I+Rqv4dalwCpd0Bm*+?Nag?Wd!GwgoU@Oe|wyl9)80YvoKWQ>R?0JX3k0 z@^a;k%C!*Cyb(f}wk;gyvXbybvf%5Q2peAxe)s!5=(pQ%kKdzykNfTQd&=*1zt1(SW~^qa#-b_G z)N9%_?V2++=W8y|T&P*4xma_JX0_&e&5fFyHET4FYTnj->o53^@elS7@elKl@Q?J5 z^Edb#{S*8x{#O4I|0e%s{;U0O^^r3=bF`5E`Hh&%V7f`Foc z;((a}_5epfN5IN}bpg8r_62+v@I}BcfqbA4C2Bz2a%w2 zgH{Bs47w=jlAwEnUJW`H^hMCuLEi*@AM|6;&l5r>Oqq~4A!$PLgwzS?6EY`cPsp8+ zKY>ixKH;;8u@fsMUNUj}#0MunJ@L7T&rdus@zBJVCLRw)!F;d~EC%}p`v;E-9uuq$ zE)K2@?h5V+UKG3}cxmvm;Io5C@VUX~2VW3;Vep#ZzTkDi>w|9#-WYsG@aEvH!P|p( z2HzL_K=6@C!zKk!nlh==q@+gsu#|DD=+I zr$Rps{Vi-_m?11BtR`$$m@~{3))?jvn-kU?Hb3mVuxrEChdmUwKkU=6&%?e9`#S8K zu*$hPQ{W2){i1?(iMqhr*AB9}9ms z{QdABBKQa)LW~#|;U6(7A|xU#A|fI(!WfYrkr|O4ksD!;sEu$%v_+f|abCpoh!qhl zCo3mwCPz+=nVd0s`eeuCipf=zcTIj|^1jIjCm)&oLFA~&@sU}PC6SepO_8%B=SI$p z?223xc~<16$a^FAL_Qk%c;w#5ry>tT9*TS^@^Iv#OTuKy6DBxtD>)p?u}j>eQWfF=-Z<=Mc)~{Ir@RqoO1V+2dC_wvS(_*)X1sPQ>RWnXX>R>FQ0nl)Q_fqH}!|9 zKgHN%YGayX=Ecm9X^mMJ(-*TT=AM{)WA2N2Am*W%hhrXzc`oK~%(0lyVt$A<#ummp zVq0Pt#IA_FJ$6&B~)K+V2wRPGjwJ&O4*1n>BP5Y(x zSKS!hI9-4)NH89vnbeXzrU9K))H(gh#o1rVwmFmiL<+@5;wQiB_ z8r^-m*W;MD$he%i=C~DcTjKV`y&U&y-0N{~#=Rf+Mch|$$K$?@`#$c+xS!*G)vNU* z^cwvr{TTf?u*ziUbM^W9>H1>*Oub$2&^PHj^j-Qh^%v?d(O;&&LVvCPI{gj$oAhh- zTlL%Zd-PB0pVmLC-=}|3|FZrS{cHL+^q=T|GlUqX80Zx~7K7E0YbY=j8Hx>MhH^ut zq1sSyXfv!dTx(ck=rgP{tT)_d*kagb*kRad*k!oiu*dME;h^D&;j{Sh@!9ds@tyH~ z@w?;Sia!?rZu|%FAH{zX|9SjZ@yCtBjT+-f<7nep<9K7BG29qy)PZf#1Qt81G1q7_ z78r|+PGgI)-MGNmWn5xhZd_qpX}rjIo$*HF&BisxJB{}lA2jYZ?g2w(hAGRGW6C!< zOf9B%Q>UrNwAi%Nbe4&j&NE$LT4}o2w9d4_bh~Mj=}yz#rfsHsO!u1ZGd*b9ZQ5gc z)AUP1XhK%PoP>)L?nrnc;dAqNbAmb5oMFy3=bDSnmF60Ao!Mn>GS4>8HP16IGM`~y zW|qw7n$I`)nKzran|GRbnIAImF+Xa4+VSWK^E3B(lW^s zX_;b)wZwTY_?c!&wWM2$Ee)1)Ef-lXwOnDj%F=6DZ@JB~(X!dH)w08~({j-AmgPgs z$CghmpIg4P{Al?lkx66|`9vXcTw+FIYvL7&HzeMjxIghw;!BCIB)*>bX5!JrcM{)A z{LPB33aiqpvZ}4atz)f|tYOy4)@W;tRclSMrdZRg8P*xr8f%@k-r8Vou`aPLwJx)s zZN12Psr7Q}mDV-ZJFRzHw_3L+MJFXBS(2=$+sup zmAoZ+Tk?+N$CCFZA4+~D`L*OXlaD38oBToYN69~=@F~MnG%2G}#->b6nUoTiGC3tG z#hhYGnU~U)vM^;y%F>h-DHo?)nsRx{H7VDn+?aB6%6%z&Q(j0pka8&HrIf=dM^oNO zc|YZ&luuJWPx&=9I<+iyN$NSN*QTyXU7NZ-^|sV4sn4c9pL!tm>(p;jzf1iw^_SG& z(ohEx6D=+JotUIzcXKl$Avd3hP z&koG)%wC$kBKyMZmDv|%U!A=t`|0c#vkzy#mi=b-(d>7!-^>0o`{x{G&d8jJIgvT3 zIaxV1IjuR%b1uxeDCd%#%W|&Bxhm(ToZE6X<=mCCC1-oi&YWF2Pvkt6^GwcjIq&6s zo%2o3cR4@g^0`8;m^&;tAU7y?V(z5e*xa~WL#{D5J+~;gICo~QJ-0r$F?V)ub8bs+ zTkbizt8y>Py)yTj+|{|a=H8LJId^OBj@$=wcjxZOeJuBh+(WrXbC2i#ny1VgofniB zoEMrGkr$Jv%hTr>^Ahs%@=Ee%<;~7(&TGkQ&+E)vns-(n$vZFag1ig!uE@JFZ$sYh zyl3-X%sZU-THc#^NAo_)`y}tPyf5;;$~&GvGCwZAG=F~n;{2ugXXTUpbMw#7zb=1$ z{_Xj9CeUa`Gyd(-yK^pVpiO*c<(p1ySY>ggM&KR*4y^jD_8KK;n_x2JzN{gdgR zPycHA@q&ngx`NdOw-!85u)E-qg2xM~k-jLt$W&x0y0hrMq6dl| zD%vw6bwSd}HR%r2(a3rISmeOQ)6^OOs1eOVdj;OS4OJOYNoQrB$W1rOr}U z=~<<#N-rzDvh?cG>q~Dcy`{9b^zPDar4N)oRl2YAh0=qihf7~AeWUb9>1U-s*){f2 z_ObQ=`vm(WdzgK)J=z{)*V(h|x%PbfbbFz_*j{QcvzObe>^1gT_A~6)*&nnYDPzl~ zloge=l&vbet!zu#_OhL2yUHFZd#3EUvggb8mmMs7vFz=#cgsE~`?&10vM(IN9f6KX zj!;L0V~Qijp>yaRX^u=sv7^jU?x=FqIqDsa4!5J*K^&_cH#lx~^g7l#HaIpq?sVMk z*ygy$vCHv-RH1@{-ES zE3d3vQ+aFUZIzoU@2=cdc~9lNl@C=uTlqodHWs*kHatNOC)>uOx>R~=CuRXw#@Tdl7)R-3D> z)ydUq)tS{5)z#It)pgbN)s5A&tLIkFt8T4suU=4nN%fu8`>VgM8CR21*Yo4ijuI8nhBQ-~Bj@7(d^M1{THQ(0!Q1f%mZ?(9Vs|~Hy)f#KfwTZPUwdu8) zwb`}y+WJ~|?VQ?qwe7VFYP)L})}CK`MeXgichqjK-CDb&_TJk2YagoJQ~Ox$-rA>Y zkJi3Z`(Et_wI9`fTKh%qSGC7$zpeeD_UGDPX9dklnpHPTnze4$)3e^MQ`b$cv(=T> zIqE9ws_Po-TI$;C7SwguEv#EycYfW9x>a?T)LmY8W!>F%57s?W_jujjx@YU2uRBn8 zsP0(Z7j@s&{aE*llW{7YD(5h#pVQwN=nQv8I;S{eoQcj9XSy@XneQxc&Ty7ETbxUr z=Qz)Iu5hk$UgEsgdA;){=Nji)=dI4I&YjL(&Ig=(oR2y8I-hbLcE0QU(fLa~Q_t4( z^{V<|^?vmu>&MiOuh-V=>*MQ9_2znOeR6$jeR_RXeNKH|eN+9p^&9G+s{hnA%4Knt zyLwz#xNdOW?CN!`b=~3G;o9li<+|VXpli45S=aNf1Fjcchh49_zH8tbR1NBe5e=gn z#x#s?2yB?rU~ZV!kkXLekkgR&|7yDTuPDwt4B*~HFe;6vDwRq_@P;uGZ&3mUgOZvU z?-EerFqe1VnRoV`*_qkhnc1D)8FXhBFH{edXi;NhwBAuX94}RaF=|jtHCkz6i8m}M zM-OPsg{s{XPk;G7|HJn==lKxt5Oaw_0uTWrL|DXX!XefW>xm6S1yMz8CJqvZi6cZE zag;bgoFYCW&JbS^=ZPtd60(3SB8$nDWRNT)%gK#o1-Xf=CbyB>$r|z`d4@boo+mGnjpSAG8hMv& zr{bstsvng|4Wxce4W))r!>JKe8a0~Ap>nCWso7LMHJ6%4&5wP;7gNiqLTWjsQXf%& zrJCvP^k_PdhO|TPqHE|{x{f|d*V7I3WxA2RN?)V@K{wGYbSvFPKcqY8$4nxV%#2{t zm{CjyGl9uuCNZ;^*~}uQkXga3WJrc(5Q7;HBQrrJ!bF+1%zEZe%*V_p%uZ%6bAUO- z)H03CO{RruW!jjBOb64+JOf=p56~O*0k420FboU_Bf%&z7G!`KARl}PN&pR9fCFwI z0vY(|As=#JY4Yq=h!6#rR_zTzr_JM=oFgOCPfOgj)*A!QwE9~0hs(0OjU0@&B z7xssVa3~xBN5XVC8jgh-@C}#+b6_sagL5DSMd*cosKYQUg%Rk$D!2pghI`=wSPSdm zF?a$tz#H%(?110HA7Lka#&%_Uu)W!M_9b=zo6e45$Fbwt32Y`iiJihuW2dukvNPFS z_CwZS|ID6cTe<$+IPQJU&8_49$Zg`PxvktTu7*3z9pUP@qug=sJlDWo=Kjuo#a-tf zp)RN=>W$)2fAlLf5WRv%qKRl4nt`&>TPPp>9=(U=qlKslQ3xOwA*3P$nP?S?qP3_T zZ9x0car8MlhrUFYP$OzW-=Jo68{I?q(PQ)z>O{}@?tCvkj*sVu@M(M&pTpfUgmwg%IkcP5A&t`YTo9f{93-8-@qT>FY_Jlf$pj96>iPF)qT=^ z)7|23b+@@6xPQQ1aCh7b_rZN}Kb(Y3B5G!wc~;`~hB$2~1&tA@<|dcr7l+ z8*wGB##`}ryaVsV2k}vS0-wU4;mi0czK)ylzwj-57qrnE|WD#y!x z<$iLaTp@3l_sIL@8o5@klTXX_@>%)3+#p|)Tji(92xW$nue_(sR~9J+N};k`DOME4 zQ7V-k$}VM(vR|oDYLzEyFcH*#9!bq^dIt{ z^4I%6SAV09R41yL>g(!cHCts>Ni|ecU8UM;nYvCbS2wGlsx|5f^_=>ZdQ<&Yy`#3M zt!jt*Sp7l$S$(QL*OIhXwZ$5**;<9RNvqbjX*;x?+HP&Hc1F9dHEV5Jr}j+ms`t=) z>+$+a`T+g6`tS6S`Y3&@K3;!K&(tUBv-JhKU*D*2)sN~I^=tZV{ht1B{XhBxy&kND05=sgshf+eRp|ntDXmV&;XhtYI zHiI%RR1|W>eE4uouaEpPK3*QPv@1g&#Wd-f7#3X@17?qhPX2|l>9rImtiCJJ4nZ+hyl4iiH zG7pvp-&yU}BkPIvlhs+;tu!HWCvq>+5qWNRvwPZcc7ol{PPCKl6g$;Ux5wBS_5^#P zz0}t2Pwhs#$-ZrWXFsx^*w39_PJ)x{q&s7ran5*Wy7Q)!C&CV8Qo3qb3;M6#^(K*q@(WTLXXi>B=dNbM_y%oJ% cR#fIHW6Mw(e$hg8>-HiOPQ1wf-?Cf(11hSG5&!@I literal 0 HcmV?d00001 diff --git a/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist b/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..74b228e --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + MemorizeGame.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Assets.xcassets/AccentColor.colorset/Contents.json b/jaem/week9/MemorizeGame/MemorizeGame/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Assets.xcassets/AppIcon.appiconset/Contents.json b/jaem/week9/MemorizeGame/MemorizeGame/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Assets.xcassets/Contents.json b/jaem/week9/MemorizeGame/MemorizeGame/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Base.lproj/LaunchScreen.storyboard b/jaem/week9/MemorizeGame/MemorizeGame/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Cell/MenuTableViewCell.swift b/jaem/week9/MemorizeGame/MemorizeGame/Cell/MenuTableViewCell.swift index dbc1ac3..07f4c5a 100644 --- a/jaem/week9/MemorizeGame/MemorizeGame/Cell/MenuTableViewCell.swift +++ b/jaem/week9/MemorizeGame/MemorizeGame/Cell/MenuTableViewCell.swift @@ -5,4 +5,18 @@ // Created by 송재민 on 2022/05/26. // -import Foundation +import UIKit + +class MenuTableViewCell: UITableViewCell{ + + @IBOutlet weak var titleLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + } +} diff --git a/jaem/week9/MemorizeGame/MemorizeGame/HomeVC.swift b/jaem/week9/MemorizeGame/MemorizeGame/HomeVC.swift new file mode 100644 index 0000000..52bc38c --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/HomeVC.swift @@ -0,0 +1,58 @@ +// +// ViewController.swift +// MemorizeGame +// +// Created by 송재민 on 2022/05/25. +// + +import UIKit + +class HomeVC: UIViewController { + + var menuVM = MenuVM() + @IBOutlet weak var menuTableView: UITableView! + @IBOutlet weak var startBtn: UIBarButtonItem! + + override func viewDidLoad() { + super.viewDidLoad() + + /*테이블 ui 구성*/ + self.menuTableView.separatorInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) + self.menuTableView.frame.inset(by: UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)) + self.menuTableView.tableHeaderView = UIView() + self.menuTableView.dataSource = self + self.menuTableView.delegate = self + } + + +} + +extension HomeVC: UITableViewDataSource, UITableViewDelegate{ + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return menuVM.getMenuList().count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "MenuTableViewCell", for: indexPath) as! MenuTableViewCell + cell.selectionStyle = .none + cell.titleLabel.text = menuVM.getMenuList()[indexPath.row].title + cell.accessoryType = .none + + menuVM.getMenuList()[indexPath.row].isSelected + ? (cell.accessoryType = .checkmark) + : (cell.accessoryType = .none) + + return cell + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 44 + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + self.menuVM.selectMenu(index: indexPath.row) + + self.menuTableView.reloadData() + } +} diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Info.plist b/jaem/week9/MemorizeGame/MemorizeGame/Info.plist new file mode 100644 index 0000000..dd3c9af --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Model/MenuModel.swift b/jaem/week9/MemorizeGame/MemorizeGame/Model/MenuModel.swift index c0e688d..501b47f 100644 --- a/jaem/week9/MemorizeGame/MemorizeGame/Model/MenuModel.swift +++ b/jaem/week9/MemorizeGame/MemorizeGame/Model/MenuModel.swift @@ -6,3 +6,28 @@ // import Foundation + +struct MenuModel{ + + var menuList: [Menu] = [ + Menu(title: "Vehicle 🏎 🚌 🚐 🛴 🛵 ", isSelected: false), + Menu(title: "Face 😃 😇 🤗 🤑 😋 😙 ", isSelected: false), + Menu(title: "Animal 🐷 🐰 🐨 🦁 🙊 ", isSelected: false), + Menu(title: "Fruit 🍇 🍈 🍉 🍌 🥝 🍒", isSelected: false) + ] + + mutating func selectMenu(index: Int){ + menuList[index].isSelected = !menuList[index].isSelected + + for i in 0.. - + + - + + - + - + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + diff --git a/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/MenuVM.swift b/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/MenuVM.swift index fc38a5a..95e473a 100644 --- a/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/MenuVM.swift +++ b/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/MenuVM.swift @@ -5,4 +5,17 @@ // Created by 송재민 on 2022/05/26. // -import Foundation +import UIKit + +class MenuVM: ObservableObject{ + @Published public var model = MenuModel() + typealias Menu = MenuModel.Menu + + func getMenuList() -> [Menu]{ + return model.menuList + } + + func selectMenu(index: Int){ + model.selectMenu(index: index) + } +} diff --git a/jaem/week9/MemorizeGame/MemorizeGameTests/MemorizeGameTests.swift b/jaem/week9/MemorizeGame/MemorizeGameTests/MemorizeGameTests.swift new file mode 100644 index 0000000..9005533 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGameTests/MemorizeGameTests.swift @@ -0,0 +1,36 @@ +// +// MemorizeGameTests.swift +// MemorizeGameTests +// +// Created by 송재민 on 2022/05/25. +// + +import XCTest +@testable import MemorizeGame + +class MemorizeGameTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/jaem/week9/MemorizeGame/MemorizeGameUITests/MemorizeGameUITests.swift b/jaem/week9/MemorizeGame/MemorizeGameUITests/MemorizeGameUITests.swift new file mode 100644 index 0000000..bbd91f1 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGameUITests/MemorizeGameUITests.swift @@ -0,0 +1,42 @@ +// +// MemorizeGameUITests.swift +// MemorizeGameUITests +// +// Created by 송재민 on 2022/05/25. +// + +import XCTest + +class MemorizeGameUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/jaem/week9/MemorizeGame/MemorizeGameUITests/MemorizeGameUITestsLaunchTests.swift b/jaem/week9/MemorizeGame/MemorizeGameUITests/MemorizeGameUITestsLaunchTests.swift new file mode 100644 index 0000000..b9e2a05 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGameUITests/MemorizeGameUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// MemorizeGameUITestsLaunchTests.swift +// MemorizeGameUITests +// +// Created by 송재민 on 2022/05/25. +// + +import XCTest + +class MemorizeGameUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} From aacba10ea80d574d3dbac2aa66faf81596f3a07e Mon Sep 17 00:00:00 2001 From: xongjaemin Date: Wed, 22 Jun 2022 01:15:01 +0900 Subject: [PATCH 9/9] =?UTF-8?q?dev=20:=20memorize=20game=20=EA=B2=8C?= =?UTF-8?q?=EC=9E=84=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserInterfaceState.xcuserstate | Bin 26806 -> 28572 bytes .../UserInterfaceState.xcuserstate | Bin 41442 -> 41432 bytes .../xcdebugger/Breakpoints_v2.xcbkptlist | 18 -- .../UserInterfaceState.xcuserstate | Bin 62479 -> 69113 bytes .../CatStaGram/Base.lproj/Main.storyboard | 2 +- .../Authentication/LoginViewController.swift | 9 +- .../RegisterViewController.swift | 4 +- .../UserInterfaceState.xcuserstate | Bin 39430 -> 38157 bytes .../MemorizeGame.xcodeproj/project.pbxproj | 42 ++- .../UserInterfaceState.xcuserstate | Bin 41849 -> 89741 bytes .../xcdebugger/Breakpoints_v2.xcbkptlist | 6 + .../Cell/GameCollectionViewCell.swift | 41 +++ .../MemorizeGame/Cell/RankTableViewCell.swift | 23 ++ .../MemorizeGame/Model/GameModel.swift | 41 +++ .../MemorizeGame/Model/MenuModel.swift | 7 + .../MemorizeGame/Model/RankModel.swift | 39 +++ .../MemorizeGame/MemorizeGame/VC/GameVC.swift | 247 ++++++++++++++++++ .../MemorizeGame/{ => VC}/HomeVC.swift | 25 +- .../MemorizeGame/MemorizeGame/VC/RankVC.swift | 62 +++++ .../View/Base.lproj/Main.storyboard | 201 +++++++++++++- .../MemorizeGame/ViewModel/GameVM.swift | 34 +++ .../MemorizeGame/ViewModel/MenuVM.swift | 4 + .../MemorizeGame/ViewModel/RankVM.swift | 29 ++ 23 files changed, 795 insertions(+), 39 deletions(-) create mode 100644 jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/Cell/GameCollectionViewCell.swift create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/Cell/RankTableViewCell.swift create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/Model/GameModel.swift create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/Model/RankModel.swift create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/VC/GameVC.swift rename jaem/week9/MemorizeGame/MemorizeGame/{ => VC}/HomeVC.swift (70%) create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/VC/RankVC.swift create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/ViewModel/GameVM.swift create mode 100644 jaem/week9/MemorizeGame/MemorizeGame/ViewModel/RankVM.swift diff --git a/jaem/week3/CalculateGame/CalculateGame.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week3/CalculateGame/CalculateGame.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate index bab0be0eb8b53b841c00478f66a2b03c34274b7e..6a4befce941aa96952a7ff6da8759e7b1dab82ef 100644 GIT binary patch literal 28572 zcmeHw2V7HE`|v&Ij<|pzdxL-sO~_7=AtQtt2n0}BF+#*B5G3HJ+TL5c_po~j&Z=G3 z+GSg7r)_OrwR=xH)DEp(_Ipll!VuoRecylI_Zz>@{($7(bIxE@qB6`HHiwN!l?*q8a17gQF1DSQcy}tMX4zb zWuRtKbyPhyi<(W%p&F=0s)=f*S|}UUNp(>!s+(F&^-$+h=TST*Q0G$@QI}GeQP)t{ zQa4aHQnyieQuk8#QTJ01P}`{osU6fq)YH@+>KW=;>N#pJ^%C_e^%`}MIzqicy-B@A zeL{UoeMWsweSv7iAQo{*g2tioXabsuCLurMj{?wS6pX@9IEp}#C>|xCX=pl>$cS2y4b4S%G!M0+HsnC_Q9D|IoXCZ`Q6D-F@kl`Dqt)mFbQQW9 zU4yPg*P$EHt!N9{ingJ9(7otBbU%6kJ&tyvC(x7V8FUDJfIdWDpl{JJ^gTL-enO|w zZ|HZ7Fvbj?fG6Sr9E5{$IF80sa5A2XQ*bIy!!oSHIoOCxa49z9N?e1jcov?Gn{YFp zi|yEf=i_#~2)pnyd>+0CUxqKoSL5sPP55SfJKl`%##``Kyba%nAI6X1NAY9$6}%6> zieJO~@d11gAHwhAkMSq?b9@wkg}=qe@OSuod=j6+KjUBUulNs|q7lu}5_%jxiT0y| z=_opmj;E98RC+osqm^_HolDQ8^XPoqOjpuXw1uvwXVJ6iX1bkTKs#wS-Anh;E9jLp zPhUV^NZ&@^PTxV_N#8}UrTggtdL6xhxAAE$Mh%kr}RJhk1c{ zk=e_?GEY^=AWEDI3U6W`o#ZHiV5}=E`2_D%LJ_HFhZ_FeWp_I>ta_9*)mdxHIr{h9rR{gtCRhKuFmxOgsso5Ce>NnA2F zl}q7Lx#^sm({MAmEUu6< zw}!icyMYK3bP zocn_Nk~_+M#eL0v!+pygO2Q-wk|~m8 ziB_VM=p_b8reuaBSCTI&keDPD5{twtu}NAbZIT5Nr=(NTCFzm4B}*mCBr7GW>XN$p zIyw$few05IKuM_}Ds)z*Rldlv+}!|wy_~T=skOuIb`Ma2)MOs>^Z*r1g#fOgC_cp| zP0Cah<{OMgwJcMo)5|n!MWM`~QYdA{g2H@dfl+JJ<(q8M;8J7JELYDWcelOOVRW_j zbvn9weUKxmDOCC&rjedZlV&YB>0&MuTrTreiH8o zKmPDa%182Yn>3t^@9b)GEv>cBE3o&Jy6c^8=e!O_%is%BkG<1jlTICcAhw)k_cfj6 z>|J1WcC~jn;Ehe1I(*9F*tSY*jlaoMf9-&~g_u6gJIB1&R+6z);aE&lTp{uL6 z$JNo{=&?z2hYvRLokFM56=~F3nMRqZlxa+b`LcriLY=J8sDmBU8P&>6qfHtyG;Now zW8Sb8jkJl?#O^;up)M((dL7`c->L8_sbJbCLa;j}G)2cxB)I6VvM3`n+ zPLlfXipCG5(sAP_Oq}HBA0Q2!926W98WtWA85JE98yBB2B{3;^YD#mZRopnw-oq?v zoxL4!27$1V;tO#*$qofpTI)(JJ&q+#SD)L{9&nHxQ4gGCJ$YGboLy-xb(`(ZE}K+Z zX*D@}+`TYBx2vno(bHIIEhKna={aTTAcPA0a#vrk8(v#`mv=bKcK0IK&B52u-+7Ls z*wN+avG;<0V3P*<<~DeirABco#Xa4gIl!z++m^NX+Ew;-&U5teh^GN%qsH-gJRmN^ zPYC1#qzvG3q?8Io&)+AFPD`IQT_(>^=nc6gl~oq&tl4wy^IGRGT-4#}7HQEYjZ(ll z#*;%NXBe-5|HLnPZG0WfdWOLzjiU-FJJmt8Qho5-PW4g_su=!u!r%D#V4x8*d~<@r zA*bB%)&(lZa%vv@wZo@2n{@JUY!nZ|Vv~lzQy*y#;qD=&O0CiAyj)=RQV)@p0!9XR z6OGE<3xr(S2EUy1ov;hzVaem_$wZaNX_>OT z!VKI14!gC-;ph?%zpk#-XzlF*(uSpvd}W2h_iz(`U}Z(xOp^{)D=RA08D)j}YNJeH z(CTGIU8YV`q*g0b#)765%1jjym{MU-tbzqr*8m<22R2ZhL*`LiX9nIB$dhr^dZWRh zR^`i7Mx9QkP!S5xH0fmqlUk9lRwxTpYDJ@$a5EK(RfiiIl}*hpwzqSlQ;sL|g)`A=MQSVTk@O)5%7otv3gcl<>sKU$8P3R6#bDst^HVl+k z1=izST#Q>lIh}{wKr!vcEARz)1E``8fC9Q3zYNOfeo#DL$8X@b@H?P<{!UK@wK9|r z2Q{(+l*hSrE4`5Jpu6a9`X*2hx6!ZC$C-(Y9}@uTUJxjF)l5C8Z3~#iptkja%61L2 znYjm)u_u_ll;;nbZs_iyD(4fl-_LdWlpc0&N{M%b{|C>5^|l zgy>Zi@~AQ@{cb9sDgZuYq>3mLaGw&Y6tI0VAH)a4afk9@aO@Fx!-iJ?GpeMjC<|2$ z->e|6qxdGinQ!M8@C)HvClTLLU_I6auBDasCC+v`2w3sa;Me>f2Z-4~XjRwi>~Quv z9qw7qw%!F|v(heiH`s8lo}p%gJw45u>Ya|IM#sFq_I4s_g3OLiSI_d|9((tKp{Kq% z3_@rIa7ArS@3`ZA+R9vNK9#-!Sm``qz-^R+kLF|eSU!%A-$1of3xMw~q!#fBdBRkK8a6W4>+z`>8AV^nU6pei~6uiB>pfpM#(mT}NF{ zt+7cX;b;irTZreN#?js5aDy=O=mNZqpI#M+w~thr(x)lsIC~wPp2YwHg3{5O-`eVM zyB%#nFOC*>E7AHaE;rz*tE<`VJ-JfoXYPMLa8GH(3D+HQOAf;k{j9m`6b zUA_GinhWh6t$iIZN-@YwDRmH(tBGU5a4hS#cM*SvXBO)GXO)5bPQG#gMhy%em>e{6 z9FTe=#;pY%)-6Q`L1FvrgTHYY29Jyxh>AXY@Zb@H*OiLH}BG8Bn}f?B^0 zw4ISqM}7msBPytz*a6Gux$He{QpO`-qCnlsp~|UR@cgs@BUlJ@b`E%YuA}-v(A)JdPt7Fo&cq-M(xEI=iJC#kEJ9sqCAtiJD+Ay~c?|p~&!fHQAbK6W z4?dKm#D{_@?2qHcbqun5gKu5l=Tk&Vx{10O?46=z-5sLEQq|Yn;p}n{W+?i%2;O_C zavgOGbt|E>zu4X;ooB1+3Kw}wQ{JP2aT=3 z{kz(Slu4VojN7R@JX*O=LJcYe>!{laZ}2gIyQqy+`bKIkZ1(`Qj#^J`;1#@*SMh3I z!)rH!`M8-P5@{>74OBiIuLtQEj5qL^{0u&8Jt!|vz>1#Yv-z2@nmn== zj~Y*89qg_hSWBCS@z|t^0JgBN2lVUS3VUBy>jJ9?X;A6*uy!T1DzPtdly(94vUhYW z_l;6$sS{--*_Q>*jy4#`%K~GekLTS;AqB|g0e#TpUU;5*v7dT@&*5`Nl9PDGFH^4o z)AydR)zQ0-dYRfs<<^02?dYAOQMIX+YNcHv4z-^;u$IpkJ07AAgYN0=nBUW5U%n1D z?R6>_>;=zP5Mo36rAn`w9-paEtXd_-e?ct!cK>baeJXt&^$w8kd%TG+=1bOrnEw!D zzmYHH!W2y|pG;xGa70&E}MEt<>=$lXV8jyAyL z)A}I=7%g+{3bsq{EAV@8~ z3GpDY6pewOO696miFw_59&YYEN)&}+sPujm&A0TUSl&jqJfY9+=mD1r7|lJ7wnESv z+Cd~0Ed!oKSV*bSErOat9oU2tQ4&f^MQQw8-pIBYfSL+T*hvG=UH&)K@D#L>|` zsCf8J7Bm2IvRdBR53Ws*J2T3IZ$uh-1qG4WUV`v-%R;$S`T)vCIs78NV*n^L5vXyJ zM+6LZuW;BGIod?I1Kxz$HG^-wQq0uj>Krsfd_xzZYASszGNEEr0@iRDDn}K_3`$BB zvhZEJi|^(a^F6$q@8$dWCHzu;*;Z6TZAP`I4%MSsU_Q@*PmQRFUk-BiLjEHDV*V2T z7Xi`yMFI*JkY|yBqC5ff0$h^4WQ8_qdVyoUs|OCPv)kU|bi2AdQnn88+`AE?K_<%H z0Ix{)sKMadYZ(4A-sqTb@9XFtax;xo?%~KnhO9f!6nv69vAf6F36_QLwI^N(&Z*(H z0ca$^0=zIh3n9j5i`l-+*$H?^7)mc-W4w?3`NneCJ)la7&Tj$jADVb)4RdvNgIk$w zhDDr!=uGjQ%0kpZ@MIBxPCx48S9q8%=w5@nHzaJ)V(P#E>OpRPC4WAj?;BtVI)@5f zhn9l-VmZHxKbJpm9a=%9p;bH&XeaO~%`n`M`qf0f`JCfmguqvFNM?IG=mvdljybRa z;@lJSF6bV?tS&^Cj)HU0KXxUz+b~(%RdLde*W@^#}@gv-iwEPAS#X z8p+O=gBj=1pkjc;`a0SOp;;ijQsY<*ws$XJGzobOThey46O;<{AliW*;;-Yc=hv)5 z4}+vV!`}dMwSgS(WU>kQZZBxB9w+so6uk7H5$vULIrvwdt-dGXCOw7r44~cUY5qq3 zL4N(4=vnj}*b^^M0q8}rDqcb_qgT*A^eTD{?MDaDLGX&Vc>^;dU3v94(V?C&9~d%m z2N9VFwh!^9o=p(}Zi4hf4mcX=E--jU93A2{MIfz#-lL0wG+^!9xl5@|z@=KJ}3 zd5XV@-_Ae4Z{zPff)0b3bOaXn26_{{h2BQ*pm))G)MkRX-epy|T0M4s%Mgx>Bp5Qt z0c%_?qAxha=g_kgOueDzBo0F;w`#s`%%OMu2L2X)GryI;nb+B*8H3_r#Ab>E4mo}X zHAC^pK0zN{+g#Ca+0B7{!gAd0Bus_+(v0j1m2}^Mxp4>p(KjJa) zTll+e((1oy+VW+@-$-UH&N2jtHo_czh&_PAsNCTX#8weFQjB_zz}O*#8A-kaIEpNg zL`nZr`$ISe)*XxX;dm?|!JK)pwGfq)_c*}c1))=*=6m>!;#3lG5}1C#1O{*-fs)?p z>Xh5NK|F#MH&R1YgLUO|#K>ot77XcdqPO+z_HvPR`_5}v?|(=_prRAO=hY-3o-21R zbmk^Q{->_ll(*cZjtSHm+A74+z&Cmjruw!C44&RR-1UnGIjNAD)`L_n}4lp9$7R^ZM zFpsY{UKTv;QK_3Ksi4Ru=Jl=}uD|2Q@iUN-gLmO4@RRr{2wy&p_weuX@A2>RAMhXY zAMqb=!q4L8@bmZu{36~9I`t>~cl`JK5B!h(Nzn!k5S=e%{@t#gUUx%-%hhSJlQ12G z#6?@S)L82xp``LI*U~O=y}q!C+1?F4A@VYxM4-Aru^bwpu@F>gaDBKSXacPW3Om|r zUELLgLwL`@2>UMul68VF!|t;l#r->s_H2Ye>=6jXz6rtDxA{-`FZiSU*ZjBqaoAV< z9?;19_yhbQ{)ps3^?;4P#NI(Zk~kxHMfuP8&!x0YTEAk2OsP<6<(W#oN}*L7GBp~V zZhE{-uhz?TnVBl3R;yMTR64!E6I9nKRC2XmuheK&YNZzP3BVbt&y?$Q8m-o#Qb8h+ zR`1E%flw{}6n_R{(&Gmnz@HK=NSxOfWM2H2{O5lPUn^I_1awMBHqq*}S}`?f4EQ9w zL$1>6lzNyb$#XEQTIGZEwFlBy1X7*2Qng%d&}V8DnQEOfQ|no%Qm)U`>r@bg*J~9p z1X(Do4j!ukvI0D=kVBz{gb7%iT&vLNwJP#i1i$NM7jlpW9X`10r z@jvnF8w%z$$kYZ|ucD`6j;=t~u96j38$6m1J)WK*x_C#35RZX8Ku;v95Apwweo1V9 zufc2+-;PjjXn#7uCp`p&`Cmtf4>|35HzfXS9 zX=FqJoZq9(n9=%+_b6x;tp@=@t7#3brF8;g1jGu66Od#*ZJ;x$Gjx`K#tF!eZxB!@ zFP~lEnD2Fgd7S1xkJzARsX_Dw5y#h@8fW_gPh%gn0=k$$E2NEd5p5FCcmYii&_n@E zS`Ti)&3GM%1QPK&B8r8mdho3g54uOXcsznN_N5+9157h=+*T0qePiV;w(fZ_xcFQ5bgO%YI{fRZ36N?%7`Pp_eG zpl^iSqnqhl$UYFDfT4#p@T9&vqnojYP)8^K@k6AAuxwF9&+Ku>TpmUhi|L35wBHrPB|)KkPc`vxOZ z_HF0}dJ7@0jr1mZGkv#!rV1!UK&b*sTTgF=EUkO!dj*s(pd10^3+OVkKi(*U#|5MY z)sNgTvG#S78J}%_j%Z_($_L#>B#JO{QgUCoqRk9d{28k`1lc@!$+h;L zc1N!`L?I;pK=Y|%jBXR34R0+9*L`SDKmDqJRQ&qG^Z{^bfn$q4NFSmP)348Qd*%ej zf`GIF(g;W`pc((cjZDmc=FCs2U8}eI&h{<(U9cqRx9N8T1TsrMK)*-7FCc?}GI@Eu zxaxXwQNV&muW_W4Pvm8v(V#twd1*dMID606(O=MC5_RFvn~m*E8*?JQ1$|9_BPv9` z2gm8}2=<;3P;Ni{y?`KLVmS8xBx3K(|E3?&H??2rUq{aM41>sNoWOtyx%{311~asP z3IsGCCN0tc!!Z+qH!%`s95bE)kzXhvqkxJ8WCB~2nZWon0sIm8CZJ*gc~TMotLCb2 znxRYtEQJYUAoN!vpwfTKNc9aC&!mEF$|NvTm_#OtNoJ-pz>muXR3RXU8dISG+?1=6e^9! zE*xpDX;lXO+2$JDL4lkEuU*KPL{RmEpfoBCfKsdB21}*^%s?>r0F)jqLlu}UA{ zjWgP|84J`1%aAD(LCq$hhz*(vRuq`jI)z%PR}hPa*fLserre+~fB~BcR*nJEC&sct z4QDS3AY~3Ms#1j1Ft{iMv0)XEZmv`-6ks7i|No)E=G(s-5meJiP^wIwN|CA1YrtRN z1*HL}LZ(t2s+MrW`A*j_67lcd;EU zs{zYUr_zGy>oMY$`b@F`U;%?=nyJufz&2K@3>vvkqtb$J12#sP382n4LzzaVX_Oh7 z3XT$kN}j2K|cf6GjlMB4PX*$)q0gyuT*7!6gD(EuaeobfJJQ641r}$8*8k2p7Da zxr4crxrObmM4}tnm`Qn&IL{R4sf%=a-`zbT_T`M|B*Y+#}i&+_EYH_n3tJXn0?Hv%xeO=TtHU}=xPC7E1>H)Fb9}} z%pvA5^SXd;5YRIM0!9CXfFTQJm=?^uEo#A6*rd&WuML-Bb###{5_U0uIvnN)BFw97 z($>Ej=EwlbXvm+6kgu^x=l;!*^Sir^VA8>TCP0=ij1w+QG~ z0o^8`+XZxofbJB~T>@IWl{o`h&5$w4VwQ$ao8gm$9VbB6SR}~9Ndnp+psny(M?P;8 z(0xQkM-I6|L}OS!XtG1%%;=X8Tr)enAe9h8SqWplE(BL$p$m$U*xSJ?7B^;_(yk@q zRd#STj2@-KbG>;mK?>R=jvmExA<5&9le^~6cRRpuHhL{f;W}IeTn`x&jCiL>9D>{- z`A6%`@9SOQ@dA(D4w8~$a&@#pyivS`FYckowE|g%@YefLeC~ne@tUxq;5A{x1O%y2 z;)6(t0kBbQH2D$GIsvU0fAj056QrSD3}F-5WVjH=5aU%zx#MuiTr)7a?(+AN^E2k<&JgD8+&_>@Mnc#mh+;cjX3<&}9ny3#8WpE@Ci zU3??y*i15#o&^^Qc*X7=K2qI$*o(8r@O8{%jo{m2^VtHnP(b$!=m7z3U&j`)CKe3& z2L-f4fTTfrOKE|3&#bL>k}n-Q_<(p)D_vbmg#z5DBnzHIPeGp?YyfUgCtS!$oavqO zV9ZF|;wrM|U`6i)-AZ&euvWI7tfrO)3-ny82odc&91?_6^#?wo;+%SsOc-wXFHYn1; z8bSx4EWPIAJvS5Ha|?Sb`4P~50UZ#31Mf+c#`(~?pIt|I&wzjq^|Qd84*x~ovzgsW zXzXrwnt(t{+dRN-!@%E;Kj1O!gt4+Qj~fWX1@v4B1i(5C|W zYzw>Bi<|pEh!PQcfZ*omKHNM?M5srA9{c~O>A6QPcY{jsS4HGMMa>TgYQmv>;X}<& z2x@*xZ6-eg`jXdqegR_o?yj?6v)>Z5{6;`u^|QwW^z~mv%kSBf1gm}^XbGJGEl&}& z{B|s~WD9(V^&9(#h>K@DxOkl4;wgfQ-;P3NZx@c`0zs?eI8MTi}o2Fi=Yy@C@xw+zxq%T zCh`V^HZcth6Egzl{$Jyme}Xh)X^(#|JN0l(E)CFyb{YZWU^g9m< zIQ#;%4D+43meT`Ta)7UY^m7IQQ-2LDxomDGL9l6DF45F50tDmo0l_dHgwQ_9&o*>|fWYzyvW8xNYo&TD~uv!j_j7v$JQXgZd2SYTLY*~o*JxyH+1Kp_?jQyD`69QMdx?9QdxhJ_y~@4D z?dJ{%I90%arRf5mCgAA;mI+uc;0ysP1gzY`9pVlXq~_k>-sIlm-savRD6JB(nqaj6 zF;}b=uui~w0UO{^${Z*)0-=9!Z9w*l_*t|ie4dd8r@N@b*-mQCK@QaL7NtJ9za9w>p_Z{~=_XGE% zfM*CeOTgIz&RNf$;(p>zb3Y3>SHLp`oG0LXBD_NZOGf4dd27s(tKlPG)RTf^q{7?i zS5@8KTfB-9V55MG)=B&&0o*A8n*>}8Ya~k}l?q3$U<7Em zBMK%jxl29m&l?uN)pn}`?$e5emD2vawFtwLkd~MyS{GDxy!V78;gaYvCeId0VkL1N zF<2^K$TMiA;*&8I?;9n?qN-!fS%Z- zuC&e#zRAH?2Sfd5WcO!5>CIpQJ2yXSU}hd4z)MLjK2QBGxV8yheIt{iRjy%|A|P9T z@GZ#-9l98oL1)(Y`2Wyf+4@>pg1wbq{l|$7(0>p)oJ@qUFdeqee zBR+d$2WC}{@f2FmY49HQz?@3>+SClZc^LY@O>ZKf23nx|7|*G*OV3q69W*PHV(g<< zQWsKJK-rz!s5_{;pfb(iq&%GiDl{+h7N@%yYB4_tcY9w!ufna~H{crVyHGpmBe+re8TtaVI1VS^ zM5wT=#@SF=xdPYW2JD0jL4EjQdmZSa>rh{0`ZZEt%G%jY+iog{ew^O z0Y1hsRU(7?*TB_MC25jLl4%kUxxm{h1Z)QOCg4f|S8bHYsWTFVL`iNr3)lj=&$wE^ zR^N5!DSytH2)=;R?av!QcnKP%{PRYhYM}qLWi4DV^|VaP8z-*%pd^b5kYr182!9~= zPZLS;t+(&WI3(a2PtKcUrX&wghRkF@GLs~>No%3T*z$OLZ@hAbLK~lN7R5v%=yg;e zupgg`RZ=V|h5R(~tY1uP7Us8+q+>JHpfMk|rwq%Z^ zLDDE`k~G7mtZf3GBj82>Hw)M%V7q`@1q{g)!@^LKd7hvUZuqaq-6ZWIN3d9&78LvZ zuZ3GBOGKDZ<@moD^pva+!9uF||J@XN_2zQVrB?q?4|eexqvoBRu?M-EOp z_Bi_;c-v00KXH?x5_uUI?Kgp;@*{T=^u#bpoL_`rlwXoxhM&RDQg0zjysk_|yJj z{z?8b{LTK2{!RW)|6czk{>%K&@n7kGuK%U}m-}Dof3^R${@458;QxsK8~$JW{~iz% zkRG58&<5xOG6S*#i~**Al7O;+ih#-hN5Jxc>jLfxcr4)YfL#Gk2D}h(IN+^-AEijj zNIB_v=|riYG*zmUR!S|>nxJt(fk88Z@`Dxzy%zLl(6OMC!8kY|I4~HBNC$@nM+8R& zrv|47PY;#{D}q(QnqXaUeQ=O@VaU;tuR^{FIT^}?P7DnSO%I(O zS{2$9+8XKzZ4Y&Zc8B(a_J%GAT^71F^x4p(VdKI=!sKC!Fjbf)Oc!Pdn-P{BmK&BA zRuE|w28j`>UT@bTfv;l^-l zczgKD@XN#3gx?T;Q~28O4dM5O-xvNs_=Djug&zw4F#MD7&%(b9|0?{O2x&w_L~2BO z#PkSxgd#!}p^4B%7$O!&Y>s#}5=G98TpqbT@}9`YBAow@1Go{c-eX(O*P=75z>0 zu^1dPF-96QIVLzJG$t`7J!X20JVp_tkI9V5iphzY8DoxVi0O=38pFq28M7wl#+aL9 zZjI@WSr@Y*W>d`FF^|VQ8}nMsftdGWK8X1+=Hr;JV~)j~i1|L|kJw4E{;|^7$+5w) zp|Rny$+0Q1X|dB{Ww9Bt%2;)5Zfs5Lg4iXo=f@7ju8(~n_L11fVt2(p8T(x93$c4+ zUyj`u`*G~aI7yr|E+j52E+Q@}E<0{^+{(DC;Vmb`#$c+xKnYb z<9>+`ik}*lyH5*4GA|T+>)>!tV)xOj$Igcgkf`uAZ`C%Dqz_o$~mUClUh_BNL}4PD|7$<|Qsl z>`uHbadYDRiMtY?Ox&HgC-LRP1BpkH=%k#a`lQaJB}rE%U6*us(o0FNB)yunKj~o7 z;iMx;ZzjE+^h46gq@R+0PWmf=+NnEF$SG$kS>IwdwGJ|!*1m|{vPNpYquO6g2-r7TWyr}U*PO8TiX1z1!)V@mZz;qTa|WR znvk|S?ZUK+(=JWBEp21kzO*mWen|IAk55la*QFcMXQXGR=cebS7o;20tJ9m(+tZ!t zi_$yOpHF`+{f+c@(~qWqo&N1K(=_X}x@ohf-8XI5v?r(So*prM>h#p<>C;zFzh?S% z)7MP@cKRt9En{S?Od^xYa%3i1xy&r9l2yx^WbLwU*(I_oWgBD<%l69-$zGSeA$v>q zj_f_z=d$Cn?_@v7PRdTpevwP$=g9^6Rq~tUx61F3ua&QpZ~~{u`}bPj6)f3X1tT}e#VCx-)8)n zaVq0<#xDw_U=*BUykepvSP`R0S11)~g;t?gxOS*x6- zY*02U=PFy34&_SaEy|6`CzK~tSQQ61a4S@ERP$60)dJNbRhMe1>KxT76|Y*Yx={6$ z>V!I7-Jot)FI0D`yVY)WpZa|D1?r2`m#RNif2TgJ{#AWOgEZqb6E*&tKuwUQK(knL zt7e^MqvmeSR?RNW^P0VyS2V9_4r*muowiV0qAk}}YOUIO?Hp~RcAnO$Jy&~$_6F@k z+FjarwcqO`x?o+1E>st;OV*|8rs?E5aAoQ8bOk!2u2}b=Zny4f-5%Yux&yi+y0>)i z=swhaqWfI;rS7Eej2`J3y+l7jAEXb_hwG#CvHEy@w%(#&qQ6>yyS`t)UcX7dO@FWc ze*Jd+bNYAnNA;)mzv|BzkbyBs3=<4~hG;{aVTvKykZPC)nfwZa%3w598I~BXG2Cw0 zY1nHxX!yYJvEehrmxiwm#|+;Ye#oRU=}b1WC38_`S7vwS-pnJJZ)LtSLp>vJM&XR2 z8F$XuI^*6M_h*gI3eAehipuKET9tKK*5z4OWL=eYQ`VlWm$UX~9m+bA^=8&5S;w+| z&1SMC*%Pw;vZdJ(*-_cC*$LT6*;BI%vg@-~X5WxKki98;OZGk4+p~9M@63KA`?c(k zvyW$=$w4_xjwEM7j$e*6Cn#r1PI69a&a@nPjxtA+qsuYml;zartjxJFXCP-+&TBbu z=6sg(WzN?*$8x^Q`7!6GoS$>K-0`^+bK7zk=l13<$$c&N-P{jyKbo02(=@YmX8FwZ zGas0_W9H7hfV}9uxV(hCbMjW_U6Xfh-gS9v^6tocDeq9;n|bf#y`T4C-q(4j^67m4 z{J{L+{ILAU{FwaI{Au~}d}Y2SU!R|uZ^|#pFUvRQ-;@7X{;vEd^PetADbN(?3Je8V z1$P!~Ex4!PzJl$A(!!X+xWa_Oq{2@MPZ%Y}2}VDo)EHz8HAWbtjd8{)#$;ovai+1r zXf&FPCB|}NrO{%nG1eMq85@jE#^uJFjk}ER7vZARqM{;a(IrKjiuM=1RrGGr2Spzh zeO2^B(W#=Ji+(Hm!-PzZq{7mul#e0iiDSo~9&Ej{8 z-!J~C_>OjmgJRmlw4bKXUT&lJ4+rd*;VpX$)1wuN?t5^x#ZQ710|oA z94+~}nZCiTUNHBY*pELWkT6iW!ILiDZ8oc*0S5n9xZ#m?B%jo%MO$sDSNBzowE1J zz9~CicDn4Ba!L85@__QmJ+TJyo@*>ba^Ht6r-5qUy)0pR0bW`oqFlILmm;L`$S4#u8_l zZc$n^7QH3gl55Gg6k2L5Hp_gA(*kA5ElVuREh{bOSp>^vmg_AyT5hr2X4!1H$8x{r zLCYhS$1P7T|1w>eba3Ru5D^UHw^&q-I)8bxmK*nwke| z_Sd{s^KQ)tH6PV{Rr5p5shXc_eyjPzimbGiwNAE%Si`MR)>v!2HOE?Jt+Liw>#Pmd zCacW~l@qOQ>q_gn*2}HeSg*IT63+X)>>O%JG-`_wykzW?M1bB z)o!odTl;?P4|NmklImvERn)cA-Bq`tZd={MbQ2;;uTQFIE?tp2=uq5iV^YwOq4-&B8V{q6O4*WX|NVExYeN9%XjKU4pF{oeYQ z>wlazWme^^S+nNMnm?;^R`)FTtR=HnK}Frwvo4x-$*ikq-Q3vS*wc7k;}wlpH(u9x zL*vbjw>93`xV7=##`_x|Y~0!SNaJITyBa@g;+hmq4Na|09ZkJWtD5+xOPj7~x}j-v z(*sRAnszol()3)@i%l;z?Q7cKbg=22ruUmZZ2GwA>!xp-jy3(%JfV3~vww47b8vH5 zb7XT&b9{4Rb8@q)S<|d*&TPJ`d0X?n&G$Dy*b>+h+Y;Y0r6sv#bIU_554Swpvdb22 tOS4V0$!rSS^|rOP0o!`prn&Ryy65)IUF!7}V)3s;@Hz>-f99U^zW~8+UO@l= delta 14128 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 BlqCQF diff --git a/jaem/week6/NewsApp/NewsApp.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week6/NewsApp/NewsApp.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate index 040e63f310b312fc84241433ce5ac40ef967d6d9..67e9f3033a5fec93284a32d1aa62ce971cab600a 100644 GIT binary patch delta 21378 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≪2; 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 anoS)%)vB+F1l2z^O#9kLY~RnR!v6>QxZ8gK delta 21629 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 - - - - - - diff --git a/jaem/week7/CatStaGram/CatStaGram.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week7/CatStaGram/CatStaGram.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate index f015b3eff32eafc17109320deab4b1fd4d61363a..aeb5adf848a04a808a76dca20949523e05f68df9 100644 GIT binary patch literal 69113 zcmeEv2YeLO_W#^_r|#_R%XF|vM!P2tAQ1Qf$d8PTY3gNFY9m_){6FP>b&kp97m-#t#(S}?@ zAD%NLxF}eV5x(gaPUe)cIpulfK@06Lg_k*%^K#9&=3EP|JJ*AY;bOU-TpSnAC2)yc zHg^^`gd55YkccGYL0+UI1DVK=nxht|9cqs{pwmz^>WaFd z1eA!9&~P*YjYOl+Xq1D-ps{Ei8jmKRiD)XyL(@?{DnPT)LbM1iMoZ8+Xel}uEkhTh zOVFig721GqLARoN&_n29^f=muo0uhNsWTFrcQHhsm z#3ap03(}IbCml#v(v5T{JxB~mCMhJ9q>&6Vn2aQ2$XGIwOeJ|FNM?}PWDW_DOUY$q zIk}vyAXku;WEHuRTt%)XtI0ZY6WKs2$rf@mxt-iW?j(1ShseX^5%LUqmOMwECohrx zI@@Mg*`0@M%{%k&vFXU(Q<$MJ{pI^W);m_wU;8*ci@>lU!^Q-wa{I&dgzLMY0 z@8ECe@8<8}AK)M1pWvV3pW~nB5AuikBm8^($Nabacl`JK5B!h(PXZQ*zzebvB{UJ5 z3eAM(LPw#K&{;TL=pw`lJ%wZ;ML0tkAPf`+38RG3LXI$Am@Lc_N`*3^T&NJ{3k!sW z!Xja@aFKAaaEWlKaG9`5SS_p(s)dchCSkL%MYvhGQ@BgGTewHKS9nm^DLg6c5}p!X z5o(0J!am_uVZU%dI4Ha(yd%6T91-3Veh_{XeiD8bei42ZeiMEdMbRs2q9F#vD6yH? zTx=z_7TbyK#b~jk*h%avb`!gcDPpRaCZ>zM#52U+;+bL}F-sgQW{YFRapHJ!f;dqO ziZjHS;w-UHoGZ=~OT`Ps3&o4Xi^WUCOU28?<>G2_t+-CSPP|^cN!%b-iyOr);?3fA z@fPt`@ec7$@hSR?Kg_lXC^x5f9Q9#WhXFC|MUQZMNYsjt*e z%8~|1+0t3kaA|~;BaM+JNE4;0(llwhlrPPcW=TcTTxp(ED$SP`NK2%1Buk>wh0;aR zWzuqKrL;;~Ev=ERmDWo)N;gT>(njfKX{&UrbenXibeD9Wbieel^oaC?v{QOodPaId zdQo~$dSCiL`cV2v`dIoz`c(Q%`ds=#`bPR$`cvj)Syp6^tjYm7O70=Y$gy%yIZlq3 z6XZlWNluni$j5Rw!$gb;|Y1MrD(7 zhw^~(pi-mkRrV>bD*KfK%0cBd<#pwd@`m!Z@}csP^0D%z@|E(F^0V@b@~iTjhxZ7c zHlDVgcAoa04xZCI(VmW;PM*%5(>>ii37#}hx+lYv>FMto;u-3h;hE`~? zp4UBxJa2dod*1PUPh;a%!I*L#_Fx%YDK3hx!(YrJc{cX;pg-sQd9dyn^C?|t6;y$^UF^giT$%=@(W z8SifIOWyt71KxMM?|MJ*e&PMn`>RGYUTdwj(b{V5wDwvD?KCY~>!@|oI&0mucr8Io z)6%sJEmP~S4bg^b1zJ#>q0Q80X@%NsZH`u?&DDyvQf-m8OtZ90v`e+iw5znMwe8w1 z+O67c+79h@?GEiu?Jn(Z?H=s`?Q!jC?HO&4_Of*E{f8|2INo$br>P50&d3VcD|4Bt%O zEMK8-uCLs;*tf)YzV8Cx<-QfZExwz5TYcMn+kLnAZuQ;f+u^(2cZcsD-^0E~d{6pz z`Cjzx_PylW<2&Fx=zG_9#P^}^OW#+%-}ETGiQZIirZ?AH=q>eDdTYIn-c~m+P17 zoAoXF&H7e-o4#GYMZZo4gC^*8kQ^!N3z^>6fF z^k0o8MpL7i(cEZZv@}{7t&KKDTce#3ZFDob8$FB!Bhfg+=xv;7^f3k-*~TzqtTE1* zZp=337#A8B85bLu7?&EC8Ox2!jTOcf#!BOAW4*D#s5G`3+l;%7`;EtqCyd?3OU8cV zfbq5Qjq$DVo$mn_1=* zbE-Mb%r(z8^UUdHzFA-f%^BuwbDmjhE;JXJ)I85T-&}59Zf-WWm^YhS&28p(^A__~ z^EPvbdAoVH`H=a9xzl{!e8Jpn?lV6#KR3THzcjxxzc#-yzcs%zzc+s{fAJ%~QD2h`+ND%@b~tg>Cf;F_7C@u@Q?RT@Sp9^^Dpu*_Al|D z<6r7O*T2kf`KkXr|M~ul{a5%```7qy@Zad)?BC*l%>TIm3I9(2lm1=)r~FU*pYcEI zf6l+#zt8`=|B(NP|2_X_{?7wKKnzF$IiLhQ0X5(aXaQe94+H`&1MLGH0$l^$0^I|N zfuz9jz=*)ez^K6JKu%yxU~FJqV0>UgU`ilAP#BmUC=HYa$^+*H&I?=?SRPm#SQpq3 zs0?fg+#J{%cp|Vf@MK_D;HkjVfoB5G2A&H%A9x|~VqkaRrNExR%Yjz{HGww+Zw1~C zyc2jga3t_v;Qhd-f$sx91bz(s6!$$u>$Zh! z#kJ*S+b>A9!uTAMROgwPF!cWr3=@|@>-geNYQ$@r6ln7rxzL&fE#p`xN-X>LYbVp@7;Vp>dEMp{x#Qbt@_OvZrB z%$W4#l$6BG{wb;PS^aa3&b1}N)0ugt5AE_G{i z+K@XuXLPWvJXD$ox7+KXti1BPT%#S_nvq`)3#!_osNXcB#nDW*&`*N5x2l}qMQxKu8UOXqrVXK=l_ zGcBK`TZUy?ek))_Sxu~_TVZtjaT#1D*PqMc2Ebo~V1S!jldQ>Bo;BSnfWKy1-E$2C z8j~|Cv|xDN{KA=e&{gNF+CMW&gV13U?iyQOSX5YE7%UrKSWrI8xh=c6tOOR4q0*zb z)jnz8Hf3C4a6wjZdd19{Fk?nwSa5ErbkU&FypmZ*zjfPCTf7=O%Cytrk{GtCiK-YO{%(%uV5@a?`k6tF6_;%C!1hgIV|5Psk4y1Y@DR zA%*3!wQY;7owr=0^>J>o+Xr3G2^K*Yg9Wt@!-9EGX{e&axd6-SvRtFhaSF*UE2{{W zxgD6v6>;4*ad1d9bEn#2h8coL*&uH}K=1v*Gg0b24%W>PYfLq8l-LS#xSi>#h&f%7F z=T34sq8Mavg7Yz0Hf4BDRw1B0RGe43s0!{{Heq>Li zQ~hMR>-RvI@WK~gem}9WJUG|xDWDUmt~?_@KUh{4EC8tuPAki2yfHFV1|SXSf zJDZrXtHT4W(7|@lj3&*io2t#~R5fvUa6x+;t_H4Y*{XG$$jZC_h3BG27#x(?u1$6O zdL`DJ7TxhLlyWwd(Yej$(^YSmu5}wWeq?C~U<(}&u+ccC^EKUj#QcRyWB2s^vlSM za02gt;d|jZcsQ%grUB54fpwZNrhl-gsAh2XSrHfYT=XI+YN%d4OdVdQpqz}7lA4jD zMn_!EcZb_TIb)h{96N6OggOP)`B)#y(=sZ`XMvm+=G&55GimaaNC&Uoz1Ok{nAT=< zE`!S1b?TT_3lNam@?g#M{DKHeWiX~t>I}WA&&)}-NwI(#FExd;=R`aT-*bzhh`DVx z7KgNwI?bx{v7z+Vl$Mo8T*GtGmqHEm_3A!-7EBrfCdrJ#vRT2>nnjBvDFYro=iKV0 z^@P+QH5NVZFO1SvP|5`tRbAMpPOl8U=8{VsCa$Sd>I5M2a$7LgU3*mo{e)t!YqRNk;N%D6sEZdB_PUbA`2%@G#Da9Rz8Y-_V=J41h+Cf3|~+l~nO zXDflWEuT(>QtoKJ=}zcqT@I2KHPexJ*Jo{9e%BSnr{_JzlM>r1mzwpIJ6;Q&9ZK`&^RHukKA8THIr6%&y zzG13$pT6-`7}dIk960z|goS{9xs*-KA$`*u$IyGtn{T}x;jqHg?0FSRdAE7>k$TKt z^Zo}PMpy=$nq5%F$9iR-nomFbJi-pZnEr5pfoykC#rkuqzWS!>>#w7^@PAd`eiz-Z z#IXO0?swrT`$OdqP|J_{rk_A}{9I?$z~UYqoC#K4u#_zhjxOk8%N7*QD2LdS2`k80 z?o5a(UB<2CR&zIU8@U}26?zsTL2q&Ia363#BN^g5O;JnK3H3zrCY6ch;39sOyfa_ zVAMeL;!W}~`HB1v(Ta9_7e0oMgIGmBK8K&q&*4iT8e#Fv`5X8uh&b%xck?y;e*PW) z1O8`-D`&SxwiRQ<1n}wi7blBni)G?@;Gy=q1H96i${=MD z_@YadCCa(re`en2Ym}RmN@cThi*la>BaFgu`X&5t_#b7zvpl(iTM5hg0Skew?FIRW zit?hu;vfT&6S-vTh450Rnp?$P$t{cc{#;{v?wImH{l~?pWQ`khv<_G`CZrtwvrd&` z7L^3cQp45dj?2kSj6ZsxZI|c3lDc?ib^)`8W)#Bm*XhR9+-k6Shvy8dC}#~my0oh1 zu4aoXcj0smcLUdLGq;vo$6d><=dRbVtEUxb#ajuR!HIGcw*eORRa`ZAPLiy? zu%z#2Ww6C#8(3hmMg554qDA&H1DqJp6eeoWm|UY7T(_6EV@laN0=}~w4C~ltS($@s zmuBJnomD1Vw7F%Dt|%^s@-mA;`E$yc%O>nm84U|%XL$$Z<J6PalXazu+f*3bJXl*5*z2`i zBO1Q%Ur|~LZ6BIfQJg<3$Ej#^a7GZm%y*W{@o?ARy!pZGVp#O$6%{RVOX)vytmA)R z!!oO|r~sa^#~{~e>(*F*;SW<~S6yTK*dE~?tK=TFdRb@GVK!%ecXCg{ia9*HIl=O3 zZYQ^kTQ+uh4*W4OA(&c_JR>17&MD^^?%54iALqg6xfiC@KA2HjnzyK$d!BodTL!*7 z`%i-dzkux_ienC6sc~FUj4yckXHik18}1RK{gl^8(;#yuXnFa z3yy@jSYz^20}qnG8eI?Ijs$U0b*QU$XpM8D517udzO!tY8$Pfbd?R(I0vn}kXprX z?Of4p92=++fF`FbT?*DgKby_NAVdMwgzHv`qO2*EsHruT4S9zOa084iW%64ZEC9br za3;742P_22Vhoa63X@{!`qf4Co+{;jnkeA=+Qg@tngVoZF?gL}~= zG?~+p&h181prYY9wIeh%SUl7Az}NZCR%AH*2S`|8*TUPgrNh3;v#o5IW zvnXRa#UCDvp}`sDV?v_~XU>9hAd-ZF+_RNvhPBA*B@Y^viEyH92s82lpzwlBO5z`CNSfPs4?gug}y=Ygxe98kkTQe_5l+iaopnaogh zo^_seeigca`@y;Z$R5DxXG4%t7OpaES{N=u;i5g}&R|@IR&GSg(dB3by2859srcE1J$r4G2D)CbznYF z=6PW399ss=R6O%okm+R%5JwF~po(HUToW$o*!#_TS6Mxi-4=`;JrtgH|2j&pwMMFS znRTPZSr=KA)&}c(>!$sv5>=sU=-fuM32lZo)y-%t+QvcJpy7u9hK3w>-?XE)t|Q1z zYDs2vD8w{Z(D@#ooSRozeDro0BYUJr%y7#*dd*s6U1F`XuCp$-`sNxv!|dxu5W+*? z6nHeOQ){F+b9WnBxe4t+x1&4Io#-y>QtNW-3Tu^hm9=^=x)o;?{mm-Pg$o;f=&IJdC4XI5~2uqafL zYx`S!jvdy&XF^;;LQH&a;fS1`?4F(rg26edJwYvlsIve`-H)ue+5kz<@aVh$9)A|z zL~jAP!V1oXkqgZ#^fnv(#x6n73B8Msp!X)R0FEMH579@Hp;@j7 zRH2W7k?VZn-1RB?%!xzSBZ{MlsxRnO=yTR57PpS*{=Mi+=;~L%P2Zr`Sbo6tys{vu zh1k+y5xDE;2SM&{u&!~M@;&+iRxCglRp@(rgrVYEBejJEvTmh7$H}o z-&y@E@1Vi8hTQunx)vjh(e;?e0=miC3~YA0RSj(RwzcCJHp4PjTsE_AwKlpWV^ap! zfikcG{vtmP;3#X8wZ+!JCr>iZ*w;UtHVX?)rOl+;&906VRZxPV?VV|BS^6%9_A&#{XcUxFd<5b)q zEKr;V&beOr4BQ)^iTmKbxF62InbsZFoz`8}-PS$Uz1Dr!{ni84gVsaaaTcuSzz7C! zUN$}p4}nj^z@B~>ENW&JKW;r^J!`!HpI&rq?3PE-=%6XK%#H(t^$2s=@FLh2!wQSR zU@UNqC*=f8zlNn0*_LRnE{1!e8W;0X@?O?ex&M?8cY(5yb%nl2G zbiTI>gJ^w&1}}gVkD*Y9vuMLp(8@|Y)q2EQ?MTfQU zOEc2lwp@m<07na64!-3T)^6)1Yfm*^iC5t(t(UD=thZRQQ)YNDbMo_8B;2U|0@nJ4 z;9q3kDEA&a^CMIo9~TD;3+4%!6bj9a?)#O~Sf>-~*f3vf)c{F3$zb?;d=qQ@4fsZD zueGlVZ@`t-tJZ$2aD@yHnEi=P1`-k#Fd1FEq`Ln`}bK2q-xCP(B zHQk7B##`|=yxlrr9kgDvUf+ms#kb)d_;%}%^@erWdJ`%rEGP`k9ajLr$q!Cqe4hv2 z#{A&qP$}~`+7(U%V<7fug}KJ*VTFirH_V5u~ZG7gajTxs%Z5-3`y2{{)tqeYMarPPhk?Xb%;xAv|FY#CS zYy1uV7JrAo$3IwKT3=aTTi;mUTHjgUTR&JoT0d>WKZO;+Z}@lo2iJ*kpay;ptAXDs z!W0pfX~_TY>4AnCBZ-XR9p$)ZW>sbiekFNE;p8 zT4PDPP30ucdX6ID1ke9<+tvW3Tv1Z%TLWV+2ayUgQMH)puigb#MTJj+2Ozv4_ zup|VEAIfa-#@0|Si_|ewT_ZFiyOd0KDl!{q7&4PF3`GHlVpg0YimAVJJjOCTLEmc^ zM`R{rB50?Jni6sj*rjA1DJ5m3oK%qcWC2-77D4!T2}MmQYDQ6WidsI zMQtf+M^XFjWU0eUgp%{f`Q!rNri-{P6m@XOiK0#vb)%>|1)nYuRP6sAL7jwM{s#W~ z+YD8*hB4VS6rJW$*|m(y)|2bli=t?XIy&#xY9q~XQhU1IkYA7LlYtcV zt*1|T1p0)>JmGm>;6;l1QItVZCPn=z%Bs~TsE*^~|Ee&plOm_Ln& z5ZHD;n(xSW;yd%FLz+!jz8l}2@4?4VG?=1niq4{F2t`9F8b%T5#Ss*Zq-YdHqqp-t z9X93@aW<6m z4}kN&40Z%L8E8cnbBl*p%$?45gZYES%vA}QCw0Cw^Jjsd2sT`mj%HpLaM7_HE8z&^ zfMRfQ+TKmWd6u~tYm0Mhsr%)z#f9>%+YB%d78rdkF7RjgKRXD)zsA4LGT_FI83PU= zH;my%kgEAZ{2L4-$Gf5N=#tABZ}M+Lz6bvnMT;tVFj5w?@UsbZ)g}(u(PsxS?{m*q zL00I86v6s})<)m?Pxvpmrq%qX{AWCPmd>GQDMja2^IvjZ_^&BiX2CkdvN}$MQjZ2Q zr?9_{Zay^NPqy|OmNye};wlP)6Kf$K-S2`l+rD(${4@W1W7UCoMc^nz(?LU?&-g=! zBHTO(mdOy^FMmVbf&@Vjxo#UaSlz=V3yR=_1VF(fsDf93CF?~LT};s>6kWPe&;^70 zL4d{XWfZNnCQx*x)qMg>O<>KQXlEy|Pm>%Eu#*I0r)Ies7_}J{Zfz}uwyd_6LMuqH zg_YTIiY}*U1w~hEgxJ9bTn?_HL_qgbj=FC(4zf;IN*0V-aDkmWGdgd9onQeD*_QRM zhIf(Wr251z6 z38d|mg^Gd@Jp{2}J$CPo6B5|n@f2NEDZq4GeF9VosemdWO-QF`HAQP{Q6=;i`ZY4m zLI&4G0K0DuL)A4_$J!}~?srbVdb2xN7~0tL5Qh=K@LI>7zt-w_bmpLdk#+$!!Wd2$ z#tP$DFtPx0AV!${{q=YCiBgw;^%SGG3`17zO?bKi8|k8&G;P+r1=|7Fwq5%Uu!pTv z=hLSS&#AN77+Nu=u$=kfP1yMBTyVDkGO}s{shJQ7%7e_u!o{}pItO-+!1ijE!@?4i z#zD9=J39;5YIx4TLfHNT&z6KBtErS_b=E!_EP#+}f989)6Eh$yv>dXQhUJybfhTM0 zb-)d9X;J$X6bH#cgM!5nYGOMWfp6Sa)}~oxXE_ZTR9cdq1-mp^21~(0sNd<0!;}`X zh$R|ChE3p{V(}(y9+nIY8?{Y?GlhLS41d>du>UT$XIy&Uei?&?j~JOV&e>uP&ha?d zp4^`8O=eq@+sDEG_8)1f$?eBNTYII2jILaNIRB{}PJt?ie+R+&PkFG#wSD_2u05O$ z735~Z{?{_N7KAI~;B$~$08fNq6Kpwr6M{{$MfSB=xN8AipTU6-pXDgl_S5OkNJnA2 zOwE$poUP$|<6-AtN*aSz<9pyMlP$S26?UH+dwZRc*!xUKA#4sakzX1N7CS>fc5HT* zlM4V<*T2Nl04;krJM5ZIOwCG5V;klZvQkoF;u2v0d}jK<)R?q^iE$Z;aq*du>N`2S zmpeT!ZfQ+s|EvLx?XrbQs#||m*1+0bw)+NWC!RGVdgw5SFS=9dw6g9K;SKX`>)+83 zH7)AjP)7jlZR8qIl$~Ro8h>;bdRko4(xoukN0X18-RUsvOIepjjjqL*)3B;h1L5m2 zW9#o79y4%q+`{;T_`LkY|(x2rE?7UsaoTm@(b?C@aOXLC^ql|Zst1?=`%1gCT@g%i5YgXF6l;9RaJV2?+4 zI9n?V55YNj8Ve_I12O4v{x)#^P%CD2#XhcHh}j{yN)=5GDu{fmht@eqgm~_-Ycj z4ETZNdAZa*MVJA5`Gu*%G$B_w8+Lk3hrJ#J0+`S@Py}9K@D6XFsFI>8iomSgNYN(P z)DOEs3WeFi9HB^mZrz8a1k<0A! z3Lt!3V8>DI1VzV|wL`dN14E^d>`^wwIo2R2e^!ts21hx6LZUF}Ffic4#1qG2kt{uv zZO1G)n&;>|mI-Mk1@?o5<%=L?1vDVo2(FYmcw>sx`NT5Y!Omkjd$!`vE}jvB+uaCaA^-NIOt-2q)9SWL^GBP@K<9EZjYA>E132NTjrKZ0TyAmfq`{2$49u@abw?_Ys@=Eu68!lu|*}Nq1io$ zK|UX(FS;Y-Ht`zadam0hVJ+-lx>i^(fCqI4MYmIQ2Ss;Mbk`=~23#&&Cu|S^=yx;6 z=Uo(m|K$ZXI}tk)TDV|=(zaIUU zTe(DqRDBt^R@f@y;ci~qmk{q8ir zE*ysBVBwJP21U|2agpSKCL&W5k-GG?qhi_bb=+sB+l_bYTXfm`hR0&G>; z0=x7+6h0C@7CwR9dY@7BB1O9?dWj+!I)KP46xD1Iz7W0?z7oEM-F)A2ohaH%5iAei zrRWGn?@{zVtV}=_Amqki4lO8lpzEG? zF^uIeHDebu*b)MGJb1K#$^D^Z*m}X#1l+}_DOfNjR5FxhaE3)H3q;9qaxx%cJDi5^ z0P+vCaWk(W~!cMXIc}46a zJ3$8aW>NGiMf(ksYm8g6BqlyCH8D0NEiE-AJvk{UF+DA%NBfxg`1I7+w4|i;l!Vlj zhOON~teyDTv!EhQ~3AuSEe6n4gm=o57? z3+=^RmFR<9*Ko^B)-s9?LK|vZkQ|$on3}|jNr+ELY@`KA2??wPiSh9%vGGaC$!YQF zDXA&(=}C>Zz^$u^Q`hT9>w<@46VsBC)6yA+S8SgoCe*BONWY*(^HdD z;-FjcDJd`m@kwdPi3w@R$;r&FjqY1B^fvTn$P5d$*jYT?abwg{GVE1Z5|ap6ONGv* zB&DUXfnmcG10aZnIZFiaLhs|DE&w3xsStahl{dqF-B_`w7$?TVzTHF-VqYIn1m2#H zDEgQpFeN{w28(sywcA(%?ml zV@hVl%%3x8(uj-!F-7Sy`4jUe<;@rvQ&bUCkdihjam0d{Vev6%1;0&-bzcPo}ZxsD* zYnJRxFkv41arc{X1up; zZROrtCN2SGC6uCUvhPyUfZ$S7)6!vIq|2PwI#qdUnKKP)0v-d^mYkH95(hd3>SBs6H9kH8{0MPC zD+vjSOd}jyR~jgf6V-KtQR29B*?Q(|fYJFYIQ4=SCSe6_s-l@XgBpO&1Em=K?ymJFPJ z>-DkYw&srq%*AHhvdT}EwP&>PSl%t%O20UeN*%0@6fAvHE7IXNAOJ_*zS zs5QGT#^7mSWyFD7D=scE4s=g^dU|>+C=y_9I7krmSHjVLZFTAb7j;;tCZxy5Ccxv# zDanwY%hV>+l>)jCG)WwklnUMssG8LUstA-iQ!}846VuaE*cc`z!C=Ryq$Y!sk4s5T z0X-Mp_t5BjuQ~EfH&;k+6L&OFKFz?f1PVF{lsTxKl(@u{co6d{;FkA*_%Im0;)CKt6t|?fb(Q#t_$b9~DL$>f4tUbm0k{>cQjf0# zz~W>_=#=$jORP{r?A(&1I!jUUS*Hv*Yb4?_;_Wgx`$$Xi1@R@;*cZj!6t|rP1#g;WjGSD^PN<5@l_ZW+q3TGNQ5CVnn{A$}=-Mf!^0h~J9eiQkJqh(C%yi9d_Kh`)-z ziNA|~h<{3)gd{8xiI)UPlz@SNN>8V_E5+Td9Tdk<+>_#XiW4bLrZ^Q611V0Y_za5A zq_{7|85H-YcmTzND9)yM2*txF9zpRaigPF)OYwM$CsI6_;;9tpQk+L|KD1R*B#)#@ zUN{8AC+U(QnUY@$NKsM~I25Fr)Ld#IwS>b#T1#!Dwo*H(JscBqniMT{lsZYBrPHM@ zQdg-P#X*V}Q+x%*cTs$h;%_MNP|}u?G)i(PnMuiFO0K5lK1yDpl;yX6`i|Kn|j z-5cO8bbA9E1nd6kWU|+7dLO6hJ#vlmQ`B^J?qW~&R^JM1*p&WGQ({jY0Btk)WX%}t zG$St8n01Oq1a?udX@P@aVJ|W}jj8U040Eba$TcoMMb+2Gzdb$20-ybzF)};5xTK=I zVG~C?O-#x)R{vX?7>S4;M>5AdO-;!)t~f3R$~EXIYRPfoR_9#6h9fk?X;<%D9 z4M?NJX-dCbBj@iNA2?l=6<3#@Dx8`i(sJJ4SyP?;>Hne-Ep{3PnE@A{qK4H%x4}V{ zJsDp*-yVlDmpQE)m}~s|_k2kgIF%31HL6e1(9~Bck??h`Np(WGztm~@S-D2hDQfvR zwvrlIl()z^C$S;*T;VhU4)j=dikeVYgG5TyPSg<(&o$PcqE`L&DLvs#u5*SFl1ct+ z4zs9BYd1K}8=Y&c{I`r}Etk|8G@C{0{M`_XRZc6%3euKeeH5-z?SXT(W;63;tLhXkPis3})h8XXTQoY#90w#!Q|NqeN1rB|dHX|J?T zdR5vl9iVsy#WN|MMR6g;vnifKaS_FHDK4fsv`u==*$^cimf(B_mcF8RUU-j_^eyahf_bq2hk$(SWS;H+nI{w2_UQZQ}z zyX=*9_OvDYV8at$bb_bdCz{CZ0giH0xtZKtZXvgnTgk2EHgJkGc<+}`d=AA+DL$9t zWfWT!Q;N@{_OgRhSC_^yfl1drk5tl{?$7~t43@|tjVQ{>R0ctpd+aBxZ(s4xlc@yXTercd8#~3&Xv!W^W^Dpa(RIqlxN65h$|^xMe&ssLlpRG zidR#-hT>}|UQ6*him%-+7lxrYS1yK6z>RXLTt@MF7mPPjTn(RXcHBtuW)_QTDDeOP z9x&CgFSVyS{Vm5!odm2A=6~7|ItbDOaDhawbAfvy02fZDznHx!zMkS6oOjr_?=jqz z9{CD+6$3Cp;igI%pa8#xW#>nqgszjnbC!!;2K}YlI&fb59cZCUSMn)7;&RISa0X^G`#cN$3B_OiW2AsP&ffDB%Z`LR z=^2Om!yQoFAFf0} z0^UaDUF8USQT!(*kebZiLG|`G1b4naQ9fgm^(iH|Qu&+`5}{;$t$gQ5*0&&8ga>3P zfQpYP5gLe=>3Jl&Uq?s#8cA-&zk4_bntuW`iE^R>!jE`Fj|tfHNFLdvcsw4})ct|`g0h^?Wi_K=PhXl@6{0EwU zyNATn1)%BaN=ef&G(E7PqO+L(f14!!{>)$bDj%XCl}hp5dMmo{^qWp3$Bh`H*L^Vjn(h?3`s=RNkKWB?@top-Q0ni!qk ziT~8|ITNYRC>dPo`GS({2u11}&-ac`RG?I(VkS^SDH$0S zC{=*Ys9>cIs}(3!QN48qO4U@bFNRYxA|ioO{c5v+U7*x<>S?w>sU4U=jXrsA)h=ox zSV(GDwVT>q?V-k~v1(5>PK{R+C>cY^SW3oGGM*ApR1+zgM9E}Ircg35|jMU*L(n#u9bv%fV3UirPslr^^M-ra= zrykTPYOXCl>NF-k`2Z+2k7=ZW1{%rK?+i*3Q-{?16IdL={QRg{#uD1?)pk8h`|*Q;P`RH`>nQc6TtBlLNe|-ZV;Xa{0abXmS9#nUZ_{5vsrDcaJzycyb55cOZl6 zAWGl|o@~S33nt1&?-1`$_M)Vck}BsN411pu7ZwX|j(03bpcib-jg?-oPc}uS6TFkW zQ*8 z*n0^jcTjRCC3jJBHw?&sTZrGg(tDLdC08;kx%ULK=T6-^??%QX*Lv4`uk&8-y}^5< z_a^TKZzV8EH6`~`@&F|dQt}Wb4^#38C67|_7$u;(fk~d&?%fn-lC8B&a+?=Md#B4J z5Ug<1j0m)7&p%A^*O(*%f_Rt-0nE&kE3igNIPwENwr{2#f*-gnykqP!!UT|16{OPyG1>0@=Z{9y0ar%Ra z)5|CCttMy&thh8$lQdaVG>@igUQN?{noda#C3`9SUI=-WlKqq%pyVJWuTcV~)uC;g z=}3~+#947^E!c|d4OfmJZtkQqllS1O|1lf)KOeNyP6xngFyM#7LZo#EA(AiBV&sby zNbCa32_ky%E`boW3ByB6)L48(OQPiMN-c$wcOnxat(V5)Bib2k-SsZ4yR<%R-F2jq zb(e|yxs_#UPJBeO<0J0_7PUT1F&}B5nB6CaX^@+-O&hL_&_-&bw9#6QHbxt(jnlyI z1UC6clzdF#>{s$BB_K)=as>b1my~?9O$(=GXj6gJAuyulF)V)VViBUx)s8For+={c z_xqr<5W^z4VZL#(SO!>xd1ySXUR$i4!%zrj&UcmCQcAv$5QS6&Q>RKhkD>4fhQbRO z3V&=Ag()tkmTOnojHs<(jQBG{;e`x^KOPr_tF@aL3fE}YXlu1~+O^tx?Kzq&!D?M0rekLV13hwjs=j8^ervGoX+c!YJe=ml1jO9~7Pf6yD2F zcpv3O7ljX=5QR@@Pcjtlq`X|I?V`LAAqt<>;HXwu@oUe6LBV?fP$%&yX|HJe90=|O z2=d+to_JmR0Bl_CkoJalSbI}@OM6>Bq`j*h(caVEr@W8yI^_+@o0RucKER?|d=%xI zP`)YUn{C%V4CC-qxmf$$=EJWT5}Uh7Yz2Hs`Svy?@}2(y<0$|moYPvV0YkJ1gVBd= z8`sB^>wIu%Ye&k%uNSg+P|t0{T;=olAY@eOQz_rN(g(*lwTTRpKGPRvL(&&ukZcP8 z^ECyIWlHkQa+mU9a)o4nwaiO@TE3p+%}$pT5D>%mG|{!+5+XvU;=ge3Hs|kG1!*_obJo^ zo#h+i8|oY88}1w78|fS68%_DHl@(J5~V;ouXO>lnc z);EPoQlcwK$rNFV*l*qP>Ho;l-!DhL*$jGfD4*oYQ8AOF20wP|&U1xt0h6HlluxPj zEu?&Egc5X)kNskoZz-4-d>WV*!H0!1hP^ zt_uV4Cj2|s#aG33@@@2OqWnM?irD}~s~P2oGcb;z{HXu6MOVYD&wt|vDSMUgyOW{u zF3Jyb(FiujM*ddc{p z%Mdot_Y7n8p^W98XRJP~0jrz*VMBGRd)c?w0cH&Y=ExH~=05S74^CxoWWST|E#KQr z6#z%(9P9A#y~lYR>rtP`Er1zpr7%1@yDM9NR1{A9{cq5M?JPosP;<S)raZB^%43=eH7(GlrNz?Xqr;W zmr=f)@)eW^m9>EK3%BVxVO)-P7Lod7hRa1RE|=6UBH;(<{?{1(Kkru43mGgy@)x_X zoC~l#t}80{vnKf)_CeS`j?4 zLcbR54gCszrM^nPQol;ST0W$&(XY|h>gyhtuW-S54}ej> zSHF+FDEtyEztVXJj?|50mwO)7nZcoh!Et4!4hF|n5rXn5-7z?H+u*pGv7BvitZvAX z_#(HmJ^J28*OB_G`hI3STtoTw;dP|W@-p<-DZjSXX3!7oZ!=iCHiQ0-{x0PKp70xn z5v(Ki4|Fg&8U{%sTSw~O>R@reNgMk2Os1|sL5JPm{$_x=v0eXN|3m-N;0$D7gBZLa z2x|>6IBul;O_bk2`AW)HQNEh;8!5ku@|#(55Wi)+5w)Vi;T}!*2vAf3qu7 z5Ox+Vu;K4u0tOR*|Nk1p|L1LZqdkDo=s@|cVWBc$f9*!2lhK*ID8G&J+nsk1s_Trz z@XQ-AMo$nb1B{MaD-G}t+!mQo8A%3sI)Leo6vp&BAXmmnX9+_5?G2?WYOmp*KyUOl z`h#3G{KYF{fH9EC)m@ao`&gsgV9B7y5X#?ED`3WOV-(b1Crii}ZRAk?UdrDWk%$@N z4fw(Czw*;m?ug|ZGi>QHf=s#|I8o2tM~aMvAYR5?qu2-;CB{6XR6b;s8x_WU1C;AS zlz*7=k5K+m$^*HOeyuzfZ26WZ&(wy9JJ!rhg zgy}Wob>oonhH=<<(|F5x+jz&@-8e$|eUyKd^7|=&fbs_^{~F~%F(0D*8!SWgDgV*YHlJ>Gt1DM#cM}%)pdmkrNUqE{ z6BhRkZNL*I%9y>)zN}8m?8C(AvlH~!?dlzoqeUcKT!V1ZRRLPlFYGAa)JrT3Y2G=2=+P(w!k~d2?F~DRN$!~P(h@ELXGGT#qV{ z41c0R2N#CF0t~%7<%`*iW#Pgxe)fB z3_Ag+{63&Yp>soOBne^6`vd-_HWK|!7!tdj;34;k)_(ZW&~5%U{36p8d+0ucyJ5D4Wc zAogST=+R@jJEAk^ely?9xzBUwzj=6IZ}wh&{noqQz1j;Gfs4Y$;Noxz044*#WC55Q z0Fwt`3II$IfGGhmWdNoEz*MPlsjW^LF1I!84p(B6sP>aYji1Bru;c$F@&7-Ga3n|~ zTpxg`{~!@=1d{l_4!groz)!I$JPE)wE8(XBnATsU&<1Y*T^Pdc*c57mK>cs$eEbB; z6%LMmfO;a_osFQ*U+{4j9>PY@8}0-5h5NzJ!TsR@@IZJFJQ#rK0Wc&01M_JBzzhMH z5dbp=U?u>}6o8pg;p}ry!Sf2?5kC;bvJo`@iQtL9A!zwu1pfmFCbAJs0$||W{r8V8 zcdi)JR^nVfj4+;T$VTub{1yB){0;mq{2lx^_<3_>KM+Ll{wo9#M-luWf(THh53fXkD*gGth9E)&Ar1nC5Cz9>VG-ciEkXhuyM;ym zld)SKzaL#OLJk4Wz<^ybLV*n-`&4~|1Y3oS{09~Crw?_6A^4$;&_HM+v=G{e;|QHy zD+oOV5}}U(JKzfdEE<5}0N6zUh6iBa36bE3ax4J51i<0|7@-bfv_&GqY*#44f<2vp zIK^fW_{m}d`$HL)`h7Y9mib>C{{uL>vT<|+VDUe3JOkp$RkbaV{mWh-1eM?V`?oI? zgbxCocB({x;w7;X0g9KTzldZIBJ{gdM1-)VVlo@Aa5i2k|A5zisK`&kq7b-$I-`KV zBVxcIP*@rO1NZWO*C!Bh?7R>FSo+`7nt(`V$N6(c0g-}81z?u}7?GXkf4hnrK}3K$ z$UlyJ{R74x1o!tNtU5IW(526>*$8LP(0POpwe*i`TV3h!@3V?yhxdt{qF;|RN z#5rG#y~!yQ5AW8!i7z(5+ZQzJhVcuF2yqLDGz|_o7jQNhJc#z-pIw}YbPk9BO|eEs zqIh`z7L1cuM1WT)=6{6t)7&+R2fpRfFv8nE0ORY8iHZQj{qb-sW;3slfM6Vo=fIyM z`)*Ih7_=b#4~Z}i3-yiwt@fgL5CP%-VWGZ}0U=Rf;=>}8)fGTXu-yOS=|6jd^uc0$ zzGqwhf4DxbVgedy*HF@ySJKutkyq1DR+85?Q#X@W(l7)~rW=`RnW?EbdPRh2s5u4L zu%ma|x+ts2DgUg_IRpzLa}99^F^^b4ECR3^09FgYC;;sGHN+BP8L@&`1z=PFRtLc9 z0T{T49?T?&h@m$o02_e8+4+WtMPM8PFzhNg{0}djd?O+Q!b1PqE9-C!_y(*WrGM~c z92OD|S~mt?+ed|jctzm0V6^u=7vdX=iF7oI3JCUbQdTtv&BgOP-D->vA){o(BgA9G z69Co-z?uPA8v*$d{no*4W1; z{NGvozoNE@2ndS+bB*GW{nrkSyu5?#eZ##X*maHKk^a}Nx8iW}3Xbx%75bUxPl&Jo zB+VRIybxX}TM~iI4LlXY8zLmm0TG&oIY791VK67_{oC%G4G#{8#3(7K8_dpQxj$M_|sdqWH;O3p@Md?N`Qgu{ewuJ%#h-k@DgU!S=H2e}UM9d)*{R|xXOMY@649|@{F zOxRv|ga9KToUH84!a_0g0)j%|Mix;qaS8B=eZWOuFeX2|!TXUim0lK~agYo3Mwlm#m_yM&KmuBOD+gz~;T>o1Lz|`9tvtS=k#qu;VfF z^qRTK)A zm&?|z@AtwC0NZw0DA??(PFsPcZ2g@P(1(q!r;l19|>u!B#ng)-C_xDd^hO?MG!-xmtn$dYl1;@1OAT*#fJ|6ZHO1 zo~HDT7o%u0xt29S}NX1ab>9 z24O-bA$K76AP*rgA)le!pqx-{=zb^ydH|{bRfXC>?Vt`&cPI+#1@(sdLIa>d&=6=C z^gJ{LN`;O<$DtF@Dd-Gz5xNXrh2Dj(L+?W$Kp%0yI0QL_IYc?6I8-@wIZkk#=5XY2 z;|Smg;fUf$;UIEUa8z-$a&&PFatw2faoput-=dWR`eUUs>c{oR1w3UNTJl3M@b2Q} zhRD<)p7ZYE-HUk1yN{RayGUyXU>%@T17Mv1tgD8X2lAd5&WnJ&hr9=1-2@i^)&s!$ z2rl3cNV~|t7D#Mh{;ea>Y#``J@?VFT=L|k-Yl6280`gQUJ;ez=i=B1AvVZo&&IP05$=@roafHFTsrr zub@|th{%acNo}DM0xwmhev=tz3+wN->v9IDn^Ed zo(uHy4G9PZZR=rugTum67s4WfA{8AijTKdtR8-`ZQQ!$MiflKE7kqt#G!_4@u>JYg z$ScxUAvioDEKt$XD>S{Qq7OVHZM! z!@PVV6|roa$0`aT;c9=*hUZoo)Bg`)D6_)=1u7pz1R@7fh8&0JgTmAa;sNpGU*gn&J;rnv;HSQY&cR|18;^5lOwS#LHd%r9f7Z(o~oQs!Bl1rLP z7Tkua$feAs%B9Yw$)(Mu18(#5;)>!*)pBiA2XU%(whP;j>p zsD^OExp}z{av$bC!mY>c!5z*W&t1iRgS&&fi@S##TujY9#?9oOT*0|G+=!vYL}QGwe6a{}`MivmjmD*|r?`2;NmLj^AgUKETGyd;<= zm@8NzSR+_3*e2L7I3PGA$P`=_To-&I_)LgHXuFW4keZN=ke-mfkfD%;&`F`wLRLaH zLeWBHLW4r9p9zSwCoZ!ur7b7BEvL1H0dVPfaSBE_P_E{Ng8 zvc+=6u80+g6^UIHD;28{s}idgqli((>cytTK8tgS^NH(QV zk-Q{XA=xc?Px7@Cx0I-qqLihSwUn)ty_BPrvy`ipyVMyetW=s*kyMiuUFx3Hy3~EC z2U3ruo=82D=9iX`R+d(kR+rY4)|S?h){{Od?I`UoeMTB3?Ij&2ohn@`T`$d)W=Stg zuS(yQeklD``Zwth(jR4Z%J9k@lsPPOL`GajMMh0VLq`s8lO-Icp9_euV!Jiol4ys*5eyturiytKTmyotP-yoLNp`P1@N@;36W z@;>r@^8WIH^1{993vk=vL@e=vNq2m{NG4@JQi_!ZU^E3NIC2E4)?sP2q#W zM}5=kw<^9SBl@BW)Q5IAd zRu)wjS5{UwP(Go2Qu(y9wX%z{uX3PruyUw!q;iz<1!bIaoN}gewsM*BHRT#*iZWH1 zrrfODs@$&JsXVH@qWoC}royGNUj?CZKt)7FQbk%tmfb(AsHteE=&GDl@l%OZiBY+v zLQsiUNmR*D$x_Kt$y3Q!DO4#_xu$YMrCViO<(;aKs;R1#YOrdCYL;q_YMyGoYN6^? z)l$_8)ehAz)gIM8)dAHZ)nV01)jO&Ssw~xI)m7Ces?SuPtG-l&s%=x_QG=`TsvT4l zR#Q+@Qd3b=Q`1*Ft!AZWqh_bptv0GQt~Q}IrM9B>O6`r>JGJ*}zpH&x`>gg=9iq;m zzFnP9{ir&>x}dtSx~RH@y0p5ix`Mirx{A8BdZ>D?dW$+s{i}wEhM@*pBUU3>BTeJ7 zMutX#MukSD23ez8qgLa(Mx923MwdpfM!&|8#;^uMCw(&E#S(2~}Y)sojbq2;9IqUEOLp%tJNq!pqS zrWK=gNsFKrua%{ht93=IK&w)#QH!S4tktS*s%@w3pzWmXs_my8t4+{O&`#D)(RNX%A`-Yv0ly*PhUx*1oMhr~T?U_i@$Zj>mDwD~|Ua z-_U{T$mp2s*yz~nIO(|Pco9 zk{_vo)JEzekw^oi5z+){iF84_BR!E`NFU@mWB?M2j7DBW#vs#>mysFBEMzIN97#e} zA#WgQ$Yx|KatJwsyoDS?-a)P-?;{@|A0glA@73qh->=WBe@Op`KEJ+@zNo&0zO=ra zzOlZUzJ>lt{nPr^`nLKG`cC>T`tJH?^ildT`X&1P`s)TT16hNU2B8KS25km&2CD}5 z3^oil4W1jkH~8J)lfh?$uZ9rAeTF=S2*ZPhe1=C2wG2%SPa2*!v^I1wbTV`?bTbSv z#2VraV+{$0NrowgX@-{#uNu}GHX70lTMXL``wRyR>4pr$QNu;Ur$&2?c#RGj9Wgp) zByFT%q-3OGq-k{ANY6;$$lb`_=)6&+QIydIBb-sZQIb)r(Pg7dqimxJqcNl3jE@+L z8p|1*7@siqGLAGR8fO{j8ecIkFfKB_YD_l1Y20euVcc!pXFO;;Y<$aj%y_|=WxQhi zi}5|1a@2(1M9@UeM8QPKM8!nk#L&dp#MH#j#KFYL#Kpwd#NQ;) zB-rGF3CX10WWr?4bUZ)Q8pcAM=r<1*th6EKrCLz-Eb*_wHp`I`Bg z1(}7KMVUpLT{Md|i!;kHyJ}WzMlri?R%g~>HfT0%cFSzsY|?DOY{_iZ?5^3m*?qHL z&EA_s&H2nF%oWX5%+<}c%yrC7%)t>N^ONSM&8^IX&C|^5%}2~<&F9To<}2pEnBOyh zYXPy?X0gL!w*{vKmxYLhtc8+=s)dGymW8RsX$xx$I}1k(4-1rqmxYgopGBBOv_+By z(IU$t*COAd)S|+o(&Cy$jYX41r^S%PxMi|snq{sf$+Fh6(X!ie%JR159m_?_Wy@bI z*DUW_KD2yd`PuTT71V0G)lREDR{N}YtPoZQtqxl$TAj9XwF`jwz;-f zYzu9#+LqeZ*-qHrvwdv)+V-982iuQ!JM8w_aoO#+J7~vecg#+}PRvfl&dSc+&d2VY zU7%fvUAP_2F2*j-F5WK5F2ydx?uuQ7U7cN*-KIT{y}G@Ty}A8KdrNyedk1@Gdsq8# z`viNUeX%{ozRteUo@U=--)`S!-)lc%KW#s2KX1>nU$MVyzh=K-|IYrKgM@>D!)XUU z2b@E^LzY9XL%u_iLy1GVL!|@Rp~<1!f#ERbFyS!eDCwx=sO6~Vc+%0*(Z=z*W1C~A zV~^7TClM!cCn+buDbp#(>59{Hr%z5_oFUFG&R));&SB2s&JoUdXNoh;dB}OhdDNNd zJmq}b`Hu6V^Rn}*^CRas&R<-1xgcBwT%=vpTnt@IT+Cfgx>&kcyLh;Gx}aUWU05y; zTwb`mc6sOW!R3?dHdmPIF4w)TT&_H>a;{dcXI=5GsjfM$S6mBSuez4G*0^4Gt#fU3 zrMWh{j=QeA{^7>ucGT^d8^4>No1B}xn}VB?o3@*oo1>eHo4cE*o0pr9+c~#DH=J9H zTbx_GTasI<+hw;5w=B0xw|ciZx94sj-4X5*?uzcZ?)vUV?xyY*?x)@ z-MigK-I?xF9+Dnv9-1DJ-WfWs7n`xuD!pXHb5q08|hv z1a%QLh#E%SLXD#)QPZed)I5rXT0z}Kt)n(kk5Erh&rz>XZ&B}2A5ouC-_RUr71v(GutobT~Q^jYZ?oG3YpSJUR)TioT4_MCYKdpbODg(Pd~7 znvAYNUq{!YZ=i3YThSfpZgd}d5Iu~(g&s#wqG!-^=y~)8dK3K+{lv?|E5IwrE5s}O zEYDf~vw~-Z&x)O8o@Jd~KD&DMp0|@XI8N++&O6Zi7w@Ouzk0v$e(mGo6W|l%6XFx@ z^Uasjm&=#O7vW3wZTIc;?e^{Sv-b1w^YlaedHXH-J@k9*_tfwCx%1~Pog%>r)( z_5}6?4g}JJ0)ntX(Loo3VuOAS`WW;_(3fCHaC~r9a87Vua6yP_h(U-^h)IZf$ZW{H zkoA!JArC{NLz6;NLeoNtVSB<3h4FN8AP>;A4L5paPXpQKIL`4QihDL@*Mn-xMmt4aSCI&ttLJXe=HZizQ-9S|K99TFWDeLgxenh>23ogAGOO^nWru8Z!7?v3t`9*VvdJr+F?Jr(^+^jh@u z=r_^tqCZ4`j{b^+;G%wM7QP%$!e7U?;k)s@_yPO~ z{uX{5KY?GtKgPerzrp{8{~iA=1{$+HW@pUqn8PvJF{fi3V?1NLVtiu!Vj^RrV=l(T z!~ijgF)1-=G1W0QW4dE{WBOwTW9TsxG1D=#G4nC3nB|x!v4~jxSfAMQv8l0_V>4rO zVz0y&#$JsrizUU9V{2ls#}35OV@G0d#g4^J#7@V~#?Hqs#xBRM#@>znYye z#^a{rZpYn;TZ&tWyBoI__cHEH+#iHp;N%1xoDDfl5F`i_#0ZiEX@VBv6d{-pMTjQg z39*E9LKY#1kVhyYln}}ZBtj2ij4(^MLs%fN2+M@~;FgUigkK3S39kVNAOko7cpwo- z2J(O+;3`lCQ~}q3THrd+0gM2vz&&6CcmO;Go&hg_*T6gA1Mms>63-idD4s9=Xgq(s zP`pUISiD5MRJ?4wLcCJEWqfdacKprwh4{}2d(JwJFF**^ScquUn}kckCtXS+B*iCHB=se2ByUfaO14UlNX|)aN}fz!NM1@_ zO}?A_DEU?L+vNAjACo^Pe@%g;aHMdjz*7#S98Ni!!k=F z#iw0P%Sg*g%Sp>i%TKFHt4^b&)ulD2HKk3aEvMZ}TTk0edy@7n?M2$Fv~TGg>D=kO z=?ByK(go6m(nZt7)78=q(_PZt(>>F@(tXm;r3a>mq=%DlRd>G|n}>BZ@# z=@sc!>DSV0(yyo2r8lIHr$4#8@3P`$`^&h?WtV#{uU!6}2qkVO?j-Ie!ih(T{6s;b zFj15!PE;YP6SassL?qFGc!n5A3?rT=Vu&~*o_L8!AZ8JBh~>m;B86B-Y$Uc4+lgJo zUSdCSl*lAb5od_M5I2aM#D~OZ#23WZ#J3sT8OJijGbA&lGh{R5Gc+=^GjucbGmJ7! zGAuLPGEf;_89o^S89^DL8Q~dm8N>{7Moq@`jQWfl88s*b2#&H*3K-6EaR-xS=L#0Sq@oe zvi!0FvVyb1vLdoDS=g-TtfZ{etjk%MSvgsGSxs5JSwmUFS+}w#vZk_bXU%18WWCIK zpY<{8bJn-)9of6G_hxft^JMd7AIlcZ7S2}4R?Sw=*38z;*3UM|Hpxb3hh$^3qq8q& z$7Em1PRUNs&dAQr&dbivF3+yZZpyxy-J0E%-ILv)J(xX{{Y&e zUQ^!9yq3K7yso^SyuQ4_JbK>=gQlBo_xOiWBG#l!uc}! zs`={qn)%xKI{A9}C-W`yZSw8&o$_7sqw?ePQ}WaEiTOGCSMm$;i}GvpTk<>eyYu_< z>G>o1qxs|cOZgA;zZ5_UwiWCs*j>O`z+C_@I8bo7;AnwBfqH>f!SMp!0%U<bzhD12P_v}k7$ZxLV7(ISB&(ISZ==_1*p<3;90Rz(TAc> zMW2gz7jqT!6eEfc7auJaC>APKEY>ZyFLo++E%qoz6`w8kE%q-CDh@3^UyLcfT%1{) zU7TBdrMR&8YH?|Cc`>P&TwGI3DIO`_yt?bE%vGzan5#KgTd&@^`laMpiFk=riEN2{ ziAITjiBXAZiABk&63Y_n65A5b60Z`Ul5-`2CBY?WB?ToVC1oX~lIoJ$5^70(Nq0$K z3A1FjWWIz|vQn~9vRU$| zxU%H3vNBQ`xvZw_dRcwhjk23%tz{i$-DQ1c^JT2E<+9bXyJhQTn`IBn9+y2WdtUac z>`gg$xk9;JIi|d*ysvzv{9A=Ug6_73d1@3cregir|XSitviq3PMFf zMRG-2#pMc0MQcS@MNdV4#c&0qVyuE$v0U+};zh;ligy(sD!zi}`)niaAnhXUCmkX2 zlY~ejBt?=MNrR+C(jys=j7g>>FH$J!0trXLlVVA6q*T&nQYI;fbcIwvB9ka2Dye~V zlhi_LCv}oWN%N$qr01kpq_?E^q>rS}q;HiRm9WZPmHd@Ll_Hg5l@gWGm2#B|l}eSW zmFkt6l@67Wm4%g^mA_PNtCFlTtMaXiuew~7S(Q_jS5;b7T}7#?t7@#GRW(<&Rt;5+ zRE<_KtEQ@Es-9PUs`^IeAa5t{CUcUx$UJ00vItp`tVY%#Ym<>=1F|vMlyhi?ZP4t@SwSa3` z*P5@*UVB~5Reh-XNHu@8V6{}WaS9MQyfAw%Rqk62GS-o8SsQPpDw;GNbSk11Qy)|4l`)hb> z4%Hl~QLIs^QLE9Y(W=p@LDm@57}c28nAe=B@vBLyxn9Gpd0Bg)R=d`v_CoEI+Ok?w zExES3wz0OowzIaowzsywcCdD;_IB-^+Qr)C+SS^RlpU14lzkK)$^ptD$`Q&jiY!Ho zqDRrE7*WhACn%>WR+KZ8Kng%fq@+;NDH)V(N*<+vQcNkOR8XoYos=F*A7y|tL>Zxs zQkaxU$~0w`a)+`&d3zmxUE{jT^_c5b*BRFzP`6WssWMb~suER&szWuQnp00wEvYtC zJE{ZKi|Ru?M-8NgP{XK1YB9B(N}`gf6e^Y4K)pfjqYh9fsduQ0)Me@~)J^I`>J#d( z)ECqb)KAne)Ngg*X4N`G-GRCzb^LWgbs}}hx>I!ybxw6Ib#8SYb-s1}bwPEZb?56M z>tgEC>N4xH>+aNz6)m79r)^*p-)Xmi`)GgJm*4?YysC!WNxb9iqi~4Q#JL-4U z@2Tgk=dOp>AE-Z6f296cy+FNAy<7dIdQ$yx{nG}n2Kk254FL@i4N(oz4Hp{{8i);< z4cQI34Obcp8psVb4c8m$8*Vhv8m1an8rB-_H#}%~+VH&LWy9-6NaMancq4D)p~hp4 z0*%6rqKztz`i)MFu8kgzsK&F6zK#BkL5-n}=NmDN7aB7ga~ks+^BW5ruQrx7Ry0;M zUTdsvq&C(!jx|2M!FfaBhTVZc&tsM1x7JIoWvxA}ORXEN4_Y6$K5c#5`nmOM8>Ee+ZF}2} zHbmROHomrFZGvsWZOFD$Z8mLoZH{ejZ60l?Hm|nuw%E4#w#2rSHey?5TTWYETUFbQ zHb&c6+eF)R+icr>8>?-l?QYw8+h*INwy*8b_HFI3_MPo}+V{2dw8PtZ+Yhz#wI6NQ zYIknOx0kgKwm<6F+ac3&qQkEvtRtc$s^dZjp(Cw>*pbzd+mYW<*iqb3(n0B{>uBtt zb+mM}bJ)bX_AWykA|cOCCLVV%1=4|E>u6zmk~6z`PlROnRhRO{5})bBLv zH0?C+bm(;LbniUVdA8HH)4wyYGr2Rbv$V6klhj$&d9Aadv#GPWv#qnUv%7P+^LFP# zC#!R%bFFiu^FimM&i7s0x{h@5cL{Zgc1d(ecgb}rcByo!cWHI$blG(|b~$&scDZ+X zc6oJqclmYscLjBYc7=Ckbk%pwbiL_5*sa^`+8x_n+TGnf+_PX0^k8}} z^jz$T?MduO?n&z*_GI=H_K9%j#E&y$|_J>PmcdSSi0 zdiVD7_8#gz(#zi~)GN}f*sIp7(W~8y>^1B)={4(h>^<9y>y7D+>y7VC>P_vv+?&~( z(|e`2u=i>&t+%DOt+%7MtGBmzpqJh|(tE3Symz8^s`pvno<4;>n?6k6)xN&IU-~}x z^Yx4Li}y?Q%k-=E>-OvS8}*y^TlAmockXxVKhuxy_wM)YkMGawztUgWU)*2bU)fLY zukLT@AM79PXZBC^-|lDiFZch_zt+Fe|E&K-|Lgv@1CRmOz|Mi)16%|92Y3e#4yX+1 z4_FMG95_8-HDEK~I^Z#Y8aO-PJ8*6wd?0opejsrmWq>%4Igm4uH&8WjV}LO*HZU;Y7IIM;s?tH z2L~SwZ6A^vG9U6A3LANjNA`WE@;DeS&_QK1W}muh4&?uhBQ?ujyZh z5yJ^$r`>@n;)j2@01E*%~i z-Wb_2A~#|)5;l@KQa92v(mv8P(latLGBt90WNu_(gf+4}vO4l)6#v0>3<00b-;~C>Q;}zo#JwQIi)Yag+GT*va(CjLGcD zyvc&eqRH0Dp~+j5W0Mn;wduhryfu5nO2x~ zn2w$}4LChSS;hQ-+qcvkXb8_bN zjP;DejMI$kjQdRB%!QelnM*UkO!7?X%;lMknbMi-Gkr6IGs82tX2xeGXJ%&RW)^0a zW>#lj&%B#?KlA&{ry&9BK|TcVP}Uho6g` zBg`etCC#PGrOy%PGUw{%#^+w#;kl!B$L-FgJ7srz?kwHexbxu7<2z69yuI`J&ewU! zJjeX@`5p6!`GfO(^T*}|=Y{8y^QY!*=I!Sl=iTPd%%kSL=ELVB=HuoQ=ac8t<}>HB z=kw^6z@ycT1V)0_l z;`PP)#T$z^7h4u@FWz0ezxZJB@#6Evmy2%}-?6r_cCZkvBP@QF5KELL!;)huvQ$`V zEIpP1%a~=#vST^3Tv_fcG|QXi$MR<-vT|5gS*5IU7Kv5Gs$(^>Xsi}iJFAm5#F}Q^ zVJ)zhSa(@#toy77tanS$rR_^Qm-Z~}TjE~gUy@oSv2<_g&GPPLfo0X@Q_DWf7nh04 z<;(Y$A1wd6{9$GL%Dxqz6~xNH6}}ab74a3R71qj4SH@Rv zuiRN#Tv=XOU3s$dYUS<9`<0KYkkxIgJ63nE?p;+|J-eE@n!Q@KdTq6KmAcxn+OpcV z+PT`Z+P6BgI<;1_c73gFjlRZM8(W)Ln_in;n_t^ld$9Is?djU{wU=wJ*WRulUe{Yc zx1O_pb)CFkx8AbezCN&iYkgw<-umP9XY0?`U#@>%|F!|$*uJrIWA_Gpr6_c`x#-RHTFxSw+W z%Kd`-MfXcKxi*h(@^1=mifra=mT!_at2V1Q=QlStH#Z+{KG|AC%)zmBI G_kREe1%%1~ literal 62479 zcmeEv2YeL8+y744y}jGLyWP-x2_P-KV}XQFBAw7%Jdz_EB)NEZp^EINAohw~B%!Ea z0Si_H6%`A`V8M=Eu=lS2XLfHxLP7|?54`XD|9#NUg}dFInP+Fd&ofV(nVID!fp8=- z@ihSnSReuwL_rc9f@@N@dHzs15G=yP2dxckpcZ7F^ z_k{O_4}?F3zl6Vqe}sRLfDponAc{mJAqSF?f;>n=I*LK9Q5)0=bw*uKR}_nSq24GN zrJz(a5{*Km(HJxq<)d+EJeq(eqDg2nDnRETKMJC9bS?^^Fp8k_(Nbg}hAu$M(WU4X zbSt_I-HukFJJ4!WiS9-l&_=WwJ&ksu-RKSUE_x4rh(1N1p+o3P^ey@h{e*r-zoNg< zKNw>PJFtSixCL&F+u*ji9qxpC;sl(Avv5CrI_{4L;#@ow=iw39k7waxJR1k_96T47 z;8I+MgE)*A;l=nod;z`?UxqKoSKzDhHTYV59ljOchHuBK@SXTR{1AQ=KZYO2PvK|q zv-kzP6TgB#!Jp#K@aOmod=MYPU*fOu*Z3R!1O6TVf&V0kVB#VQQHh&)NK4X+v?lFI zXVQx#l62CKoJDfTKr)0BkaLKS%p`@Ri1^7YQcPx(04XIAQbFdC^T-mij4UTtlB>wo z?fa)PswNGbMgiGhI~tYBL9$osX$#+ zp(>4`&1fsyj<%#9>(;PaGj-sRK7&?~b)0^q-bQP_lYv`Ty zE_x5Wmu{vH&@J>4`UHKFZl_Pv=jikFCHgYmL-*3x=V(Ev^Yi_FHRF@iiP4_u|zBt%fxx&eDNyrYVjKJTJbvZdU2(AgLtEO zlX$auySP@|AZ`>N6dw{F7PpE|iTlO3#J9zF#COH_#P`Jy!~^1o;z#1g;^$K6j z__O$n_>cInBuGedN-n9n)Iw@0wUSy(?WJx~cd3VTnv^1?N@-HMlp$qGSyDggbmV0(sXHtR3M!r`6RzoLbgk#Qdo*e=SfSX^QEPdA+3-uk*<@jmsUzQ zNHkS95Wq-jv|NOG0QQ>ajs*&V}ZkPFvkUsOC6Uv);ZQY?sVMcxZAP8vC(ml<6g&o zj{6-CIv#gycRcNQ(eaYwWye0pYmWCFA2>dCeChbg@tYhYHk8LD1R^i z;8dM%r^o4aYEIo5<80<^?rh;~>1^d}=j`f?b#`<1cJ^_mI8&Wz&i>8;&TMCnbFg!y zbCh$cbDDFybB43Pd5&|oGvK_?d69FObGh?k=L+W~&P$z_IWKo!;k?Fqlk;Zh9nRIx zyPS7BA9OzA+~(Zwe8suT`MUEB=Qqx8o!>dXcmCk~(fO0}XXh`@U!A`>{~}Mhh|B46 zxnf++T+LnWT^(F$u5?$1E7O(b>gPJ$b%yIq*IBOqu7R$zU87xNT$5c>TvJ_zt|HfY zt|hMXT}xeti@7dvUFf>Vwam5Lb*bwb*Nv{5T&rD`uDe|uTsvIPxt@2u;M(bW(e;w+ zW!EdNU9R1(YS&w?w_OKZAG!{@4!OQ`{pkA1^^facg(!-mDy@}7B}qwEQj}CBO-WZW zluRW{>8G5f3{r+E!#Y$}VNM@|p6v@`Z9xIi!54e5HJ?e4~7;e5d@R{Hgq< zVwI?hs;X|)qk7d=YHKx7O;VH96g5>%Q`6N9HB-$}`>AKCgVbT_aCMwIUY)MaP#35R z)kW%J^*nWndcL|;HB_cvpe|RhP_I<4Qdg=ssH@aF)Ya-;>fP#t>O<UQ;Mb(i|7 z`kDH<`h|K>J*0lAex-h`exrV?{;2+;{;B@u#%|(P+^XB{_PATRTe*|m$?g<)syoe{ z?#^&$y0hH<+^4(yy9c|6yGOXkyC=A3xC`6?_Z)Ybd!Bo~`$G5a?p5wP+^gM{?ke{h z_geQl_j>o8?v3sT+>g2+b3g5V#{H~&m-|)sTkf~rpSeGGf9w9vBYT`4mq+oa9=FHi z@p?3m?uqfV^mOoa^mOxd_r!VPJqex+Po`(AC*L#9Gu|`7Gto22Gubo6Gu1QAbB?Fj zQ|c-6%=66mEcF;srZt>jexy^ICXO-s;&uUMlr^>V5bFb$?&qJOkJzG6bd7k&Y z;Q7FF!1JN!BhSa4PduM`KJ$F;`NDJ1^R?$E&mW#Yy~Inss@Lu9?(N|{&D+!4%iG)A z$J^H%=Z*I!cvHOnyaT-1-aPNw-m%_%?_6(*x71ta4SLJH=Xyilus7nZ@GkTk-sRql zy;pg!_TJ>Z*}K{MfcHV~L*9qITfC2WAN4-wecbzmcboTl?do}#dJDa!-b(MHch}?ec)g!~y53(Opl9oO`q}z0eYieDFVN4?efmtjP%qN`dYOK% z9?~oHdHOdx4uW;tG}whr@ya%pdZjb)IZWc)<4k? z=|AZ|>%Zu~>c7R{7!o7Jw2WyL(>kV2Oxu`tF`Z(%#>B>)784hf6q7o>TUkX($v(j? zcm%JY3C)C-6G!I9&-E`1Pl11(ShRc10{@h?; zMXA3mVz=B@=qmKSQ)nl&7di+Xg-$|ep^G6JlHo99!)dq-U!a{;yS;I~nFE7mkx;Ot#2+fiPE5(n%1OzL z%goM9jZ4i=%#6z(l#>&em6o2Kk~1(pBPn-af!3p@Mkt-*3r+NeCj`QQnI-;-fynIq zKv{8#AMO-rJ!`jQ(NHilf2=Kns$EHmP_~+;PXI2y!!-#3kM)*sEp@l<2zVg|Jui7-!v@ji-8&njC z*6Xxif1EHy=)F!DFH8_73X_D%MjNB8(avaZbXX@$6{ZQ(g&9JD(b4E@xf)(Y~4KNnN1zLxr zRFW4CSNOyB3ls|_LhrT0Y#|`b5#}15jLt?Eqw89sR45aILa7mJbT@jyUm(2s;6O>l zA2LCgvv35w1Q?t^&2V2hQbP&9T%ffaUpA}3hg(~9_>0Ep)vd>-XTGpNXt{c|(XCoo zESx7S5ze1t$%r!Gy-C(re|Xx+{M-PbJy_-oEv$m4mQEU(KgzU+Eil)iCqD=%fd}n2 zE)XsfT2=}d8mCnX%Z#2})Nlr9uuL^buSqc`+eXPGcsU+X8iV_LLn-?>Nc#MI0)2ILMMK6>2b>E{#`&n*j8EIiM+XvJk$U3cTn zw^y#a``%3tJ^JLfXPSoQW6h*_;EnBs2df8jg zgpzIbHHy-1$}kXdvjXAS{!n#?j-8rZM`+u0sH02wHCU0hs&(AZAeZ|s2KxcGY*o-=jOdlWihw{pR!DhiUR-M)F^y8>=KGb<;!rHT9Gy2z}d*bL&5ZKUP zot-l zRtHtjo4=sR@LF8_0MxN4VclZh&$Vk=a{kgL*Am+H3Dm;$H5bHQSgVfw?DF#JWy>#a zay^UZ?16eNNvOQERxQ<+UvXuV`=nyx8mQswgsN+uhrMDyrJo}75@yi-K1A4 z)wkSwThogLUxH$*^mTWD99UheahS@FWU;C>Yn$9Xq3s^1cfG#m&b#7j)mrOo^@ff2 z97lC6p}PC@wf6&=wJX{Dz=KVG*UKX!@w;lh(~M3T0de3EW6tLR{1S7 z`rS`!zW?FJTHRUeYk}5gtlwV(MvQ;{VG}GqJU=iiqB$Y@p@Y>IFPsT+jTL5`;x^$9 zh)>)vY!TM={qyih?8D`FAwD1c`d8u`z?**`-in{c zFXLD7yWpk&6}z`w0b~%F1itrj@VcKzE+&_Qr~MXkFL{zYM_wYk$y?-o z@;&&{ozz2HfCs%h&7?!XUp}5rrbRSB&!d;o>rKD;UEnR>4u0|d^gZx~e?xx+zjq6< zqZlimCMJvN;_2Yko-WP;Uv{auKwJV|>^sD}#Cyd};uGTY;>+Np{u(^ee~N!g7(CJ4 zq&O)Vyv_rq@!)AL1&{JF=~C%R>1OaB-z_~Uy$rtMx1{%_gVHw+xfcj>d&Z8561TZ?(MKlAt0e}?!aIvw{f@; zFu_++0UB^Cu#!w0bXGEJ)jEf%g1tO$jG%Hh6>vclQ<)_aci&+@~? zLd&vEf~SW1=K1r=z&`Vplq|Gs88~{pu7%Q{sYSSII zRoGT3JZ1DVPOl};tO0&TcovM?=i6;akx)fpq#^`h>e|?I zmXo4D>(bamQFlnw932Nh&iB3tg#Je$_CFCm6+RO_2l0PUI0RF`S4N(3wlT~YZj3NS z8l#NS#u#I)k#CHVbnQ0##60j4yMf)F)BEri~csJSt% z616m@bI^6E0Mm4Ih_i3VUj%Lre=#`q1}y+N#N}6BZrEzAt*~(HZDWbQbE5 z1{kxAfHB9IYm^wJMwt;b%8hf4&_tAtf+0Ugu`d(|2g}TPhpXMup0M1u)@ag z!_%XK22wpKrO#B}_236BGJWVBfXOOKia4u{hOpUK|GD6zj{u~3C@ZWH`<<%LI?lms z&|2eiMH?A`7Frk@P7brXYIP{U($)jf3bsoNF++kd6R2l1xYmEK{T0)IlQ>Qbn3J*tu zYP2P5*!G}J>(E~GDyl~N&}+tZ#tp_z#x2He#;SejO|&20?lEB%dS}YvR>zz4KyRcN zE1}1t{$S3MM_SD0q>J81A3*d8*zB-P1T?^4n)QtK1wvKm0Jn@9ea5EdBM|WK7&n4I zUunCd?OUIlw{GUQ>N>vd2fi{NxRpOpOGmcvesA8r{fN7k;_wRkrHbpl`_OODBfq0R z(4Xju+R&NIl@|bGX}Pi5s5C$=UcuqpFCig;&l=$bm_v%^`23}TvV>g!Jby{ByukFl zB#a+1Fd;cHIXNz=ATTOFfj^Tl-|wHBkpTJua5)dG)c(q=t8wWkM0?-<_voYWF9w>L zf$X$lWJ9wGtX$~)hHgQe3KLARIE6=jtU)u@8fyx)+!OIuX4hgFJEsC!HVaf?m$0<< z1?wplyRFc59c&*)R9!}|!XExkJk;H!=l5X^UR6g=;AZF#o>DN=7xqtue?$Hf2>Q(P z1K;0ithPvLiCcl?1mdC!w={dMykcfapwMy>#f5_vp+cB(<7&8(>wK1&w!1!lw7=LF zt_bIw&ki)Bo1F`JwBHOr@PceU89k1>;#jm5d`&&jc4HIBwnvS7LALEP9yvm`;a<46E!&KTjr(koV+s(Q z2y!1M;bfd*+;2Q!JXj+*aA%wWth&kA%;kjF^sDjENz|LB+I&}wE2Ee20FxCTI)W9W z%!dbY<}F8q@f&8O{Yw6U1fxn=sXn2$X@+SPs@pgSR=@1_mMD8^VmBi8uI6UfDb=a|CqL z`n8mp%3#g>2+zdUuf>c(gYb&@{_qKH}YCn;v0-t zjp|ypj-LfRaAbX;gh01fl-JmnJ$u!c0(XEEsKiwu1=iwqAO-e=6gXhK4pQJZ<9jh!>R>1%19e9W=UJ)4{-jH4)D7p=>%@TLd&CFPo*&aWT;Jp z5{1N)ZfGm%VM`Q-i1C-Pk)hW0WD4m``q(msA#D6zN2rh_5Go{@q>xnOALCz!ga)O` z(d7o|&*cU~=-7h8?&6^&A6S;;k+aD#GMtPcBgrT-nv5Z18G;m2ks*m82SYMLPKI0z zDGaF$xi^q;7W0xx7W0y6LQjS~7WXm~W3ev--=gVSt3Owrn%N6MGb!>%6`kXn*~uK> zUow{=ZH5^_85%2nhJvYJ#f)PbRn40U3t za}6_+9>Oo2&00geVx_IZ*P8YlVbDz$gLXNBLElEdk%!3^@(6hp1kq#Yck(!S0`DMO z$y35EVHep(wxcJ=cJd6^J7LJev_g6%6{Te(D@teb4BQxh8CMD+f>GcNOVBpL*O2GQ3w#8R_CUi1x(%-v$!>^VLWuum@(S6-P;N#E&T{HWhp?)SXX=~aBTpcw9 zs%Tqqz|^_`%w(aVfTLzy(8NPE1r>pIq@B38qzIg}qjY*}-ArZ4(epoDXZ(St$xfG| zx_KsXOta=KAS<{{+ji|cwC~ip3nc1x?{V7nk@>Y2PyiL<0+AAcj1G$#tQ*#n27d^a zhJdF!hevMUvAi%i$0vaQCND1+^1JiGBf#BicF*8Ih^JtO%YzUU2|*7GbllLqcbFIoVB zc8hLH<`~Ezh9o-;>;$8N8V=p?YS<7BT@!SI@7H2__3qO*E`rvq8w+kHAwwxLyL+LXZX(hC2aR#tqgzkJ{F}p!FW3qp1>6 zGxxA_cpv08r>19eGoxjJB`e=_zNN3Gvj{(BFk2zrW1t|MBDVaxH%( zG+p-=kBM4~aA2`dN=9yGCST{1oSU8=mzWI8esZz~XT)U=PD#v8NleOtw1=tDB`8^m ziA$<;2IdZGXmJ3HPVM%qatGHe4tQ;7Udq|SVuz0ae~LYZ7L~P2MA!LrtouZLxU{MJ zKrM`$t6#tidX&X8(A$R-F`|oVG9VKJUTMtO8gN;JRgD=8m&c8-yX0Zq;HilVl9H2r zg(+#7iDt4}TUf=^OGt)gIC(JYi-Ze=8-z+&lD`QS+4aehjoDIv`&&I>Si#8gZ zXOnRRUjhr*ufSK~EwD)a1u&Z5fo17m<6rQ`fdN^bK;qnwCh5350(g!~& zFNeq0atDp62o%m89t_U4h>t#3pk>tY;2*2Hd>)jY8T5sU>^B}q=Lx;<*SbJ<YLf)kx_n^geHsoK= z$oL``={a>dU-02{u?6-m7Xzia?tf)5W8!ghu7if`_L^@xX zmB9ZTYveasq)aT(u0Bbm)E)V{>(CO$`ipr&5$Lt`Bk#3)>|u+{$pzZ%lSC#gx8cJE z>uMoSgQv>ZZvJtr`KblkLe`k-*%PT9dh}0)>n?>UE0`2<$5fbg7 zy$$+WOW%&gxjQUU3ktL=PZFuHZmxTBqM3M^@E;=XMTyosF{I_ZH>n7HB0WiSh}7B7bmniEp7bH`nL>Un~;9%36AoNT@Bu zn<^{+w5X~m&~7VJgb8h)uomB}f!e%BjpQM9OpO)VCm zBr1u*|yl#Vl9DKXo3Ur>z={S)&S>jm8=@9c7x*?j=Ax?lq1Q-YNf9QDY|5Z_U zE38H@5L#A>;19j2QUsUi%}vYY5R1fOGnYf0#dA4sfz%8!V7$W6tqmk)-Bh!^fZb|P z44H`?;<-GrV^t%i?Gg*b3ppGYii^a>;(4%i{(Nz%XoyS%f9z_8AjP7Jp*0MxWoR8k z;JF1A=Pris-XLBSh2x4S953f^++f4;o*FoAJ_W}UpCTyU!r^!;LmO>4u4)vH>%==b z9AW(KtrTJW?rS0(?-B2};CLU0ide)rTYg*T&Qq@i7a9k8%_~&`2S>#5VC| zSX(J>7oQfN5uX)zh|j^c4KIj0#TP|r_92EIW@rmT5Jq^Ep~o0{oS`QedXk~73_Z0` zd?gCSz2d84wJ954=V07sgYj90b~5y$B_3b?@5AFUGSN>!t~=k)(F#bx3gmg<;^i}2 z^lPoiAOz};k@YTq!h!oKgY<4I?Opr=fGZvp5AhE}Pc!t4^%rvEbQ>Svi9c}oe$UX3 zO7TaAo@*j}e-;0*;QKp=@ADjqe{uM}P!GP^!PsrzSpnK;qp(#7q{={syF@vRUuvYV zT|$u{J!XxhN^Z%+5VQk<-rZ{?P126|&$gJwxgsbq4CCjuIrt zRWr1&O6nqYW#~1AUN^dpFXYK2(+h*JhhcGrog307FK1w|Fz6c>tSFovrO+bCuC%8V zC$wBE^^$r^eWbn&y}{6%4DDy=t+i6TlprNaNesQsc>3MD47~>p1&RW}(g{U?nL_^* zUwOG7Qr=+miK)R5-&?~B5zl~yX7Ps`F3?VkhB|X#X&MyEub3I(YcBa?fzh*rW&ZSn zqyX$_QK0pz`y?pT(fVuE7eAb5j2+&LAzJIG!vw^ShNyGM7l_36-&+>~XG#zz-YA_V z^_K=n*-{Q{8Ivmwk_Jmdq@fHQVCX}JK4R!&hCX2kK>Zm*pEL9YLkAf;v{5?Sl1tJ^ zX_U}Y8Y6U<@+AnFMw93n`j(-ej8+UmCOt!t?f4syGuP*!|9^9F>%U9Q8kqlyB{)r# zS{4t@uzB6-%@EhoP?-`o{VTg2~P!kjkWT5KK~#q38MnB&QiRk7eUv- zT2$A;Fwt0cAa3>Fr|6sjP39Q7QX>+}g7cdk&ifhGqxy^VPGgF<^pW%l7Z4va+^kai zl;I}k^uyj`UvU;WgqJbg!g#4l`WmlbxFuvU*IS(9eZa=lkJ2xeRQMUB0&dNv!q*^G zpxBXx>=J)D92|vzOaDm!It1A53_FN}Iz$IRur0&w7;evS2ZlQ`+==1N40mC;E5orH z9I}N%hiWY_acCTc-E0*0h%PX}eNKV!#9M!kjsQXj3`qAV2pwGkgbgn+iH@|Rm!l5= z(b1dX(<&W(88-KXKKANSN1`KzV``Zr8D=WntICl|Krr=gV3nB3AC4?@&8Xe#8IJxY zA{}ROMD}f@v|VD5V>BSrG1xK0G1QUgINLGIG2Ai2G14)L;dq7<7*1q3iQ#01Qy5NV zIE~?ShBG!e#zYZ0-ja)s$sCc8^l19WaK9S4$Tx!fFVnOB_vNCam_u?l!&x>Y=Qbu6 z9U(`A!!gY8>6MNOhRA1>qwc{GcwT|l?*E?1^ZUDs}=P*2w;ar9XF+7;zAq>OC zJciF^7*a?NZFJlmh2rhvtMCn+wT>#s8it44U>pe!3HZ+4wK$*Q@&9djY-AQa`Fg)4 zhctsIX}GCZo%@f5?On~29}9M4&J+`;iUhC}NGj>oa}@Tl3Q zx#Jbb9t)1UIUL6|QpSek>yEb?RNfu$IKa-{!|()#CmMa~YTk|mybC^Lu=7Jr)1Np# zho)E4l+EM;VDhhyd7UVz!P&64?u&OxBUvg%OdBfKOBEL{&M{7_{Z@t?39XR zEECRA(-@x4@C=3v7(Rz#AHy>lE@ZffVL#_68P+HXuuX;RvN=lj${NG7Y>t{!i=)aI zE0(ye5T1)@)?}3f=wtf zP#)4CU&(p$*(P6|%W&vOzLH1sjsSHeT*FuLSb2P1zJhIkCo&vixS~mXB~O*l`H%QY zE|x=}FUhlEhv7N$T)9LpmCNLyTrLCY^BG>i@IrO=cURTdZB&beqg@We4pLV2wWp(1cC$?G{6t!P4td*w$t1n-mYmp93qANSKg$1tb|L>H|1AF^|0@3`|1SR_|0(|^|1JN+@a+t*V)zb* zS2JA6a23O#jIU*Q9mDGxzH_5fuyE+4;;T-H>+4P@N8(*J67S&wM7-Hj*q{3E17i~% z5NB&ZtrJ#r-5o`(vpt~J*#Y*?hY!OW7~W|81vSK@B}Osj?Cv}b z(Ch5M@V%8z7~3XBz@2@a38nyb#&ZG6BjC;?uHZKm0bgV{mhS8adWAE?nd!`8_yL9= zWcVR)wmZ*oo@we84>P>A0cZR12Li1=8OZwt?9nYX8i{j=^K78ImZlF!3Jhl$MDn9e z(ny@6ouJY*tm(tK1QVT;oM1IP#_;0|Kf&;mVDJ2&@v+y~LH2m~oJFQUaTaocvaOLp zv`fr!!g9%t&be^>LaDRN8FZG*e>g+VuruNWZ^hFLKf~~|4DVq0IfkD%1c#=SKy~Vh|`xOo3u>fx>eSc(xYaZ3>iXOQ7uk?-MBh`wl55Y+hIC1byQbTTWaH zasqF0Ue7DkPlqH12-d z`TX&5cRilk?R?eZslA-1-fF^2zv=u4^hD===UdLVo$ol`b-w3(-}!;_fb&C!L92L| zVbChxXZQn#4=@ZDKVtY}hCkWh{5Z-^pIdsO^GnW5pW4g>@%CuQ>+4gTbmH|y=kFYU ze=z)+%}IX)Cy580|B44;tRQ6vL(;?3upgiePZxE;#&VS|k>P`tE(m2FY8oTC6qg4u zqIQ*C6a01|v2o2*f-3HY(7v%hcJa`lIEvycoJ}UF~{KwM(iE_T$eS9!)smFa~xjBh+gSh$w*8Sad@)}6cPx& zx^4xg~7EPgM2T*W0lzGdIU6G*FCO#UH7@}cWrWQc0J&F(Djh( zVQ|coR*bY}qzxl&8EMBzdqz4i(vgu)jCAIXdDo*+96o6~=3P&_o?)a*R3egYTq0Vb zF4BvUKL2g5#Q*(Jmy7#xTzeVmY70f~$H7}%uk#Nmxg^&5vr6k|i>h~A?{PrB%SiW1 z*ZYk0Xd)m#a(!w6@)Hio(*Rzs&%qidkWFwz_*Hw@ZtE-8w-zM7;gIazNEy4t&#vDa zbj!Q`bio9>hmpRF#2x9DR|Mz=1u+s|qlYL|aX{0xj9^7pK#nFblE`29n2BbJTY)`p z8#aQa#=>1n8>JfvcBQS-PHC@nP&z7|l+H>QrK=LlNHQZSjKGAF#z;CN8H{8ylEp|r zMo!1A!muEcS{&X0TJh9f!4+LE1-oKtfB#LIQ%bdI4j7&$Y_N=kpuO7%8n zj{;R0tPBMdm)tKZ5b>5(Ua!kn74hicguT6e>lEUzw#8E3?HHl{t(IVq`EQKsd;F zApC4bhA}dnkr8}N1sSjJwoj}FF`d3=H&&^bu8)<<8jj1!jTF)pyUa%A zelFhYEt60-D-W3BeHtU@M4cbX!@Lu=Ff#qHRDVo)@(8KERe_DVW-wCFG^zfK^1@N2 zdV|Xtj&yz~dz81ic;BnMs#Gicl-HEkl{b_(mHi6Lwlf(iWTc1@KO?gkDQ09gBLPN$ z>*j7y-ih+n2g(8EL*OcKZ7@<|a}}ul5k@LFPtE)9o4$BtPR+|m47)${liFkr7AFTZqqnQr7BlH@G@2692EkgtvXGE zFkGLdL`UoZE6ZQd+|hJgMeKm%2*ZjsRu#2=&x#FR=RokiU)edS$wUgRe?V@&7 zLGf6~U|Vsrn33}sS;ENqj4WluV1zMp!3H&|c&NQBi&8Zej|**NE~~L9FF6Iw6R&xw zT=P&t^SCGqOqFXM;9+WbeX%-N<(h{I6ZrB<6(;bDn+VAfD%U&!lA|~zn-JYsC#aU@ zp_-bjI87qr5e>d)ND8U;jsOg%yY zQMam~fUIL=eUlUr^%?c(3P@vSrMg>%M9>ZD9(Aw!s#>k?Q(seGSKmy^P$)$o-6LVr26M^=(VPKAfc);co0KBty+o=*}qk;d8nXyJW}Zf&BKh{ zAA8k?o4PH{!)$6nzeVP$%&+T=CzXM#@ZaCA2k*$rDvx~HLgTk@V z-NxP4-Ok&8@t3g?xF^TynB|r*fcJy8QFKFkaxpkl}dLBBZoum?w~sa zP1o`ry2Eaek*_n-v=F;{fqThOt<1)Ry!#^e)tsZ2xtF^ycCT<>;=WY=!+p8?3ip+6 z(3wEegOT|*BkwTsE`t+KAeoK4&&UUi9AMb(0&uFiiB{NdileKudz6Z6_FHiztR?*tC1pPc4?$qm95Wb7eE zzKObD+`IXkz_l-Hv>$i1`}HFj;|(`#lKvGVUpGnnalhj}a5TnfIJ4|X_lx@r_m`Gf zIK;)mca6NZ{nFpN{{qFy{e$~Q_fPJh-M_ehb^qr6-TjCAPX;IKkRKWOiIJZffw>sm z0KYNvJ0pKE^5+Kk-xfQ0kQMOvh@6@JvYF{$ZjA7CT$G;Tq!Z6co@RhQPjg28j&hQx z6>yTLHO}B4M*cB2n}0xSazuyP)5+5Xc*)b5QK8b)l~L3*Uh?$t^fGzL(-U}!^6Q5f zDO9-D>AxpvDU@eAN97DgV{BCV0F@0dg|Y{Iwr37UWq{Gd_FbE)St zM%yylj?wmvc3`w4qn#K9#_GZ-Fjnja&lOQDUK16LD?vC?dlvxOqeeKM+6EwcoJ-Re z-5om>-m`|oa4n4*~DRZKclBrdNwoKvxzW# z*z>3b!$&v_djY6CkAtE?d)JqZB-zH*HqSE_5})Qs?Au5oyTne-S0ldUL#kObB{&IRr;H0Z_xdBP|Gy z;1C?!gc9St1ssCoy%W3>y_39?y;HnXz0Ai^2 znN5V?3h!kW1TWY{_TfMh=Z}+b9-r-&Ct@KuT z*LXqQC}wmvqoB#oVH7mk5=KiIEn_suX!!>3`X~f9SSq3SJ`TZiZ3u>IRKf+PAb1iW z_#}tmRz^cM1h+Q|!56$QatQ8ZG*amWFjX`Wg1fzZ@sD>8cyj4H@Z@@{!IMkpH{i+D zX4w<#e(yUL2;b%)TnHfaR&y0{emxb^F7cuFYcQ0(A9+9ae&YSq`%akac|CyXsHU;5T6YtbS~rf$D;p_flh;Rs!&n;#BWp=o zGWVa-s~NrK$Owa$&U+$*(Q9jLF|D5lDoE`JgLanIpV8|Wy`I1EF|(7kff^)%{0G^| z+9+)doa3#HX7q+iZ7icV)j7*jn`oY%KyNJ2LMQ38kfR;FwIDFl?&c{PC_!trsoFGc zx(15R&5Yi{=&g+2wiX7~r_Im``QY9T7Z_c~dAE&y4(tej=}dnpuLyP+4$KPp`N8a+ z@^XeQoEZuf!8y$pg^`NT5$7qwQQcN)I3*iScbW%>CWYW|W7rYdS2oXQo)ur~$Y^Vi zWo?cY zkE)1p-xR-|7v{&t2g=~!vsQUIwQd}4A2waBb$w!>C^FmbwME)uq4(<5FrHBw&)1e( z7h(sLA21Wl>^m1~mkGV^)GpGNY0I^XwH4YW+NF%HXY@`+?_%_BMmI3JkW|KSRz4urAvheq?$ ziQy^kOz{_u3ziS}&-0f=hdURBS+ll`&hhc{)dMq2{ARFSyA5qxuidV#((ce!Yn56R zqxUhonb8LseVEZl7<~-5Qd=i@we{Ma+Fjb+a3p$s2)TFG8jLfWbI2>D8ES#s^qumRKkx%fIRQRj3 zdxWJnVfS;w7<~XpD9|PV0h!Q#Qf6jaN>W;CW_n_JQWj7Ezv45Jl9J*4@Wj-Vl;q@; zhA7C)Y?uPOtp}~P9x~e+2W_P$r^lxxre&qWn`We>WTd7+ThMeoym3-udU{HFdR9sn zZwk64H9jLFB`ZBSF)2AC4IZrft|=MGNlTX4Ej?njw58^)QnNDSvyw8?l2ekCvNF>$ z((CnW7IbHN{if2BGLw#M%j#FVttYIu9<6B$-X=aJEhQxpx<4x`Ei;4nU{-QQe0o}1 zR#IwaYBF3)NjBR`Nz91POijs3goEr86H^kCp{=Z}toY2##H94pbm(*F?8EQ1&1&oM znzoX&lH!x0cv^ZI?B$-Cl*oH9JuxvpEiEk(YRZ69&Y@}ER%S+Kd`1fNZgLv*byik7 z@57W-=R} zB(sjFr2A0&gcJLb_A#R`FuIdZ<)85>;>9|XgMA$M@L-|2{d{=3J#CEk75l?hw|oiR zBCLkMo*kjpzSVxHSNl)A_LrlfQtj7zW&YIu=F{w7jP9z`{$X@aolet<^}@Rgv~!Q% zXvXs7(Y?B+>x{z4zs=~o zjK0U{hjse2feE*v=`=Pk=kUSgN1ccppLF%sdV8*Z)muHFchupi15m$SXY{S;nCP%C zZlxZ}=o>WysrS%%n6Fk|hu&N7!|0og?r)OTr6=gHG2KzLuEtjnnAp;Jx{ZD&qwfIA zjhZtyF0D9j)X?ms&-PJf)N^zQ8&>H9b#Oku&*%qLIz$2)Jz&|<^)!N`Xce&o0#=&2 zUF`^cJg5QsNPUz(S|6j2)${dnjDEx@u+b-se#+=)jDF7O7kBCt^ojZ;eX>49pUTyM zgN%X)_g6;2sQR7JKmM<)0XppHzg{oaXX^odjy_k1`TP*0UorX(qu(+51EW9Hr~!J= zlHXt2Ghk9uVti6sT5@_Si22kMm>2qX&df}Whp&mri7=~Y!km%Dr8kJ^_+${;=_#NA zWF}?unIbtUC7lc5WSCd7lCl!H^p4I-de~~|YqKS;SR|(<#zQknDKPgX!8Di-vr@xN zCBZBk4|5kxKQL2*E&!6fv8Lu*O?`WmreMBHJ))@$m~X7P_BfkbY&G@$QJP9m=Nd#4 z-wKqK#@=eF)zpvnP{o0U5}%opo&x$;N@_-OW;*CwM>GXW7ayzS%+%C)nD${5VStmf zU{LF|lyZ2eF0@+u`S44ngSM5C1qvT8oS2%0y@50JR6yn50y5PEX26gh?F+ zDJdg05r#AcdKwhPl=wtY++gssU{+7c;B)$G$~iH;|M|n_&lUP5_2%@Ri&IlU8%az{ z1T8BaUNY5G=(5t1<5Sb2MNl9z0C`!~B(K9(ICtq+=~wI5=-2Ak>DTKk^&1%dlTnzN z{$>Ctmy#J5NeoTN^iPSKFd;7FUod6CkfONI{J3$M z^X(U^)YpJ{uU9dVR_bdR97)=3Ivg)F(pTz-IROlxDTik;yUgACLqf|9`UZWYevf{y zexH87zDeJ#KcGLzLB0)YHLN_ui7%H6-2oEX= z6m#PO48+<`VKB)lBe(0zPiF)TzzZJ_@uL>&fg;;cPH!Ow+XHqSf+X zUumEOPHb&ayD{@(3P`_ z=0%Fk(|6*Ic)H)<%%p5!tcY|rrcoQgn6(S)RwfuOx>hpegmQ(@;C{WJY@{R<{`VPX#^ z_A<`0G5e+dEhsAbSNhlbH%#ox#8@VFTcaG%zt-=2-(VxtouJ8-9)J773P5 z>J~J80pS<{Enf}j8<{`MztCS~asWK^s_YyW3I-zvx?gLfsLh(U?{HerzA(de%{p_? z;2}fvM&*y2G_ZRJJ(bChZO*^o$eg-e)7yQ& z?_K}(t?yf`#d$bqpR=E5pZ)A}_?^Aap)YOTLSIH-cPJ!05SNv6#njB)!obqX8f2r* z2~VS>XgQ?@ViPe$Eek}7*0(4PuNAho@Voi4^0oOfw_01ahQ5Nnoc@zmZ(X#fQejiV z^NBernUO)?TlL3blP?m}%OV8<(Yh@1(&YS}Fg)i?lUD z528ZC_mgrGwFcLZKuF9#5gHbv?Yw_oYqjUcvwoN9zaB+fBl3{e{{H`hl}3g954l=u zWXwOEEk%ZCf5siwTHpUrpYMR7R(evDw)+2&4_FCU3)lj%2G|4K03LuKKrrASARK@L zkN^dM5J)j|QEpQ!h z1JDHs1%?8{fDyo0U_3Aphz6zr(|{SkOdt-J4P*g3fYZQp!1KTb;LpHcfH#1*fVYA7 zfDeF=fKPOm>6qzQ=ve94=|FXSbb@q(bs}|QbkcM(wL2KFbSiZuI#Qj}Izu|+I#W8c z+H+g({m@n&;Exm#|4;f)qTTljxcwi?V6?)>0N~KJ_BW$dMyuDpGSV~B4zgOC=1{!M zQES$UmpK(Lb8a&-1iUv|Yh(m?4|rd^%!Q6DUIs2+=1NCuKU!_K{NF9ocZK;siL^&Z zYl&?C_m;jBh3DjdA0GY>%80b@vkon#mG8IbV|tbV`xi>{7#dz9t6y4pQX*614Pdg+Gh9@fR`X6fR!`#t37=IWAlDcY?D zDs(xzT-|Ej-2VLqdN=q>a>w_Bic~3i`p=>t9sY;Zs^_8yRCOu z@4hyu>4V-!y}$Le@k{zZ{U!R#^jGMw(nbe?^#k+|>8I=GX`>mM^t<%C_0Q;^)t}X$ z)1TL0)W4vAOaHe1UH$v|5A`4GKQ+)baL{_kh8o}v3bjs&Wm<>CN`qqtbp}EMkwK$@ z#Gu(=K4UC8Va=Q*Q17N z!-=giThUu9xAM21F}5&vF!nXxZ5&~oXpA;aF~%6D8D|)08dHo5jEjuv#tdVoaj9{c z@d@J*<6n&bFaeram{^%WOuS6sCfiMRn)sRQH3>EeF$pyZGl?+4npB(2m^?9gZSvOS zoyiAN9aCM?O{Qk1AXBiZr>U1I+!SFNZW?2nWSVSBH7zien95E2Oi!Ekn+}?)O(#sJ zOlM3rrmxL5n0c8Y%~H*1X2oVl%}UHzX60rTW*jrFS(O>ztj4U)tjDa^OlhVv8!#I( zJ7YF#rZ$@}n=+d*)0mwzyKeTze3`kKxwmm5t}(x5{?`1n`4{tV7Aq}Q zS?F4{_(EekDcEc-2|EN@!=YI(=SUs@%XuZo zpyeQakRfO-XdP$^$QWb_G6z|LV4&@wJ)pgyC{Q#A35o?JgD{{pPzHz$Dgm)T<)8`> z2gC(cfyAIDkQCGcY6G={IzgvE1EBMu`=HmL&o;(3rZ$c?5F0len2nbW+-AGYPMZiD zj1Ar<&nDl7YEx)KvuUz9V{_N$58E}i*0vDa5Zh4OFxv>*L$=YjF}88GDBC<+wyn@s zVLN4e*Y>{cL)*u;Pi>#u{$^)tXKUwX2eaE|=V|9{=VQ0SZlB#DyEr?PU4mVb9o??n z?xfu*yYqI}?QYurYIn!(iQS)e@9qAw`((e;euMo__M7du+S}Me?cMD??7i#}_WSL_ z?Bne->`C@{_WAZyhvg224j=~y2VaLEhX{v54$%%0hm#IUhe3yFhXsd^4qqMZ9k)5| zaNO${>=@!0>WFlVcT9GqICePBJ6?5s>G+4!DkldgCnpyth?AQW%xRmGr<1o+xKpH4 zloQe^*6FZQywedUoKvbt-dWJ2g0oojRRPIdwZJoJO70P7_X3PK!>r zoNhbab-M4o&pFyT#yQRz<(%P6cV;*r+r`Yq&n3mB!lm0~!Q~}*HFzC(BX|>d3)l+m1lCTEz;0j| zcpKOg>K7=#CrL%JZtkP*ljWE`>p`5AH%av5?5@*Ct88+Tkj9?Q)%TopwFzI_o;;I`6vZdcpOQ>lN2uT(7&n zciZ6RPTO(A&@l&_~cG&}Y!UV0JK97!0;e zyXN5si-VzHi7*C?2`hz_!zy4L7!OtrI}SSq>w)#clrR-+05$}hgq?$(hb_P^z%Ie= z!tTQ!!XCrk!rs9?z&^S!cVFeM>#py<$sOcw>u&Gv=S-aW}Z z#r=qThCAK;sC$V!%e~ya(*2k_&%N5c#=Xv6=&o|V;{I{l)@?htrEDwTc5>U5LrJv}|WJ$*cPc=~z< zd4_pLdPaN3dZIiNJTpDzPFN#-zSCJRXi|5thrSO{by65%E>x0)PuP@#J?r z-s`=8^4{!i>Fw>k%lm+LoOg=%5$_D|EbnY@ig$r`kvH9&;m!1xdXIVE_x>Bc60Qq3 zfUkwGhi`;Cz@czYI2^tM?hD@!kANS7N5f;_iST52Dm)FI0ndRK!rAaDI3HdEuY-%> z&F~ZOHuy<+FMJrTfzQDg;1}SR;8)9!a0dE5S4EPZ6Dd2O!H-rvi8Db@3H9{Y; z2C)vY5wRIzj4(r3B0vZ`L?!}{AR=-R`G^7p4Z%Q^Aj%LGh+~K<#BoF&q8`zRXhNJo zv?1CNazrM6jhov=G;ck*t`?oWGG>{+!(caOoI{5_?6%J#7LaDt43 z?1LPGoP!`iXM-*WT@AVxbaO9uZ~k8D-om}~z3=xe*|%)pihZm19or|`*SJrzPa13$ z3<-7%h6Q^BcLfgzj|7hePwd~cAGF_gzx{rv{fhmg`_=m=_D_c#2#E_pg(QTa53D+{ z@xZ17TMn2Us5~G%AUe=^pefWc6ddXr3Ju*BdMWg7=>5=#p-&FxA1pmsc94CL6Q&!s zDQruaahO@yVAxEUChT0;d^kS5Fq{@%9L|hbA7K$;6#QleiD-*B8POSO83~Sb zjf6&Si)@YTjZ{XeA_os89LhX|JCuDW=g`+E-6-=Yt0exTyH3q^RVm)Tp$mjHt{gc2s-ReAMsJ>!Q7)4@Vb7i=qdj$D^mB&qmKi zUyQyP{cH4{=zGx*q8~;75&bUuujs#{zajxh6QlzYjC4i9kX}eQatG2Ec>o!Pj71(n zW+HJ&0x}Pok1RkIAvwr;WH+)Gc^Wx@97c{J$B|RWv&eJEdE|ZMBjgk0Gvo{8E94vG zTjV?BU&v3$&oP^0JY(WwiejWOnwZ~Wbz>c3cgKdthQ}U?jgC!>O^?OKX2s%T39&h` z#j(s-RxCS~6U&X2#}3Ah#;Ri{V>PkoV&`KQV{gaai+vUQHuhcYU$I|ezs2dqEsa|j zXCCJf=N-2_&NnU~E-)@AE;#N$Tv%LWTyz{Rju4jAB``KFORQ?uZow%565fb zFT`Jt|0VuL{IBtka3$ed z!p(%+3HK5nCOk=ap71i^b>fP|)roqE28nAD*ClRD+?2RA(InA4(JB#?=%1LFSdu79 zoJ)L@WRT>RbTA2@ButVfwI-cR>PYHKI+HY(G?6r&q)9rLbUx{N(ygRBN%xZ;B|Ske zMXyC~L~lZGMVq57(IB)fdK=mc?T<#F526pDqtUVG1auNQ1&u-HqK~54Xbzf(=A-M; zjp!!y33Mx3iN1nH? z9c3kFC+8&RB~y|Mk}Hx=C*Mn1p0X~*F2ym$CB-!bmg14(ow7Z}Hzgn?FeNqxm6DK> zq=lR?Dd}2RI4dPPg_uH0sZLR(TuJ$mx;51=H6@jqdLmVwI-7bvbuslq>W$O~sgF{h zq&`c1k@_HdL}6ku7z`GZg~`U`V#pXO zrVvw!5n{S93XBrdj~T*@VAPmN%nW80a~^XK^APhG^Az(O^Ahtr<_+d8<~`;w%%>wi z9ocpy=19SjrXw>)o~Nx!vrF5R7Ls-_Eh6nuT724(w2ZW@v}~<|PHx)Kw9>Tlw8}JY zT2)$C+HjgWZ6a+t?OfV?+G5&;w7Y4~(w?XNmi962YdRo(N&3ok-E{qQ!*tVh$8_KH zfb_uhp!DE$?RsZ=WO{UZY&t5Pn4X(XPN$?7q|?$F>CE)f^z!tIbWVCl`fU2k48sg) z1~Q{CqakBB<66eOjE5OdGM;7pk@0uNmyB;%Aa)6M8P*890lNvi6>Ew$$8N{&#U8*O z#71C|*jOwIn}Ee(30NAIi7myJV~=5ZSU$D}+k#bKHP|`q0`>y-GWHkj4eYPjyVwWV z$Jl3?fXpSC%Q9DFuFBNQG|XI^xh``<=BCW8nI@UunaIq-OiAWs=CiEjSq@o2Sx2(6 zvvRWXvhuSSSru8FEN)g+7C);dOPbZ1buz0nt1GJ~Yd-5n)}5?-Sr4$qFEJGiH~7r0lrH@H7>@9|6UTks}$bG#Ma25*md z!h`W{cz3)f9*z&gN8+RKNPH|Fg-^tz@hNx=J{_Nl$KfmSo%luko9qqQ@a*X9f^1Rt zQ1)c@O!jQ{T=wPcU$gIKKgfQZ{Ve-M_RH+wvp;8lBj^y85mpjb6Kn}Ef)@c!*g*&& z>>}(T>?I%xse}wdCIL?%5%LHWLII(IARtHxQbH@?B%zzoOE^s!APf;E3Firmgo}jB zgnNX?glB}`2yX~~5ed zT1i?W<)jMIF%pj? zA<0P!QXff08YYd9)T9a01=4NOThe>dN784~w_KgvWw|SJb#o1J*XG*f+UGjvI_H9O z-E!S?J#xKreR6l?`sN9JE+#X{EHazSA@j(5axGa%?jiS)Pm}w}gXA;hG4cd?iabM}CC`!P$L-YN*E=Ul1#x+(kYn~JcU5Xq2yAIQc5Z1lu8Ph zQbp;a3{%vU3Cc9(9OXP^k#d1@m-2w}n(~42iSmUCpst{rY-R2VgqilQb` zlc}jxB9%;~Qj4fZsio9%Y6VqBRZ>T&YU(6)hB`~VNWDV6M!iYBO}$I~y}ZZO7n&;#O7o%Zr1{ejw1c!mG$bvKmPkvcVQ6Wz5?VD)L=)4RX)QDvt%ue} zQ_%)#XK16ev$V^!>$ID++q4I?N3^H3=d_R7rGl+=Q@RD+nr=&XpgYqcbSQlr-HRSV zKS&R!N7AF{G4#XqczPl|nVw2NLNB4W&^7eu#RkQ&;^^YM;@aZA;xol##S_I-#q-5i zi?0{oD!x;EulPan>*BY??~6Yce=h#Y*vhbDI5Qv&H--npo3Wj-lM%u=$UrgBj1@eq;R3 zc*A&mbj?xl(eR_Bqr#(OM;|bCm=??(%-zhr%>B#*%xGpJ6U|IvVwh>n3?`XLWfn1u znM`IW^8~Ypd73%E9Ab_!Cz#XBv&^f^>&(Z@7tEK;*UWd!znFhBzm({eY%XyraV>$B zc$9dTY%lRG2`C9H2`ULLNi0b&Ni8{2l3tQof-fPIhT9ocC z%_wD;wwKP7E|y*_y;Ay1>D|(2r7ucfmi}J)rt~cfz*@pu&RWINV;Qg z)^1i1E0`6+I?PIE;aJ(M99BMy$|_>fSzJ~FtB<8(4YJO###j@qX_kgH$68=rU|nXt zV7+3!X8pnXll6i1iS?QFwG2?Uq-=TF$}&(HqU=Z+yR571a@m{m4dt-%@bb9w`0}Lk zL4|^v&kR8MhW*=Z5WXG`4>{NCdJA+MT7qaQ> zqwF$v1^XDA$Ck4P*%Rz(wuU{&USR*izQO*LeV6@!{fPZX#fpm675WuxD%MqOtk_&( zTwzvWSpll>uGn6&v%;?;pdzqhPsQGf;EDqk2P?uW@D<#OzKSaqA1lo&11gg$iz~&I zvdWIiQ-W6iPQ zxO4nDyE#FeV9r5K1m_SZnuFowa4I;*I8~hEoH|ZDr;*ddIl*b;v~%Q~NzM#M!#T$} z&spSL;9TbX!nw}5#ktM7%lUH5_?Yjpq+_MW$`x>1xgA_Nx0|cvs>g z1@{&AHTN42$TQ$=;BDe<<(cw8JUgBv&xHr!dGUOBJ9&P*5MDU%5D&>i@e+B-yi^{8 z$K?rlBA%Gn%xmGD;`Q+Qcq-l?ZZEE~by;;q^|5MRwXj-R z-CBLJy0f~gx~ICgdaQb)db(OuJy$(n{k-}^_2=rZd?0^0e-&SkZ@@R`KA1FekGsFujbeA1$+@-%tY3k~h!Q3WlZ6;zx-e6S7ZQcJ!hB(Yuu6DbSSu6=>xGTNCgBNTtMH_-Lns%{ z3ZK^N)w|S()syQR>WAxZ)PE2GL`y`=MJq*XMO#EBB6E?I$VOx*au7L*yhJ{tog#nH zF41mLvM5`WDVxDPc$GJ7LAJ3qD9dy(Oc1b(MQo| z(YFSjhGh*a8+02C8rC+fZ*XjIX@E4iHNYA?8oV2P8g?}JHUu;THtcCgX<#;-Y&h5O zs&RdzYhz?%P9wjup;6K(ZER^g)u?J5Y#eSJZB#c-G%hw?Y`oHVt?_2#uZ^F?E5v$Y zL-AViM)79xRPvP-f@ zvQH8sIVg#cL`h;Khb0LTv?NthD4|Oj5~ie7QZA{Ka3xg|zNA(nkkm{1C08Y%nv9!# zn^K$DP3=vZrbkV$n%*@1+4R2YTl321)y?|NhRsIJ>zgf_t($F|9h#k+!Og+V$YxY? zLNmJgNOO8~W;3q2sF~i(X+GXu*IeJ+*xb@AYwl=1)!f}Y&^+8c+N^G#Z@$=krTJR( zug!OxA2dIb0;LAhpQKx)CQ@^$mDEWJmbyvZrJho6X@E3T8YzvE#z^C(iPB_gs+1&U zNb98a(ne{M^n|oc+Aft#yQRI-)6#R&dFi6`g7lK~s`R?_ru0|oUFm)4!xO+0<|hy* zQcti?^qjbS;%$pj3%JFz1>Ulw#kVD>C9EZ~C8{N+C9Vb4lF*Xbf^Q+V*3af)}+?t*3{OtR%~llE54P`N@^vyQd;X;RjoH#Kew5;1-7Bv zSZ!@>(`^fF7uqhjU2VJ5_O$Iq+pD%WZGX1Cmo1Skm#vcN$qZ#{WiB$f%va_w+a=p8 z3zi*_9h4==GGzo=jx0}BAS;p;%Z|#bWfGZE)-M~9jmXrpN!g5SR(4*tD7z?oB6}|T zP4-ImTJ~1|=_Sp8r?Wyg! zc0xO;o!m}sFKlPGbJ}_B{Px;*LHp_UiT1PYv+d{GFSK83zuJDS{Ym@t_7Cl!+rM@I zJC=8>?9lDd?=bGL@7U4d*MaER-Lbc0e@AFXc*mg*WJg>_c1KP}ZU?!8(oxt!?_hK= zJ6Ij%9TgoXJI;2z=+y6Y?TqTAcGhCB1bftF@y9&CBy69buE@oG07r(2vOW4)WCFyGJ8tt0v`nl_3 z*OjgtUAMaKblvNE-Swqgr+aDlif+AbgYLE6>$)ww!QF`N-Q9b;_jiYOhj$<9Ms~+_ z$9E@n=XF!M3%ZNC>D@=WOS{Xu+1;FOZg*98SNDAPo1S$&9zBP9ihG)R#(Hk`JnDJc z^P=Zv&-;`@&DW%OnB;rsIY7=6sX(!R1jc3)+m zu&<#{(kJa}?UVIQ^BZBpPJcN4>GYS=-&8AAYgOx2KdH8;OjKqn3zdt?RRvRdsJvA^ zDx@k^m7&U1;Z-D6o{FL>P*tdosYI$Kl~mQL>QKp5-71Ajt(sR|R{f&7q54(zQ1wLh zT=i1*yXvogVE@wo75%IFH}-GsH|@9Rx9PXS+~3)Ms=vFxcffW4Hn4rbcOYOOa3E+PY#?GFY9MCd@Id?k zb)a!TF>r3+mw~$j4+b6&JRA6J;N8Gq1D^)I3<3sq1~(5v2KNpg9!ww17{m@{4N?aS z1`7vigJpv?gDr!y!H&UGgFSK6GPKOnxS*Umcx$2&|%NvUBi2Z_YJoU zD~6TB{bvl$Y&~Oo#^Ma+4C@U04Cl<_GjGqlKl5=UY$SH%$Vl2q`UrM}G{PMbjEF|W zBTXZnBLgENBkGaK5zWZl$im3aBfpN^8M!y|aCH5s#i-RNXw+^rdNgG;eKd0vKS~NgEj0#5EM!QFQN0p`RIqyPorPP0AowW zmW>&WtsmPswt0**RzKD{c5MV7hnxZaL)6|vfD)n)7ow`9SQA^b=YPEV^eN}y3eM@~ueP8`T z{Yw2t{ipha`s29HxbC>Y_}cMxgGx{@YX4cJYoY_3Hb;f=MHsd#QXa+eGHxoaTG?OxOWF})KYbJXpXQq6n za)vuoHN&5&oe|D7%t&UWGc7Z1Gd(kBXD-gXnE85k!&!&3JI_X)#h)!bD>&P6_QlyZ zXaCYH(X7#I));HdG?p5W#zo_*foVK6-WnfGqz134(2Q!PG;^AH%~j2>n!B0@n#Y=_ zns=Jdns2i@v&&}nW({YJW;e|KH0wK?HCsR1Fxx)cGut<-njM@~&rZ(H%+Aiv&0d_n zIo~(0nxCBidH&M;)%ol5x90E6-=F_|{*U=T=ReGUn*TiibpfydTG+dgvmjoOE%Yo5 zEQ~KqEzB=mUbw#S+rqnrzZO0%d|q6&sJCdaxOQ><;>Ja@MaxC&MVm$NB4p8R5xy9@ o7`7O(cxVy17`GU|n6#L(cw{l-hc}gu&X3f{???LY@5Rjj2kIKpivR!s diff --git a/jaem/week7/CatStaGram/CatStaGram/Base.lproj/Main.storyboard b/jaem/week7/CatStaGram/CatStaGram/Base.lproj/Main.storyboard index 4078f1d..400ca42 100644 --- a/jaem/week7/CatStaGram/CatStaGram/Base.lproj/Main.storyboard +++ b/jaem/week7/CatStaGram/CatStaGram/Base.lproj/Main.storyboard @@ -1,5 +1,5 @@ - + diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/LoginViewController.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/LoginViewController.swift index 41e5119..666a22f 100644 --- a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/LoginViewController.swift +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/LoginViewController.swift @@ -48,13 +48,12 @@ class LoginViewController: UIViewController { guard let userInfo = self.userInfo else { return } - if userInfo.email == self.email && userInfo.password == self.password{ + if userInfo.email == self.email + && userInfo.password == self.password{ + let vc = storyboard?.instantiateViewController(withIdentifier: "TabBarVC") as! UITabBarController - vc.modalPresentationStyle = .fullScreen - self.present(vc, animated: true, completion: nil) - } - else{ + self.view.window?.windowScene?.keyWindow?.rootViewController = vc } } diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/RegisterViewController.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/RegisterViewController.swift index 43ccca8..f49c2e6 100644 --- a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/RegisterViewController.swift +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/RegisterViewController.swift @@ -66,7 +66,7 @@ class RegisterViewController: UIViewController { switch sender{ case emailTextField: - self.isValidEmail = text.isValidEmail() + self.isValidEmail = text.count>5 self.email = text case nameTextField: self.isValidName = text.count > 2 @@ -75,7 +75,7 @@ class RegisterViewController: UIViewController { self.isValidNickname = text.count > 2 self.nickname = text case pwTextField: - self.isValidPassword = text.isValidPassword() + self.isValidPassword = text.count>5 self.password = text default: fatalError("Missing TF") diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate index d85218b9600787cc5a6807810a8d03a67e380f2a..7c1dda6d0479950491fccb2618a07f500e237d7d 100644 GIT binary patch delta 18338 zcmbVT2UwHW*U!E0j)UMv_Li_gK=u|8*YzT5GMN z)!J#%+NyP1XKih*w$`fOeG|~O|Ng({dp>y}dCnd0J?H+;Ilpu7>x{i{?pC-!1w0m( ztTBlG5dAKCA$kc!fCK};Krjdl219@q7z(U`D{uquU>NWNF<>-E1G%6G6oav#45&dR zr~=hM3v@san!rRb2_P^X%m8nId0;VE3YLNOU<23)Hi6Ax8|Vak!8>3d_!yi5pMX!n zXW(=21^5yez6F=T6>uGV4{m|G;2wAYegr>*U%+GV1pEe`fj_}>2q6JUNI@EkVShLj zIzm_I2Bpvw`ocgM0!P3I7zy)X0hGf+SOkmV7^r|sI2Nj42`q=TPy_X_5l(`Wp#ky` z!8!0vI2X=?^9|4lSHe|rHCzL?!fo(vxE=0*JK-+48}5Of@I814z7G$>58%h}OLz`m zfEVEvcoklQ-^07`9{dsh1Rud);bZuMpordtgs>zA5`&1rgf(GLI1ny`E8#)J5^+R4 zkw7F8BZ(v;nMff<5vfE5kw+Nvi2_1FD2WPU98pQs5%okXF@=~-%q8X#i$uQ>Mq(+k zl2}WuBi0j#i4TY)#8KiHah&*&_=q?`oFqObP7$9IXNj+gbHsPVCE|PH7IB-nL;ONK zB7P+v6HiG%Lb5MuK}twVvLD%>96;KTwxk{DM7oi_1~Qn8BxA`$GKEYd)5&bIimWDU z$XZfEYDpbgN7j=Kq@Em48c3c*

    riIh$NaE+UQOV)89=HMxddOLma!$!+9b@&NfR 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 A0RR91 delta 19376 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 diff --git a/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.pbxproj b/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.pbxproj index be064ce..510180d 100644 --- a/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.pbxproj +++ b/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + BC39E0DC284BC8C300072630 /* GameVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC39E0DB284BC8C300072630 /* GameVC.swift */; }; + BC39E0DF284BD46100072630 /* GameCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC39E0DD284BD46100072630 /* GameCollectionViewCell.swift */; }; + BC903E98283F5DEF0089E4C7 /* RankModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC903E97283F5DEF0089E4C7 /* RankModel.swift */; }; + BC903E9A283F65270089E4C7 /* RankVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC903E99283F65270089E4C7 /* RankVM.swift */; }; + BC903E9C283F65BF0089E4C7 /* RankVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC903E9B283F65BF0089E4C7 /* RankVC.swift */; }; + BC903E9E283F683E0089E4C7 /* RankTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC903E9D283F683E0089E4C7 /* RankTableViewCell.swift */; }; BCC0A7FC283E729100F9373D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCC0A7FB283E729100F9373D /* AppDelegate.swift */; }; BCC0A7FE283E729100F9373D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCC0A7FD283E729100F9373D /* SceneDelegate.swift */; }; BCC0A800283E729100F9373D /* HomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCC0A7FF283E729100F9373D /* HomeVC.swift */; }; @@ -19,6 +25,8 @@ BCC0A830283E7DE800F9373D /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCC0A82F283E7DE800F9373D /* MenuModel.swift */; }; BCC0A833283E811400F9373D /* MenuTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCC0A832283E811400F9373D /* MenuTableViewCell.swift */; }; BCC0A836283E82C800F9373D /* MenuVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCC0A835283E82C800F9373D /* MenuVM.swift */; }; + BCCBFE0F284CD7E600BD452A /* GameModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCCBFE0E284CD7E600BD452A /* GameModel.swift */; }; + BCCBFE11284CD8D600BD452A /* GameVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCCBFE10284CD8D600BD452A /* GameVM.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -39,6 +47,12 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + BC39E0DB284BC8C300072630 /* GameVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameVC.swift; sourceTree = ""; }; + BC39E0DD284BD46100072630 /* GameCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameCollectionViewCell.swift; sourceTree = ""; }; + BC903E97283F5DEF0089E4C7 /* RankModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankModel.swift; sourceTree = ""; }; + BC903E99283F65270089E4C7 /* RankVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankVM.swift; sourceTree = ""; }; + BC903E9B283F65BF0089E4C7 /* RankVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankVC.swift; sourceTree = ""; }; + BC903E9D283F683E0089E4C7 /* RankTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankTableViewCell.swift; sourceTree = ""; }; BCC0A7F8283E729100F9373D /* MemorizeGame.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MemorizeGame.app; sourceTree = BUILT_PRODUCTS_DIR; }; BCC0A7FB283E729100F9373D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; BCC0A7FD283E729100F9373D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -55,6 +69,8 @@ BCC0A82F283E7DE800F9373D /* MenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuModel.swift; sourceTree = ""; }; BCC0A832283E811400F9373D /* MenuTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuTableViewCell.swift; sourceTree = ""; }; BCC0A835283E82C800F9373D /* MenuVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuVM.swift; sourceTree = ""; }; + BCCBFE0E284CD7E600BD452A /* GameModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameModel.swift; sourceTree = ""; }; + BCCBFE10284CD8D600BD452A /* GameVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameVM.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -82,6 +98,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + BC903E9F283F6D560089E4C7 /* VC */ = { + isa = PBXGroup; + children = ( + BCC0A7FF283E729100F9373D /* HomeVC.swift */, + BC903E9B283F65BF0089E4C7 /* RankVC.swift */, + BC39E0DB284BC8C300072630 /* GameVC.swift */, + ); + path = VC; + sourceTree = ""; + }; BCC0A7EF283E729100F9373D = { isa = PBXGroup; children = ( @@ -109,7 +135,7 @@ BCC0A831283E7EF400F9373D /* Cell */, BCC0A82E283E736300F9373D /* Model */, BCC0A82C283E72B800F9373D /* Apps */, - BCC0A7FF283E729100F9373D /* HomeVC.swift */, + BC903E9F283F6D560089E4C7 /* VC */, BCC0A82D283E72C100F9373D /* View */, BCC0A804283E729100F9373D /* Assets.xcassets */, BCC0A806283E729100F9373D /* LaunchScreen.storyboard */, @@ -156,6 +182,8 @@ isa = PBXGroup; children = ( BCC0A82F283E7DE800F9373D /* MenuModel.swift */, + BC903E97283F5DEF0089E4C7 /* RankModel.swift */, + BCCBFE0E284CD7E600BD452A /* GameModel.swift */, ); path = Model; sourceTree = ""; @@ -164,6 +192,8 @@ isa = PBXGroup; children = ( BCC0A832283E811400F9373D /* MenuTableViewCell.swift */, + BC903E9D283F683E0089E4C7 /* RankTableViewCell.swift */, + BC39E0DD284BD46100072630 /* GameCollectionViewCell.swift */, ); path = Cell; sourceTree = ""; @@ -172,6 +202,8 @@ isa = PBXGroup; children = ( BCC0A835283E82C800F9373D /* MenuVM.swift */, + BC903E99283F65270089E4C7 /* RankVM.swift */, + BCCBFE10284CD8D600BD452A /* GameVM.swift */, ); path = ViewModel; sourceTree = ""; @@ -307,12 +339,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BC39E0DC284BC8C300072630 /* GameVC.swift in Sources */, BCC0A830283E7DE800F9373D /* MenuModel.swift in Sources */, BCC0A800283E729100F9373D /* HomeVC.swift in Sources */, BCC0A7FC283E729100F9373D /* AppDelegate.swift in Sources */, BCC0A7FE283E729100F9373D /* SceneDelegate.swift in Sources */, BCC0A833283E811400F9373D /* MenuTableViewCell.swift in Sources */, BCC0A836283E82C800F9373D /* MenuVM.swift in Sources */, + BC903E9A283F65270089E4C7 /* RankVM.swift in Sources */, + BC39E0DF284BD46100072630 /* GameCollectionViewCell.swift in Sources */, + BC903E98283F5DEF0089E4C7 /* RankModel.swift in Sources */, + BC903E9C283F65BF0089E4C7 /* RankVC.swift in Sources */, + BCCBFE0F284CD7E600BD452A /* GameModel.swift in Sources */, + BCCBFE11284CD8D600BD452A /* GameVM.swift in Sources */, + BC903E9E283F683E0089E4C7 /* RankTableViewCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate index 4980697c82700baf962a023f83522800395b7e10..df61992f16ee5c6a54b3864c4688cccd572e6178 100644 GIT binary patch literal 89741 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 JJO8zY-hZq@1sDJT literal 41849 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 diff --git a/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..cd37eb6 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Cell/GameCollectionViewCell.swift b/jaem/week9/MemorizeGame/MemorizeGame/Cell/GameCollectionViewCell.swift new file mode 100644 index 0000000..1043ada --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/Cell/GameCollectionViewCell.swift @@ -0,0 +1,41 @@ +// +// GameCollectionViewCell.swift +// MemorizeGame +// +// Created by 송재민 on 2022/06/05. +// + +import UIKit + +class GameCollectionViewCell: UICollectionViewCell { + + @IBOutlet weak var picLabel: UILabel! + @IBOutlet weak var frontImageView: UIImageView! + @IBOutlet weak var backImageView: UIImageView! + + func flipUp(speed: TimeInterval = 0.3){ + UIView.transition(from: backImageView, to: frontImageView, duration: speed, options:[.showHideTransitionViews, .transitionFlipFromLeft], completion: nil) + + picLabel.alpha = 1 + } + + func flipDown(speed: TimeInterval = 0.3, delay: TimeInterval = 0.5){ + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delay) { + // flip down animation + UIView.transition(from: self.frontImageView, to: self.backImageView, duration: speed, options: [.showHideTransitionViews, .transitionFlipFromLeft], completion: nil) + + self.picLabel.alpha = 0 + } + } + + func showFront(){ + picLabel.alpha = 1 + } + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + +} diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Cell/RankTableViewCell.swift b/jaem/week9/MemorizeGame/MemorizeGame/Cell/RankTableViewCell.swift new file mode 100644 index 0000000..39d9862 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/Cell/RankTableViewCell.swift @@ -0,0 +1,23 @@ +// +// RankTableViewCell.swift +// MemorizeGame +// +// Created by 송재민 on 2022/05/26. +// + +import UIKit + +class RankTableViewCell: UITableViewCell{ + + @IBOutlet weak var rankNumLabel: UILabel! + @IBOutlet weak var rankNameLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + } +} diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Model/GameModel.swift b/jaem/week9/MemorizeGame/MemorizeGame/Model/GameModel.swift new file mode 100644 index 0000000..add2050 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/Model/GameModel.swift @@ -0,0 +1,41 @@ +// +// GameModel.swift +// MemorizeGame +// +// Created by 송재민 on 2022/06/05. +// + +import Foundation + +struct GameModel{ + + var cardList: [Card] = [] + + mutating func setIsMatched(index: Int){ + cardList[index].isMatched = true + } + + mutating func setIsFlipped(index: Int){ + cardList[index].isFlipped = !cardList[index].isFlipped + } + + func getCardPicture(index: Int) -> String{ + return cardList[index].cardPicture + } + + mutating func setCardList(cards: [String]){ + for card in cards{ + cardList.append(Card(cardPicture: card, isMatched: false, isFlipped: false)) + } + } + + func getCardList() -> [Card]{ + return cardList + } + + struct Card{ + var cardPicture: String = "" + var isMatched: Bool = false + var isFlipped: Bool = false + } +} diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Model/MenuModel.swift b/jaem/week9/MemorizeGame/MemorizeGame/Model/MenuModel.swift index 501b47f..6a0aac2 100644 --- a/jaem/week9/MemorizeGame/MemorizeGame/Model/MenuModel.swift +++ b/jaem/week9/MemorizeGame/MemorizeGame/Model/MenuModel.swift @@ -16,6 +16,13 @@ struct MenuModel{ Menu(title: "Fruit 🍇 🍈 🍉 🍌 🥝 🍒", isSelected: false) ] + var cardList = [ + ["🏎","🚌","🚐","🛴","🛵","🚓","🏎","🚌","🚐","🛴","🛵","🚓"], + ["😃","😇","🤗","🤑","😋","😙","😃","😇","🤗","🤑","😋","😙"], + ["🐷","🐰","🐨","🦁","🙊","🐻","🐷","🐰","🐨","🦁","🙊","🐻"], + ["🍇","🍇","🥝","🥝","🍈","🍈","🍉","🍉","🍌","🍌","🍒","🍒"] + ] + mutating func selectMenu(index: Int){ menuList[index].isSelected = !menuList[index].isSelected diff --git a/jaem/week9/MemorizeGame/MemorizeGame/Model/RankModel.swift b/jaem/week9/MemorizeGame/MemorizeGame/Model/RankModel.swift new file mode 100644 index 0000000..11f2a70 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/Model/RankModel.swift @@ -0,0 +1,39 @@ +// +// RankModel.swift +// MemorizeGame +// +// Created by 송재민 on 2022/05/26. +// + +import Foundation + +struct RankModel{ + + var rankList: [Rank] = [ + /* + Rank(name: "Ximya", date: "2022.02.22", clearTime: "00:59"), + Rank(name: "Jaem", date: "2022.02.10", clearTime: "01:32")*/ + ] + + mutating func deleteAll(){ + self.rankList = [] + } + + mutating func addRank(newRank: Rank){ + + rankList.append(newRank) + + rankList = rankList.sorted(by: {Int($0.clearTime.replacingOccurrences(of: ":", with: ""))! < Int($1.clearTime.replacingOccurrences(of: ":", with: ""))!}) + + } + + mutating func setRankList(newRankList: [Rank]){ + rankList = newRankList + } + + struct Rank{ + var name: String + var date: String + var clearTime: String + } +} diff --git a/jaem/week9/MemorizeGame/MemorizeGame/VC/GameVC.swift b/jaem/week9/MemorizeGame/MemorizeGame/VC/GameVC.swift new file mode 100644 index 0000000..c35e7ab --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/VC/GameVC.swift @@ -0,0 +1,247 @@ +// +// GameVC.swift +// MemorizeGame +// +// Created by 송재민 on 2022/06/05. +// + +import UIKit + +class GameVC: UIViewController{ + + @IBOutlet weak var timeLabel: UILabel! + @IBOutlet weak var countDownLabel: UILabel! + @IBOutlet weak var quitBtn: UIButton! + @IBOutlet weak var collectionView: UICollectionView! + + /*게임 시작 카운트다운 카운터*/ + var countTimer : Timer? + var countTimerNum : Int = 0 + var gameStart : Bool = false + + /*게임 완료 시간 카운터*/ + var timer : Timer? + var timerNum : Int = 0 + + var gameVM = GameVM() + var rankVM = RankVM() + typealias Rank = RankVM.Rank + + var firstFlippedCardIndex: IndexPath? + var matchedCard = 0 + + var cardDataSource: [String] = [] + + //var ranks: [Rank] = UserDefaults.standard.value(forKey: "ranks") as! [Rank] + + /*quit 버튼 눌렀을 때*/ + @IBAction func quitBtnTapped(_ sender: UIButton) { + self.dismiss(animated: true, completion: nil) + } + + /*타이머 함수*/ + func startTimer(){ + if timer != nil && timer!.isValid{ + timer!.invalidate() + } + + timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerCallback), userInfo: nil, repeats: true) + } + + @objc func timerCallback(){ + timerNum += 1 + + let minute = timerNum % 3600 / 60 + let second = timerNum % 3600 % 60 + + var minuteText = "" + var secondText = "" + + if(minute < 10){ + minuteText = "0" + String(minute) + }else{ + minuteText = String(minute) + } + + if(second < 10){ + secondText = "0" + String(second) + }else{ + secondText = String(second) + } + + timeLabel.text = minuteText + ":" + secondText + } + + func startCountDown(){ + if countTimer != nil && countTimer!.isValid{ + countTimer!.invalidate() + } + + countTimerNum = 3 + + countTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(showCountDown), userInfo: nil, repeats: true) + } + + + override func viewDidLoad() { + super.viewDidLoad() + + gameVM.setCardList(cards: cardDataSource) + + startCountDown() + + collectionView.delegate = self + collectionView.dataSource = self + + /*ui 구성*/ + timeLabel.layer.masksToBounds = true + timeLabel.layer.cornerRadius = 5 + timeLabel.layer.isHidden = true + + quitBtn.layer.masksToBounds = true + quitBtn.layer.cornerRadius = 10 + } + + +} + + +extension GameVC: UICollectionViewDelegate{ + + @objc func showCountDown(){ + + countDownLabel.text = String(countTimerNum) + + if(countTimerNum == 0){ + countTimer?.invalidate() + countTimer = nil + + gameStart = true + collectionView.reloadData() + + countDownLabel.layer.isHidden = true + + startTimer() + timeLabel.layer.isHidden = false + + } + + countTimerNum -= 1 + + } + + // MARK: 게임 로직 + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + + guard let cell = collectionView.cellForItem(at: indexPath) as? GameCollectionViewCell else {fatalError()} + + + if((gameVM.getCardList()[indexPath.row].isFlipped == false) && + (gameVM.getCardList()[indexPath.row].isMatched == false)) { + + cell.flipUp() + + if firstFlippedCardIndex == nil{ + firstFlippedCardIndex = indexPath + } else{ + checkForMatch(indexPath) + } + } + + } + + + func checkForMatch(_ secondFlippedCardIndex: IndexPath){ + + var cardOne = gameVM.getCardList()[firstFlippedCardIndex!.row] + var cardTwo = gameVM.getCardList()[secondFlippedCardIndex.row] + + let cardOneCell = collectionView.cellForItem(at: firstFlippedCardIndex!) as? GameCollectionViewCell + let cardTwoCell = collectionView.cellForItem(at: secondFlippedCardIndex) as? GameCollectionViewCell + + if cardOne.cardPicture == cardTwo.cardPicture{ + cardOne.isMatched = true + cardTwo.isMatched = true + + cardOneCell?.showFront() + cardTwoCell?.showFront() + + self.matchedCard += 1 + + if(self.matchedCard == 6){ + winGame() + } + } else{ + cardOne.isFlipped = false + cardTwo.isFlipped = false + + cardOneCell?.flipDown() + cardTwoCell?.flipDown() + } + + self.firstFlippedCardIndex = nil + } + + func winGame(){ + + let formatter = DateFormatter() + formatter.dateFormat = "yyyy.MM.dd" + let current_date_string = formatter.string(from: Date()) + + let alert = UIAlertController(title: "Succeed", message: timeLabel.text, preferredStyle: .alert) + alert.addTextField{ (textField) in + textField.placeholder = "Leave name" + } + let okAction = UIAlertAction(title: "Confirm", style: .default, handler: { okAction in + let myName = alert.textFields?[0].text + self.rankVM.addRank(newRank: Rank(name: myName!, date: current_date_string, clearTime: self.timeLabel.text!)) + + //self.ranks.append(Rank(name: myName!, date: current_date_string, clearTime: self.timeLabel.text!)) + //UserDefaults.standard.setValue(Rank(name: myName!, date: current_date_string, clearTime: self.timeLabel.text!), forKey: "ranks") + + self.dismiss(animated: true, completion: nil) + + }) + + alert.addAction(okAction) + + present(alert, animated: true, completion: nil) + } +} + +extension GameVC: UICollectionViewDataSource{ + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return cardDataSource.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GameCollectionViewCell", for: indexPath) as? GameCollectionViewCell else{fatalError()} + cell.picLabel.text = cardDataSource[indexPath.row] + //cell.backgroundColor = .blue + cell.layer.cornerRadius = 10 + + if(!gameStart){ + cell.flipUp() + }else{ + cell.flipDown() + } + + return cell + } +} + +extension GameVC: UICollectionViewDelegateFlowLayout{ + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: collectionView.frame.width/3 - 10, height: 130) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return CGFloat(13) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return CGFloat(12) + } +} diff --git a/jaem/week9/MemorizeGame/MemorizeGame/HomeVC.swift b/jaem/week9/MemorizeGame/MemorizeGame/VC/HomeVC.swift similarity index 70% rename from jaem/week9/MemorizeGame/MemorizeGame/HomeVC.swift rename to jaem/week9/MemorizeGame/MemorizeGame/VC/HomeVC.swift index 52bc38c..299bbf4 100644 --- a/jaem/week9/MemorizeGame/MemorizeGame/HomeVC.swift +++ b/jaem/week9/MemorizeGame/MemorizeGame/VC/HomeVC.swift @@ -10,8 +10,10 @@ import UIKit class HomeVC: UIViewController { var menuVM = MenuVM() + var gameVM = GameVM() @IBOutlet weak var menuTableView: UITableView! @IBOutlet weak var startBtn: UIBarButtonItem! + var selectedMenu = -1 override func viewDidLoad() { super.viewDidLoad() @@ -22,8 +24,19 @@ class HomeVC: UIViewController { self.menuTableView.tableHeaderView = UIView() self.menuTableView.dataSource = self self.menuTableView.delegate = self + } - + + + @IBAction func startBtnTapped(_ sender: UIBarButtonItem) { + + guard let gameVC = self.storyboard?.instantiateViewController(withIdentifier: "GameVC") as? GameVC else {return} + gameVC.modalPresentationStyle = UIModalPresentationStyle.fullScreen + gameVC.cardDataSource = menuVM.getCardList(index: selectedMenu).shuffled() + + self.present(gameVC, animated: true) + } + } @@ -39,9 +52,13 @@ extension HomeVC: UITableViewDataSource, UITableViewDelegate{ cell.titleLabel.text = menuVM.getMenuList()[indexPath.row].title cell.accessoryType = .none - menuVM.getMenuList()[indexPath.row].isSelected - ? (cell.accessoryType = .checkmark) - : (cell.accessoryType = .none) + if(menuVM.getMenuList()[indexPath.row].isSelected){ + cell.accessoryType = .checkmark + selectedMenu = indexPath.row + } + else{ + cell.accessoryType = .none + } return cell } diff --git a/jaem/week9/MemorizeGame/MemorizeGame/VC/RankVC.swift b/jaem/week9/MemorizeGame/MemorizeGame/VC/RankVC.swift new file mode 100644 index 0000000..0d974d7 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/VC/RankVC.swift @@ -0,0 +1,62 @@ +// +// RankVC.swift +// MemorizeGame +// +// Created by 송재민 on 2022/05/26. +// + +import UIKit + +class RankVC: UIViewController{ + var rankVM = RankVM() + typealias Rank = RankVM.Rank + + var rankList = [Rank].self + + @IBOutlet weak var deleteBtn: UIBarButtonItem! + @IBOutlet weak public var rankTableView: UITableView! + + @IBAction func deleteBtnTapped(_ sender: Any) { + rankVM.deleteRank() + self.rankTableView.reloadData() + } + + override func viewDidLoad() { + super.viewDidLoad() + + /*테이블 ui 구성*/ + self.rankTableView.separatorInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) + self.rankTableView.frame.inset(by: UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)) + self.rankTableView.tableHeaderView = UIView() + self.rankTableView.dataSource = self + self.rankTableView.delegate = self + + } + + override func viewWillAppear(_ animated: Bool) { + //rankList = UserDefaults.standard.value(forKey: "ranks") as! [Rank] + + //rankVM.setRankList(newRankList: rankList) + + self.rankTableView.reloadData() + //print(UserDefaults.standard.value(forKey: "ranks")) + } +} + +extension RankVC: UITableViewDataSource, UITableViewDelegate{ + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return rankVM.getRankList().count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "RankTableViewCell", for: indexPath) as! RankTableViewCell + + cell.selectionStyle = .none + + cell.rankNumLabel.text = String(indexPath.row + 1) + cell.rankNameLabel.text = rankVM.getRankList()[indexPath.row].name + " / " + rankVM.getRankList()[indexPath.row].date + " / " + rankVM.getRankList()[indexPath.row].clearTime + + return cell + } +} diff --git a/jaem/week9/MemorizeGame/MemorizeGame/View/Base.lproj/Main.storyboard b/jaem/week9/MemorizeGame/MemorizeGame/View/Base.lproj/Main.storyboard index 4b4944f..0b57662 100644 --- a/jaem/week9/MemorizeGame/MemorizeGame/View/Base.lproj/Main.storyboard +++ b/jaem/week9/MemorizeGame/MemorizeGame/View/Base.lproj/Main.storyboard @@ -1,17 +1,17 @@ - + - + + - @@ -57,16 +57,144 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -87,7 +215,6 @@ - @@ -101,6 +228,7 @@ + @@ -124,18 +252,72 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + @@ -146,5 +328,8 @@ + + + diff --git a/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/GameVM.swift b/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/GameVM.swift new file mode 100644 index 0000000..4f84451 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/GameVM.swift @@ -0,0 +1,34 @@ +// +// GameVM.swift +// MemorizeGame +// +// Created by 송재민 on 2022/06/05. +// + +import Foundation + +class GameVM: ObservableObject{ + @Published public var model = GameModel() + typealias Card = GameModel.Card + + func setIsMatched(index: Int){ + model.setIsMatched(index: index) + } + + func setIsFlipped(index: Int){ + model.setIsFlipped(index: index) + } + + func getCardPicture(index: Int) -> String{ + return model.getCardPicture(index: index) + } + + func setCardList(cards: [String]){ + model.setCardList(cards: cards) + } + + func getCardList() -> [Card]{ + return model.getCardList() + } + +} diff --git a/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/MenuVM.swift b/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/MenuVM.swift index 95e473a..5facb16 100644 --- a/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/MenuVM.swift +++ b/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/MenuVM.swift @@ -18,4 +18,8 @@ class MenuVM: ObservableObject{ func selectMenu(index: Int){ model.selectMenu(index: index) } + + func getCardList(index: Int) -> [String]{ + return model.cardList[index] + } } diff --git a/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/RankVM.swift b/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/RankVM.swift new file mode 100644 index 0000000..9367226 --- /dev/null +++ b/jaem/week9/MemorizeGame/MemorizeGame/ViewModel/RankVM.swift @@ -0,0 +1,29 @@ +// +// RankVM.swift +// MemorizeGame +// +// Created by 송재민 on 2022/05/26. +// + +import UIKit + +class RankVM: ObservableObject{ + @Published public var model = RankModel() + typealias Rank = RankModel.Rank + + func getRankList() -> [Rank]{ + return model.rankList + } + + func deleteRank(){ + model.deleteAll() + } + + func addRank(newRank: Rank){ + model.addRank(newRank: newRank) + } + + func setRankList(newRankList: [Rank]){ + model.setRankList(newRankList: newRankList) + } +}

    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 literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_white.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_white.imageset/Contents.json" new file mode 100644 index 0000000..8e4ea3c --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_white.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_home_home_white.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_white.imageset/ic_home_home_white.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_white.imageset/ic_home_home_white.png" new file mode 100644 index 0000000000000000000000000000000000000000..1072f82754d59e90091ebafa15762c84717b19be GIT binary patch literal 9869 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 RqH~&O@PP0B%Dv&|{s)n$Y-<1j literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_reels.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_reels.imageset/Contents.json" new file mode 100644 index 0000000..74b405e --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_reels.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_home_reels.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_reels.imageset/ic_home_reels.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_reels.imageset/ic_home_reels.png" new file mode 100644 index 0000000000000000000000000000000000000000..3bfdfe623e2026560fa071c7c992f0c46ff201ad GIT binary patch literal 12969 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 B!zBO! literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_send.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_send.imageset/Contents.json" new file mode 100644 index 0000000..41e2aaa --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_send.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_home_send.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_send.imageset/ic_home_send.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_send.imageset/ic_home_send.png" new file mode 100644 index 0000000000000000000000000000000000000000..76aae87abd384bff49f5f4be35bf6472e88810c4 GIT binary patch literal 12495 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 literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_black.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_black.imageset/Contents.json" new file mode 100644 index 0000000..07ab32c --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_black.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_home_shop_black.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_black.imageset/ic_home_shop_black.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_black.imageset/ic_home_shop_black.png" new file mode 100644 index 0000000000000000000000000000000000000000..c89519d97051f46be961c78a3dc39fccc6f481a0 GIT binary patch literal 7889 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 literal 0 HcmV?d00001 diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_white.imageset/Contents.json" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_white.imageset/Contents.json" new file mode 100644 index 0000000..7f0cf60 --- /dev/null +++ "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_white.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_home_shop_white.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_white.imageset/ic_home_shop_white.png" "b/jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_white.imageset/ic_home_shop_white.png" new file mode 100644 index 0000000000000000000000000000000000000000..34490e5addc50d4bb3cbd5fee291f17a3ef239e5 GIT binary patch literal 7004 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 literal 0 HcmV?d00001 diff --git a/jaem/week7/CatStaGram/CatStaGram/Base.lproj/LaunchScreen.storyboard b/jaem/week7/CatStaGram/CatStaGram/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week7/CatStaGram/CatStaGram/Base.lproj/Main.storyboard b/jaem/week7/CatStaGram/CatStaGram/Base.lproj/Main.storyboard new file mode 100644 index 0000000..4a6e04b --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/Base.lproj/Main.storyboard @@ -0,0 +1,541 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week7/CatStaGram/CatStaGram/Info.plist b/jaem/week7/CatStaGram/CatStaGram/Info.plist new file mode 100644 index 0000000..dd3c9af --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/jaem/week7/CatStaGram/CatStaGram/UIViewController+Extension.swift b/jaem/week7/CatStaGram/CatStaGram/UIViewController+Extension.swift new file mode 100644 index 0000000..8ee341a --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UIViewController+Extension.swift @@ -0,0 +1,21 @@ +// +// UIViewController+Extension.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/02. +// + +import UIKit + +extension UIViewController{ + /* + func generateButtonAttribute(_ button: UIButton, texts: String..., fonts: UIFont..., colors: UIColor...) -> NSMutableAttributedString{ + guard let wholeText = button.titleLabel?.text else{ + fatalError("버튼에 텍스트가 없음.") + } + + let customFonts: [UIFont] = fonts + + } + */ +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UIViewExtension.swift b/jaem/week7/CatStaGram/CatStaGram/UIViewExtension.swift new file mode 100644 index 0000000..0b6df86 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UIViewExtension.swift @@ -0,0 +1,20 @@ +// +// UIViewExtension.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/02. +// + +import UIKit + +extension UIView{ + @IBInspectable var cornerRadius: CGFloat { + get{ + return layer.cornerRadius + } + set{ + layer.cornerRadius = newValue + layer.masksToBounds = newValue > 0 + } + } +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInfo.swift b/jaem/week7/CatStaGram/CatStaGram/UserInfo.swift new file mode 100644 index 0000000..a619d25 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInfo.swift @@ -0,0 +1,15 @@ +// +// UserInfo.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/02. +// + +import Foundation + +struct UserInfo{ + let email: String + let name: String + let nickname: String + let password: String +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/PostCell/PostCollectionViewCell.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/PostCell/PostCollectionViewCell.swift index 3c2dd00..47340b7 100644 --- a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/PostCell/PostCollectionViewCell.swift +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/PostCell/PostCollectionViewCell.swift @@ -6,6 +6,7 @@ // import UIKit +import Kingfisher class PostCollectionViewCell: UICollectionViewCell { static let identifier = "PostCollectionViewCell" @@ -16,7 +17,13 @@ class PostCollectionViewCell: UICollectionViewCell { // Initialization code } - public func setupData(){ + public func setupData(_ imageURLStr: String?){ //이미지 뷰의 이미지를 업로드한다. + + guard let imageURLStr = imageURLStr else { return } + + if let url = URL(string: imageURLStr){ + postImageView.kf.setImage(with: url, placeholder: UIImage(systemName: "photo")) + } } } diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/ProfileViewController.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/ProfileViewController.swift index c14c127..fd097ec 100644 --- a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/ProfileViewController.swift +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/ProfileViewController.swift @@ -7,18 +7,45 @@ import UIKit -class ProfileViewController: UIViewController { +class ProfileViewController: UIViewController, UIGestureRecognizerDelegate { //MARK: - Properties @IBOutlet weak var profileCollectionView: UICollectionView! + var userPosts: [GetUserPosts]?{ + didSet{self.profileCollectionView.reloadData()} + } + + var deletedIndex: Int? + //MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() setupCollectionView() - // Do any additional setup after loading the view. + + setupData() } //MARK: - Actions + @objc + func didLongPressCell(gestureRecognizer: UILongPressGestureRecognizer){ + if gestureRecognizer.state != .began{return} + + let position = gestureRecognizer.location(in: profileCollectionView) + + if let indexPath = profileCollectionView?.indexPathForItem(at: position){ + print("Debug : ", indexPath.item) + + guard let userPosts = self.userPosts else {return} + let cellData = userPosts[indexPath.item] + self.deletedIndex = indexPath.item + if let postIdx = cellData.postIdx{ + UserFeedDataManager().deleteUserFeed(self, postIdx) + } + + //삭제 api 호출 + UserFeedDataManager().deleteUserFeed(self, 17) + } + } //MARK: - Helpers private func setupCollectionView(){ @@ -28,6 +55,17 @@ class ProfileViewController: UIViewController { profileCollectionView.register(UINib(nibName: "ProfileCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: ProfileCollectionViewCell.identifier) profileCollectionView.register(UINib(nibName: "PostCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: PostCollectionViewCell.identifier) + + let gesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPressCell(gestureRecognizer:))) + gesture.minimumPressDuration = 0.66 + + gesture.delegate = self + gesture.delaysTouchesBegan = true + profileCollectionView.addGestureRecognizer(gesture) + } + + private func setupData() { + UserFeedDataManager().getUserFeed(self) } } @@ -43,7 +81,7 @@ extension ProfileViewController: UICollectionViewDelegate, UICollectionViewDataS case 0: return 1 default: - return 24 + return userPosts?.count ?? 0 } } @@ -60,6 +98,14 @@ extension ProfileViewController: UICollectionViewDelegate, UICollectionViewDataS guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PostCollectionViewCell.identifier, for: indexPath) as? PostCollectionViewCell else{ fatalError("셀 타입 캐스팅 실패") } + + let itemIndex = indexPath.item + + if let cellData = self.userPosts{ + + cell.setupData(cellData[itemIndex].postImgUrl) + } + return cell } @@ -96,3 +142,18 @@ extension ProfileViewController: UICollectionViewDelegateFlowLayout{ } } } + +// MARK: - API 통신 메소드 +extension ProfileViewController { + func successFeedAPI(_ result: UserFeedModel){ + self.userPosts = result.result?.getUserPosts + } + + func successDeletePostAPI(_ isSuccess: Bool){ + guard isSuccess else{return} + + if let deletedIndex = self.deletedIndex{ + self.userPosts?.remove(at: deletedIndex) + } + } +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/DeleteUserFeed/DeleteUserFeed.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/DeleteUserFeed/DeleteUserFeed.swift index bb071d4..668419f 100644 --- a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/DeleteUserFeed/DeleteUserFeed.swift +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/DeleteUserFeed/DeleteUserFeed.swift @@ -6,3 +6,10 @@ // import Foundation + +struct DeleteUserFeed : Decodable{ + let isSuccess: Bool? + let code: Int? + let message: String? + let result: String? +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeed/UserFeedDataManager.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeed/UserFeedDataManager.swift index 71fa167..0218216 100644 --- a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeed/UserFeedDataManager.swift +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeed/UserFeedDataManager.swift @@ -9,6 +9,7 @@ import Alamofire class UserFeedDataManager { + // MARK: 유저 피드 조회 api func getUserFeed(_ viewController : ProfileViewController, _ userID : Int = 2){ AF.request("https://edu-api-ios-test.softsquared.com/users/\(userID)", method: .get, parameters: nil).validate().responseDecodable(of: UserFeedModel.self) { response in switch response.result{ @@ -19,4 +20,16 @@ class UserFeedDataManager { } } } + + // MARK: 게시물 삭제 api + func deleteUserFeed(_ viewController : ProfileViewController, _ postIdx : Int){ + AF.request("https://edu-api-ios-test.softsquared.com/posts/\(postIdx)/status", method: .patch, parameters: nil).validate().responseDecodable(of: DeleteUserFeed.self) { response in + switch response.result{ + case .success(let result): + viewController.successDeletePostAPI(result.isSuccess ?? false) + case .failure(let error): + print(error.localizedDescription) + } + } + } } diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeedDataManager.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeedDataManager.swift deleted file mode 100644 index 8a4766a..0000000 --- a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeedDataManager.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// UserFeedDataManager.swift -// CatStaGram -// -// Created by 송재민 on 2022/05/15. -// - -import Foundation diff --git a/jaem/week7/CatStaGram/CatStaGramTests/CatStaGramTests.swift b/jaem/week7/CatStaGram/CatStaGramTests/CatStaGramTests.swift new file mode 100644 index 0000000..7a2362d --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGramTests/CatStaGramTests.swift @@ -0,0 +1,36 @@ +// +// CatStaGramTests.swift +// CatStaGramTests +// +// Created by 송재민 on 2022/04/02. +// + +import XCTest +@testable import CatStaGram + +class CatStaGramTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/jaem/week7/CatStaGram/CatStaGramUITests/CatStaGramUITests.swift b/jaem/week7/CatStaGram/CatStaGramUITests/CatStaGramUITests.swift new file mode 100644 index 0000000..6cc1585 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGramUITests/CatStaGramUITests.swift @@ -0,0 +1,42 @@ +// +// CatStaGramUITests.swift +// CatStaGramUITests +// +// Created by 송재민 on 2022/04/02. +// + +import XCTest + +class CatStaGramUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/jaem/week7/CatStaGram/CatStaGramUITests/CatStaGramUITestsLaunchTests.swift b/jaem/week7/CatStaGram/CatStaGramUITests/CatStaGramUITestsLaunchTests.swift new file mode 100644 index 0000000..5738e01 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGramUITests/CatStaGramUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// CatStaGramUITestsLaunchTests.swift +// CatStaGramUITests +// +// Created by 송재민 on 2022/04/02. +// + +import XCTest + +class CatStaGramUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/jaem/week7/CatStaGram/Podfile b/jaem/week7/CatStaGram/Podfile new file mode 100644 index 0000000..6bab783 --- /dev/null +++ b/jaem/week7/CatStaGram/Podfile @@ -0,0 +1,11 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'CatStaGram' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for CatStaGram + pod 'Alamofire' + pod 'Kingfisher', '~> 7.0' +end diff --git a/jaem/week7/CatStaGram/Podfile.lock b/jaem/week7/CatStaGram/Podfile.lock new file mode 100644 index 0000000..1046ca6 --- /dev/null +++ b/jaem/week7/CatStaGram/Podfile.lock @@ -0,0 +1,20 @@ +PODS: + - Alamofire (5.6.1) + - Kingfisher (7.2.2) + +DEPENDENCIES: + - Alamofire + - Kingfisher (~> 7.0) + +SPEC REPOS: + trunk: + - Alamofire + - Kingfisher + +SPEC CHECKSUMS: + Alamofire: 87bd8c952f9a4454320fce00d9cc3de57bcadaf5 + Kingfisher: 184d4d1a8c36666e663caf8e08abe87898595c53 + +PODFILE CHECKSUM: fd492e0a81b684bf01518e165eb2e09de28a0ee6 + +COCOAPODS: 1.11.3 diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/LICENSE b/jaem/week7/CatStaGram/Pods/Alamofire/LICENSE new file mode 100644 index 0000000..cae030a --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014-2022 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/README.md b/jaem/week7/CatStaGram/Pods/Alamofire/README.md new file mode 100644 index 0000000..b3fc817 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/README.md @@ -0,0 +1,227 @@ +![Alamofire: Elegant Networking in Swift](https://raw.githubusercontent.com/Alamofire/Alamofire/master/Resources/AlamofireLogo.png) + +[![Swift](https://img.shields.io/badge/Swift-5.3_5.4_5.5_5.6-orange?style=flat-square)](https://img.shields.io/badge/Swift-5.3_5.4_5.5_5.6-Orange?style=flat-square) +[![Platforms](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_Linux_Windows-yellowgreen?style=flat-square)](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_Linux_Windows-Green?style=flat-square) +[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/Alamofire.svg?style=flat-square)](https://img.shields.io/cocoapods/v/Alamofire.svg) +[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat-square)](https://github.com/Carthage/Carthage) +[![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square)](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square) +[![Twitter](https://img.shields.io/badge/twitter-@AlamofireSF-blue.svg?style=flat-square)](https://twitter.com/AlamofireSF) +[![Swift Forums](https://img.shields.io/badge/Swift_Forums-Alamofire-orange?style=flat-square)](https://forums.swift.org/c/related-projects/alamofire/37) + +Alamofire is an HTTP networking library written in Swift. + +- [Features](#features) +- [Component Libraries](#component-libraries) +- [Requirements](#requirements) +- [Migration Guides](#migration-guides) +- [Communication](#communication) +- [Installation](#installation) +- [Contributing](#contributing) +- [Usage](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#using-alamofire) + - [**Introduction -**](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#introduction) [Making Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#making-requests), [Response Handling](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-handling), [Response Validation](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-validation), [Response Caching](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-caching) + - **HTTP -** [HTTP Methods](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#http-methods), [Parameters and Parameter Encoder](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md##request-parameters-and-parameter-encoders), [HTTP Headers](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#http-headers), [Authentication](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#authentication) + - **Large Data -** [Downloading Data to a File](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#downloading-data-to-a-file), [Uploading Data to a Server](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#uploading-data-to-a-server) + - **Tools -** [Statistical Metrics](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#statistical-metrics), [cURL Command Output](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#curl-command-output) +- [Advanced Usage](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md) + - **URL Session -** [Session Manager](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#session), [Session Delegate](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#sessiondelegate), [Request](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#request) + - **Routing -** [Routing Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#routing-requests), [Adapting and Retrying Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#adapting-and-retrying-requests-with-requestinterceptor) + - **Model Objects -** [Custom Response Handlers](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#customizing-response-handlers) + - **Advanced Concurrency -** [Swift Concurrency](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#using-alamofire-with-swift-concurrency) and [Combine](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#using-alamofire-with-combine) + - **Connection -** [Security](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#security), [Network Reachability](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#network-reachability) +- [Open Radars](#open-radars) +- [FAQ](#faq) +- [Credits](#credits) +- [Donations](#donations) +- [License](#license) + +## Features + +- [x] Chainable Request / Response Methods +- [x] Swift Concurrency Support Back to iOS 13, macOS 10.15, tvOS 13, and watchOS 6. +- [x] Combine Support +- [x] URL / JSON Parameter Encoding +- [x] Upload File / Data / Stream / MultipartFormData +- [x] Download File using Request or Resume Data +- [x] Authentication with `URLCredential` +- [x] HTTP Response Validation +- [x] Upload and Download Progress Closures with Progress +- [x] cURL Command Output +- [x] Dynamically Adapt and Retry Requests +- [x] TLS Certificate and Public Key Pinning +- [x] Network Reachability +- [x] Comprehensive Unit and Integration Test Coverage +- [x] [Complete Documentation](https://alamofire.github.io/Alamofire) + +## Component Libraries + +In order to keep Alamofire focused specifically on core networking implementations, additional component libraries have been created by the [Alamofire Software Foundation](https://github.com/Alamofire/Foundation) to bring additional functionality to the Alamofire ecosystem. + +- [AlamofireImage](https://github.com/Alamofire/AlamofireImage) - An image library including image response serializers, `UIImage` and `UIImageView` extensions, custom image filters, an auto-purging in-memory cache, and a priority-based image downloading system. +- [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator) - Controls the visibility of the network activity indicator on iOS using Alamofire. It contains configurable delay timers to help mitigate flicker and can support `URLSession` instances not managed by Alamofire. + +## Requirements + +| Platform | Minimum Swift Version | Installation | Status | +| --- | --- | --- | --- | +| iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+ | 5.3 | [CocoaPods](#cocoapods), [Carthage](#carthage), [Swift Package Manager](#swift-package-manager), [Manual](#manually) | Fully Tested | +| Linux | Latest Only | [Swift Package Manager](#swift-package-manager) | Building But Unsupported | +| Windows | Latest Only | [Swift Package Manager](#swift-package-manager) | Building But Unsupported | + +#### Known Issues on Linux and Windows + +Alamofire builds on Linux and Windows but there are missing features and many issues in the underlying `swift-corelibs-foundation` that prevent full functionality and may cause crashes. These include: +- `ServerTrustManager` and associated certificate functionality is unavailable, so there is no certificate pinning and no client certificate support. +- Various methods of HTTP authentication may crash, including HTTP Basic and HTTP Digest. Crashes may occur if responses contain server challenges. +- Cache control through `CachedResponseHandler` and associated APIs is unavailable, as the underlying delegate methods aren't called. +- `URLSessionTaskMetrics` are never gathered. + +Due to these issues, Alamofire is unsupported on Linux and Windows. Please report any crashes to the [Swift bug reporter](https://bugs.swift.org). + +## Migration Guides + +- [Alamofire 5.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%205.0%20Migration%20Guide.md) +- [Alamofire 4.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%204.0%20Migration%20Guide.md) +- [Alamofire 3.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%203.0%20Migration%20Guide.md) +- [Alamofire 2.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%202.0%20Migration%20Guide.md) + +## Communication +- If you **need help with making network requests** using Alamofire, use [Stack Overflow](https://stackoverflow.com/questions/tagged/alamofire) and tag `alamofire`. +- If you need to **find or understand an API**, check [our documentation](http://alamofire.github.io/Alamofire/) or [Apple's documentation for `URLSession`](https://developer.apple.com/documentation/foundation/url_loading_system), on top of which Alamofire is built. +- If you need **help with an Alamofire feature**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire). +- If you'd like to **discuss Alamofire best practices**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire). +- If you'd like to **discuss a feature request**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire). +- If you **found a bug**, open an issue here on GitHub and follow the guide. The more detail the better! + +## Installation + +### CocoaPods + +[CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate Alamofire into your Xcode project using CocoaPods, specify it in your `Podfile`: + +```ruby +pod 'Alamofire' +``` + +### Carthage + +[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate Alamofire into your Xcode project using Carthage, specify it in your `Cartfile`: + +```ogdl +github "Alamofire/Alamofire" +``` + +### Swift Package Manager + +The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. + +Once you have your Swift package set up, adding Alamofire as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. + +```swift +dependencies: [ + .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.6.1")) +] +``` + +### Manually + +If you prefer not to use any of the aforementioned dependency managers, you can integrate Alamofire into your project manually. + +#### Embedded Framework + +- Open up Terminal, `cd` into your top-level project directory, and run the following command "if" your project is not initialized as a git repository: + + ```bash + $ git init + ``` + +- Add Alamofire as a git [submodule](https://git-scm.com/docs/git-submodule) by running the following command: + + ```bash + $ git submodule add https://github.com/Alamofire/Alamofire.git + ``` + +- Open the new `Alamofire` folder, and drag the `Alamofire.xcodeproj` into the Project Navigator of your application's Xcode project. + + > It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter. + +- Select the `Alamofire.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target. +- Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the "Targets" heading in the sidebar. +- In the tab bar at the top of that window, open the "General" panel. +- Click on the `+` button under the "Embedded Binaries" section. +- You will see two different `Alamofire.xcodeproj` folders each with two different versions of the `Alamofire.framework` nested inside a `Products` folder. + + > It does not matter which `Products` folder you choose from, but it does matter whether you choose the top or bottom `Alamofire.framework`. + +- Select the top `Alamofire.framework` for iOS and the bottom one for macOS. + + > You can verify which one you selected by inspecting the build log for your project. The build target for `Alamofire` will be listed as `Alamofire iOS`, `Alamofire macOS`, `Alamofire tvOS`, or `Alamofire watchOS`. + +- And that's it! + + > The `Alamofire.framework` is automagically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device. + +## Contributing + +Before contributing to Alamofire, please read the instructions detailed in our [contribution guide](https://github.com/Alamofire/Alamofire/blob/master/CONTRIBUTING.md). + +## Open Radars + +The following radars have some effect on the current implementation of Alamofire. + +- [`rdar://21349340`](http://www.openradar.me/radar?id=5517037090635776) - Compiler throwing warning due to toll-free bridging issue in the test case +- `rdar://26870455` - Background URL Session Configurations do not work in the simulator +- `rdar://26849668` - Some URLProtocol APIs do not properly handle `URLRequest` + +## Resolved Radars + +The following radars have been resolved over time after being filed against the Alamofire project. + +- [`rdar://26761490`](http://www.openradar.me/radar?id=5010235949318144) - Swift string interpolation causing memory leak with common usage. + - (Resolved): 9/1/17 in Xcode 9 beta 6. +- [`rdar://36082113`](http://openradar.appspot.com/radar?id=4942308441063424) - `URLSessionTaskMetrics` failing to link on watchOS 3.0+ + - (Resolved): Just add `CFNetwork` to your linked frameworks. +- `FB7624529` - `urlSession(_:task:didFinishCollecting:)` never called on watchOS + - (Resolved): Metrics now collected on watchOS 7+. + +## FAQ + +### What's the origin of the name Alamofire? + +Alamofire is named after the [Alamo Fire flower](https://aggie-horticulture.tamu.edu/wildseed/alamofire.html), a hybrid variant of the Bluebonnet, the official state flower of Texas. + +## Credits + +Alamofire is owned and maintained by the [Alamofire Software Foundation](http://alamofire.org). You can follow them on Twitter at [@AlamofireSF](https://twitter.com/AlamofireSF) for project updates and releases. + +### Security Disclosure + +If you believe you have identified a security vulnerability with Alamofire, you should report it as soon as possible via email to security@alamofire.org. Please do not post it to a public issue tracker. + +## Sponsorship + +The [ASF](https://github.com/Alamofire/Foundation#members) is looking to raise money to officially stay registered as a federal non-profit organization. +Registering will allow Foundation members to gain some legal protections and also allow us to put donations to use, tax-free. +Sponsoring the ASF will enable us to: + +- Pay our yearly legal fees to keep the non-profit in good status +- Pay for our mail servers to help us stay on top of all questions and security issues +- Potentially fund test servers to make it easier for us to test the edge cases +- Potentially fund developers to work on one of our projects full-time + +The community adoption of the ASF libraries has been amazing. +We are greatly humbled by your enthusiasm around the projects and want to continue to do everything we can to move the needle forward. +With your continued support, the ASF will be able to improve its reach and also provide better legal safety for the core members. +If you use any of our libraries for work, see if your employers would be interested in donating. +Any amount you can donate, whether once or monthly, to help us reach our goal would be greatly appreciated. + +[Sponsor Alamofire](https://github.com/sponsors/Alamofire) + +## Supporters + +[MacStadium](https://macstadium.com) provides Alamofire with a free, hosted Mac mini. + +![Powered by MacStadium](https://raw.githubusercontent.com/Alamofire/Alamofire/master/Resources/MacStadiumLogo.png) + +## License + +Alamofire is released under the MIT license. [See LICENSE](https://github.com/Alamofire/Alamofire/blob/master/LICENSE) for details. diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/AFError.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/AFError.swift new file mode 100644 index 0000000..8cd60c7 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/AFError.swift @@ -0,0 +1,870 @@ +// +// AFError.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// `AFError` is the error type returned by Alamofire. It encompasses a few different types of errors, each with +/// their own associated reasons. +public enum AFError: Error { + /// The underlying reason the `.multipartEncodingFailed` error occurred. + public enum MultipartEncodingFailureReason { + /// The `fileURL` provided for reading an encodable body part isn't a file `URL`. + case bodyPartURLInvalid(url: URL) + /// The filename of the `fileURL` provided has either an empty `lastPathComponent` or `pathExtension. + case bodyPartFilenameInvalid(in: URL) + /// The file at the `fileURL` provided was not reachable. + case bodyPartFileNotReachable(at: URL) + /// Attempting to check the reachability of the `fileURL` provided threw an error. + case bodyPartFileNotReachableWithError(atURL: URL, error: Error) + /// The file at the `fileURL` provided is actually a directory. + case bodyPartFileIsDirectory(at: URL) + /// The size of the file at the `fileURL` provided was not returned by the system. + case bodyPartFileSizeNotAvailable(at: URL) + /// The attempt to find the size of the file at the `fileURL` provided threw an error. + case bodyPartFileSizeQueryFailedWithError(forURL: URL, error: Error) + /// An `InputStream` could not be created for the provided `fileURL`. + case bodyPartInputStreamCreationFailed(for: URL) + /// An `OutputStream` could not be created when attempting to write the encoded data to disk. + case outputStreamCreationFailed(for: URL) + /// The encoded body data could not be written to disk because a file already exists at the provided `fileURL`. + case outputStreamFileAlreadyExists(at: URL) + /// The `fileURL` provided for writing the encoded body data to disk is not a file `URL`. + case outputStreamURLInvalid(url: URL) + /// The attempt to write the encoded body data to disk failed with an underlying error. + case outputStreamWriteFailed(error: Error) + /// The attempt to read an encoded body part `InputStream` failed with underlying system error. + case inputStreamReadFailed(error: Error) + } + + /// Represents unexpected input stream length that occur when encoding the `MultipartFormData`. Instances will be + /// embedded within an `AFError.multipartEncodingFailed` `.inputStreamReadFailed` case. + public struct UnexpectedInputStreamLength: Error { + /// The expected byte count to read. + public var bytesExpected: UInt64 + /// The actual byte count read. + public var bytesRead: UInt64 + } + + /// The underlying reason the `.parameterEncodingFailed` error occurred. + public enum ParameterEncodingFailureReason { + /// The `URLRequest` did not have a `URL` to encode. + case missingURL + /// JSON serialization failed with an underlying system error during the encoding process. + case jsonEncodingFailed(error: Error) + /// Custom parameter encoding failed due to the associated `Error`. + case customEncodingFailed(error: Error) + } + + /// The underlying reason the `.parameterEncoderFailed` error occurred. + public enum ParameterEncoderFailureReason { + /// Possible missing components. + public enum RequiredComponent { + /// The `URL` was missing or unable to be extracted from the passed `URLRequest` or during encoding. + case url + /// The `HTTPMethod` could not be extracted from the passed `URLRequest`. + case httpMethod(rawValue: String) + } + + /// A `RequiredComponent` was missing during encoding. + case missingRequiredComponent(RequiredComponent) + /// The underlying encoder failed with the associated error. + case encoderFailed(error: Error) + } + + /// The underlying reason the `.responseValidationFailed` error occurred. + public enum ResponseValidationFailureReason { + /// The data file containing the server response did not exist. + case dataFileNil + /// The data file containing the server response at the associated `URL` could not be read. + case dataFileReadFailed(at: URL) + /// The response did not contain a `Content-Type` and the `acceptableContentTypes` provided did not contain a + /// wildcard type. + case missingContentType(acceptableContentTypes: [String]) + /// The response `Content-Type` did not match any type in the provided `acceptableContentTypes`. + case unacceptableContentType(acceptableContentTypes: [String], responseContentType: String) + /// The response status code was not acceptable. + case unacceptableStatusCode(code: Int) + /// Custom response validation failed due to the associated `Error`. + case customValidationFailed(error: Error) + } + + /// The underlying reason the response serialization error occurred. + public enum ResponseSerializationFailureReason { + /// The server response contained no data or the data was zero length. + case inputDataNilOrZeroLength + /// The file containing the server response did not exist. + case inputFileNil + /// The file containing the server response could not be read from the associated `URL`. + case inputFileReadFailed(at: URL) + /// String serialization failed using the provided `String.Encoding`. + case stringSerializationFailed(encoding: String.Encoding) + /// JSON serialization failed with an underlying system error. + case jsonSerializationFailed(error: Error) + /// A `DataDecoder` failed to decode the response due to the associated `Error`. + case decodingFailed(error: Error) + /// A custom response serializer failed due to the associated `Error`. + case customSerializationFailed(error: Error) + /// Generic serialization failed for an empty response that wasn't type `Empty` but instead the associated type. + case invalidEmptyResponse(type: String) + } + + #if !(os(Linux) || os(Windows)) + /// Underlying reason a server trust evaluation error occurred. + public enum ServerTrustFailureReason { + /// The output of a server trust evaluation. + public struct Output { + /// The host for which the evaluation was performed. + public let host: String + /// The `SecTrust` value which was evaluated. + public let trust: SecTrust + /// The `OSStatus` of evaluation operation. + public let status: OSStatus + /// The result of the evaluation operation. + public let result: SecTrustResultType + + /// Creates an `Output` value from the provided values. + init(_ host: String, _ trust: SecTrust, _ status: OSStatus, _ result: SecTrustResultType) { + self.host = host + self.trust = trust + self.status = status + self.result = result + } + } + + /// No `ServerTrustEvaluator` was found for the associated host. + case noRequiredEvaluator(host: String) + /// No certificates were found with which to perform the trust evaluation. + case noCertificatesFound + /// No public keys were found with which to perform the trust evaluation. + case noPublicKeysFound + /// During evaluation, application of the associated `SecPolicy` failed. + case policyApplicationFailed(trust: SecTrust, policy: SecPolicy, status: OSStatus) + /// During evaluation, setting the associated anchor certificates failed. + case settingAnchorCertificatesFailed(status: OSStatus, certificates: [SecCertificate]) + /// During evaluation, creation of the revocation policy failed. + case revocationPolicyCreationFailed + /// `SecTrust` evaluation failed with the associated `Error`, if one was produced. + case trustEvaluationFailed(error: Error?) + /// Default evaluation failed with the associated `Output`. + case defaultEvaluationFailed(output: Output) + /// Host validation failed with the associated `Output`. + case hostValidationFailed(output: Output) + /// Revocation check failed with the associated `Output` and options. + case revocationCheckFailed(output: Output, options: RevocationTrustEvaluator.Options) + /// Certificate pinning failed. + case certificatePinningFailed(host: String, trust: SecTrust, pinnedCertificates: [SecCertificate], serverCertificates: [SecCertificate]) + /// Public key pinning failed. + case publicKeyPinningFailed(host: String, trust: SecTrust, pinnedKeys: [SecKey], serverKeys: [SecKey]) + /// Custom server trust evaluation failed due to the associated `Error`. + case customEvaluationFailed(error: Error) + } + #endif + + /// The underlying reason the `.urlRequestValidationFailed` + public enum URLRequestValidationFailureReason { + /// URLRequest with GET method had body data. + case bodyDataInGETRequest(Data) + } + + /// `UploadableConvertible` threw an error in `createUploadable()`. + case createUploadableFailed(error: Error) + /// `URLRequestConvertible` threw an error in `asURLRequest()`. + case createURLRequestFailed(error: Error) + /// `SessionDelegate` threw an error while attempting to move downloaded file to destination URL. + case downloadedFileMoveFailed(error: Error, source: URL, destination: URL) + /// `Request` was explicitly cancelled. + case explicitlyCancelled + /// `URLConvertible` type failed to create a valid `URL`. + case invalidURL(url: URLConvertible) + /// Multipart form encoding failed. + case multipartEncodingFailed(reason: MultipartEncodingFailureReason) + /// `ParameterEncoding` threw an error during the encoding process. + case parameterEncodingFailed(reason: ParameterEncodingFailureReason) + /// `ParameterEncoder` threw an error while running the encoder. + case parameterEncoderFailed(reason: ParameterEncoderFailureReason) + /// `RequestAdapter` threw an error during adaptation. + case requestAdaptationFailed(error: Error) + /// `RequestRetrier` threw an error during the request retry process. + case requestRetryFailed(retryError: Error, originalError: Error) + /// Response validation failed. + case responseValidationFailed(reason: ResponseValidationFailureReason) + /// Response serialization failed. + case responseSerializationFailed(reason: ResponseSerializationFailureReason) + #if !(os(Linux) || os(Windows)) + /// `ServerTrustEvaluating` instance threw an error during trust evaluation. + case serverTrustEvaluationFailed(reason: ServerTrustFailureReason) + #endif + /// `Session` which issued the `Request` was deinitialized, most likely because its reference went out of scope. + case sessionDeinitialized + /// `Session` was explicitly invalidated, possibly with the `Error` produced by the underlying `URLSession`. + case sessionInvalidated(error: Error?) + /// `URLSessionTask` completed with error. + case sessionTaskFailed(error: Error) + /// `URLRequest` failed validation. + case urlRequestValidationFailed(reason: URLRequestValidationFailureReason) +} + +extension Error { + /// Returns the instance cast as an `AFError`. + public var asAFError: AFError? { + self as? AFError + } + + /// Returns the instance cast as an `AFError`. If casting fails, a `fatalError` with the specified `message` is thrown. + public func asAFError(orFailWith message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) -> AFError { + guard let afError = self as? AFError else { + fatalError(message(), file: file, line: line) + } + return afError + } + + /// Casts the instance as `AFError` or returns `defaultAFError` + func asAFError(or defaultAFError: @autoclosure () -> AFError) -> AFError { + self as? AFError ?? defaultAFError() + } +} + +// MARK: - Error Booleans + +extension AFError { + /// Returns whether the instance is `.sessionDeinitialized`. + public var isSessionDeinitializedError: Bool { + if case .sessionDeinitialized = self { return true } + return false + } + + /// Returns whether the instance is `.sessionInvalidated`. + public var isSessionInvalidatedError: Bool { + if case .sessionInvalidated = self { return true } + return false + } + + /// Returns whether the instance is `.explicitlyCancelled`. + public var isExplicitlyCancelledError: Bool { + if case .explicitlyCancelled = self { return true } + return false + } + + /// Returns whether the instance is `.invalidURL`. + public var isInvalidURLError: Bool { + if case .invalidURL = self { return true } + return false + } + + /// Returns whether the instance is `.parameterEncodingFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isParameterEncodingError: Bool { + if case .parameterEncodingFailed = self { return true } + return false + } + + /// Returns whether the instance is `.parameterEncoderFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isParameterEncoderError: Bool { + if case .parameterEncoderFailed = self { return true } + return false + } + + /// Returns whether the instance is `.multipartEncodingFailed`. When `true`, the `url` and `underlyingError` + /// properties will contain the associated values. + public var isMultipartEncodingError: Bool { + if case .multipartEncodingFailed = self { return true } + return false + } + + /// Returns whether the instance is `.requestAdaptationFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isRequestAdaptationError: Bool { + if case .requestAdaptationFailed = self { return true } + return false + } + + /// Returns whether the instance is `.responseValidationFailed`. When `true`, the `acceptableContentTypes`, + /// `responseContentType`, `responseCode`, and `underlyingError` properties will contain the associated values. + public var isResponseValidationError: Bool { + if case .responseValidationFailed = self { return true } + return false + } + + /// Returns whether the instance is `.responseSerializationFailed`. When `true`, the `failedStringEncoding` and + /// `underlyingError` properties will contain the associated values. + public var isResponseSerializationError: Bool { + if case .responseSerializationFailed = self { return true } + return false + } + + #if !(os(Linux) || os(Windows)) + /// Returns whether the instance is `.serverTrustEvaluationFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isServerTrustEvaluationError: Bool { + if case .serverTrustEvaluationFailed = self { return true } + return false + } + #endif + + /// Returns whether the instance is `requestRetryFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isRequestRetryError: Bool { + if case .requestRetryFailed = self { return true } + return false + } + + /// Returns whether the instance is `createUploadableFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isCreateUploadableError: Bool { + if case .createUploadableFailed = self { return true } + return false + } + + /// Returns whether the instance is `createURLRequestFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isCreateURLRequestError: Bool { + if case .createURLRequestFailed = self { return true } + return false + } + + /// Returns whether the instance is `downloadedFileMoveFailed`. When `true`, the `destination` and `underlyingError` properties will + /// contain the associated values. + public var isDownloadedFileMoveError: Bool { + if case .downloadedFileMoveFailed = self { return true } + return false + } + + /// Returns whether the instance is `createURLRequestFailed`. When `true`, the `underlyingError` property will + /// contain the associated value. + public var isSessionTaskError: Bool { + if case .sessionTaskFailed = self { return true } + return false + } +} + +// MARK: - Convenience Properties + +extension AFError { + /// The `URLConvertible` associated with the error. + public var urlConvertible: URLConvertible? { + guard case let .invalidURL(url) = self else { return nil } + return url + } + + /// The `URL` associated with the error. + public var url: URL? { + guard case let .multipartEncodingFailed(reason) = self else { return nil } + return reason.url + } + + /// The underlying `Error` responsible for generating the failure associated with `.sessionInvalidated`, + /// `.parameterEncodingFailed`, `.parameterEncoderFailed`, `.multipartEncodingFailed`, `.requestAdaptationFailed`, + /// `.responseSerializationFailed`, `.requestRetryFailed` errors. + public var underlyingError: Error? { + switch self { + case let .multipartEncodingFailed(reason): + return reason.underlyingError + case let .parameterEncodingFailed(reason): + return reason.underlyingError + case let .parameterEncoderFailed(reason): + return reason.underlyingError + case let .requestAdaptationFailed(error): + return error + case let .requestRetryFailed(retryError, _): + return retryError + case let .responseValidationFailed(reason): + return reason.underlyingError + case let .responseSerializationFailed(reason): + return reason.underlyingError + #if !(os(Linux) || os(Windows)) + case let .serverTrustEvaluationFailed(reason): + return reason.underlyingError + #endif + case let .sessionInvalidated(error): + return error + case let .createUploadableFailed(error): + return error + case let .createURLRequestFailed(error): + return error + case let .downloadedFileMoveFailed(error, _, _): + return error + case let .sessionTaskFailed(error): + return error + case .explicitlyCancelled, + .invalidURL, + .sessionDeinitialized, + .urlRequestValidationFailed: + return nil + } + } + + /// The acceptable `Content-Type`s of a `.responseValidationFailed` error. + public var acceptableContentTypes: [String]? { + guard case let .responseValidationFailed(reason) = self else { return nil } + return reason.acceptableContentTypes + } + + /// The response `Content-Type` of a `.responseValidationFailed` error. + public var responseContentType: String? { + guard case let .responseValidationFailed(reason) = self else { return nil } + return reason.responseContentType + } + + /// The response code of a `.responseValidationFailed` error. + public var responseCode: Int? { + guard case let .responseValidationFailed(reason) = self else { return nil } + return reason.responseCode + } + + /// The `String.Encoding` associated with a failed `.stringResponse()` call. + public var failedStringEncoding: String.Encoding? { + guard case let .responseSerializationFailed(reason) = self else { return nil } + return reason.failedStringEncoding + } + + /// The `source` URL of a `.downloadedFileMoveFailed` error. + public var sourceURL: URL? { + guard case let .downloadedFileMoveFailed(_, source, _) = self else { return nil } + return source + } + + /// The `destination` URL of a `.downloadedFileMoveFailed` error. + public var destinationURL: URL? { + guard case let .downloadedFileMoveFailed(_, _, destination) = self else { return nil } + return destination + } + + #if !(os(Linux) || os(Windows)) + /// The download resume data of any underlying network error. Only produced by `DownloadRequest`s. + public var downloadResumeData: Data? { + (underlyingError as? URLError)?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data + } + #endif +} + +extension AFError.ParameterEncodingFailureReason { + var underlyingError: Error? { + switch self { + case let .jsonEncodingFailed(error), + let .customEncodingFailed(error): + return error + case .missingURL: + return nil + } + } +} + +extension AFError.ParameterEncoderFailureReason { + var underlyingError: Error? { + switch self { + case let .encoderFailed(error): + return error + case .missingRequiredComponent: + return nil + } + } +} + +extension AFError.MultipartEncodingFailureReason { + var url: URL? { + switch self { + case let .bodyPartURLInvalid(url), + let .bodyPartFilenameInvalid(url), + let .bodyPartFileNotReachable(url), + let .bodyPartFileIsDirectory(url), + let .bodyPartFileSizeNotAvailable(url), + let .bodyPartInputStreamCreationFailed(url), + let .outputStreamCreationFailed(url), + let .outputStreamFileAlreadyExists(url), + let .outputStreamURLInvalid(url), + let .bodyPartFileNotReachableWithError(url, _), + let .bodyPartFileSizeQueryFailedWithError(url, _): + return url + case .outputStreamWriteFailed, + .inputStreamReadFailed: + return nil + } + } + + var underlyingError: Error? { + switch self { + case let .bodyPartFileNotReachableWithError(_, error), + let .bodyPartFileSizeQueryFailedWithError(_, error), + let .outputStreamWriteFailed(error), + let .inputStreamReadFailed(error): + return error + case .bodyPartURLInvalid, + .bodyPartFilenameInvalid, + .bodyPartFileNotReachable, + .bodyPartFileIsDirectory, + .bodyPartFileSizeNotAvailable, + .bodyPartInputStreamCreationFailed, + .outputStreamCreationFailed, + .outputStreamFileAlreadyExists, + .outputStreamURLInvalid: + return nil + } + } +} + +extension AFError.ResponseValidationFailureReason { + var acceptableContentTypes: [String]? { + switch self { + case let .missingContentType(types), + let .unacceptableContentType(types, _): + return types + case .dataFileNil, + .dataFileReadFailed, + .unacceptableStatusCode, + .customValidationFailed: + return nil + } + } + + var responseContentType: String? { + switch self { + case let .unacceptableContentType(_, responseType): + return responseType + case .dataFileNil, + .dataFileReadFailed, + .missingContentType, + .unacceptableStatusCode, + .customValidationFailed: + return nil + } + } + + var responseCode: Int? { + switch self { + case let .unacceptableStatusCode(code): + return code + case .dataFileNil, + .dataFileReadFailed, + .missingContentType, + .unacceptableContentType, + .customValidationFailed: + return nil + } + } + + var underlyingError: Error? { + switch self { + case let .customValidationFailed(error): + return error + case .dataFileNil, + .dataFileReadFailed, + .missingContentType, + .unacceptableContentType, + .unacceptableStatusCode: + return nil + } + } +} + +extension AFError.ResponseSerializationFailureReason { + var failedStringEncoding: String.Encoding? { + switch self { + case let .stringSerializationFailed(encoding): + return encoding + case .inputDataNilOrZeroLength, + .inputFileNil, + .inputFileReadFailed(_), + .jsonSerializationFailed(_), + .decodingFailed(_), + .customSerializationFailed(_), + .invalidEmptyResponse: + return nil + } + } + + var underlyingError: Error? { + switch self { + case let .jsonSerializationFailed(error), + let .decodingFailed(error), + let .customSerializationFailed(error): + return error + case .inputDataNilOrZeroLength, + .inputFileNil, + .inputFileReadFailed, + .stringSerializationFailed, + .invalidEmptyResponse: + return nil + } + } +} + +#if !(os(Linux) || os(Windows)) +extension AFError.ServerTrustFailureReason { + var output: AFError.ServerTrustFailureReason.Output? { + switch self { + case let .defaultEvaluationFailed(output), + let .hostValidationFailed(output), + let .revocationCheckFailed(output, _): + return output + case .noRequiredEvaluator, + .noCertificatesFound, + .noPublicKeysFound, + .policyApplicationFailed, + .settingAnchorCertificatesFailed, + .revocationPolicyCreationFailed, + .trustEvaluationFailed, + .certificatePinningFailed, + .publicKeyPinningFailed, + .customEvaluationFailed: + return nil + } + } + + var underlyingError: Error? { + switch self { + case let .customEvaluationFailed(error): + return error + case let .trustEvaluationFailed(error): + return error + case .noRequiredEvaluator, + .noCertificatesFound, + .noPublicKeysFound, + .policyApplicationFailed, + .settingAnchorCertificatesFailed, + .revocationPolicyCreationFailed, + .defaultEvaluationFailed, + .hostValidationFailed, + .revocationCheckFailed, + .certificatePinningFailed, + .publicKeyPinningFailed: + return nil + } + } +} +#endif + +// MARK: - Error Descriptions + +extension AFError: LocalizedError { + public var errorDescription: String? { + switch self { + case .explicitlyCancelled: + return "Request explicitly cancelled." + case let .invalidURL(url): + return "URL is not valid: \(url)" + case let .parameterEncodingFailed(reason): + return reason.localizedDescription + case let .parameterEncoderFailed(reason): + return reason.localizedDescription + case let .multipartEncodingFailed(reason): + return reason.localizedDescription + case let .requestAdaptationFailed(error): + return "Request adaption failed with error: \(error.localizedDescription)" + case let .responseValidationFailed(reason): + return reason.localizedDescription + case let .responseSerializationFailed(reason): + return reason.localizedDescription + case let .requestRetryFailed(retryError, originalError): + return """ + Request retry failed with retry error: \(retryError.localizedDescription), \ + original error: \(originalError.localizedDescription) + """ + case .sessionDeinitialized: + return """ + Session was invalidated without error, so it was likely deinitialized unexpectedly. \ + Be sure to retain a reference to your Session for the duration of your requests. + """ + case let .sessionInvalidated(error): + return "Session was invalidated with error: \(error?.localizedDescription ?? "No description.")" + #if !(os(Linux) || os(Windows)) + case let .serverTrustEvaluationFailed(reason): + return "Server trust evaluation failed due to reason: \(reason.localizedDescription)" + #endif + case let .urlRequestValidationFailed(reason): + return "URLRequest validation failed due to reason: \(reason.localizedDescription)" + case let .createUploadableFailed(error): + return "Uploadable creation failed with error: \(error.localizedDescription)" + case let .createURLRequestFailed(error): + return "URLRequest creation failed with error: \(error.localizedDescription)" + case let .downloadedFileMoveFailed(error, source, destination): + return "Moving downloaded file from: \(source) to: \(destination) failed with error: \(error.localizedDescription)" + case let .sessionTaskFailed(error): + return "URLSessionTask failed with error: \(error.localizedDescription)" + } + } +} + +extension AFError.ParameterEncodingFailureReason { + var localizedDescription: String { + switch self { + case .missingURL: + return "URL request to encode was missing a URL" + case let .jsonEncodingFailed(error): + return "JSON could not be encoded because of error:\n\(error.localizedDescription)" + case let .customEncodingFailed(error): + return "Custom parameter encoder failed with error: \(error.localizedDescription)" + } + } +} + +extension AFError.ParameterEncoderFailureReason { + var localizedDescription: String { + switch self { + case let .missingRequiredComponent(component): + return "Encoding failed due to a missing request component: \(component)" + case let .encoderFailed(error): + return "The underlying encoder failed with the error: \(error)" + } + } +} + +extension AFError.MultipartEncodingFailureReason { + var localizedDescription: String { + switch self { + case let .bodyPartURLInvalid(url): + return "The URL provided is not a file URL: \(url)" + case let .bodyPartFilenameInvalid(url): + return "The URL provided does not have a valid filename: \(url)" + case let .bodyPartFileNotReachable(url): + return "The URL provided is not reachable: \(url)" + case let .bodyPartFileNotReachableWithError(url, error): + return """ + The system returned an error while checking the provided URL for reachability. + URL: \(url) + Error: \(error) + """ + case let .bodyPartFileIsDirectory(url): + return "The URL provided is a directory: \(url)" + case let .bodyPartFileSizeNotAvailable(url): + return "Could not fetch the file size from the provided URL: \(url)" + case let .bodyPartFileSizeQueryFailedWithError(url, error): + return """ + The system returned an error while attempting to fetch the file size from the provided URL. + URL: \(url) + Error: \(error) + """ + case let .bodyPartInputStreamCreationFailed(url): + return "Failed to create an InputStream for the provided URL: \(url)" + case let .outputStreamCreationFailed(url): + return "Failed to create an OutputStream for URL: \(url)" + case let .outputStreamFileAlreadyExists(url): + return "A file already exists at the provided URL: \(url)" + case let .outputStreamURLInvalid(url): + return "The provided OutputStream URL is invalid: \(url)" + case let .outputStreamWriteFailed(error): + return "OutputStream write failed with error: \(error)" + case let .inputStreamReadFailed(error): + return "InputStream read failed with error: \(error)" + } + } +} + +extension AFError.ResponseSerializationFailureReason { + var localizedDescription: String { + switch self { + case .inputDataNilOrZeroLength: + return "Response could not be serialized, input data was nil or zero length." + case .inputFileNil: + return "Response could not be serialized, input file was nil." + case let .inputFileReadFailed(url): + return "Response could not be serialized, input file could not be read: \(url)." + case let .stringSerializationFailed(encoding): + return "String could not be serialized with encoding: \(encoding)." + case let .jsonSerializationFailed(error): + return "JSON could not be serialized because of error:\n\(error.localizedDescription)" + case let .invalidEmptyResponse(type): + return """ + Empty response could not be serialized to type: \(type). \ + Use Empty as the expected type for such responses. + """ + case let .decodingFailed(error): + return "Response could not be decoded because of error:\n\(error.localizedDescription)" + case let .customSerializationFailed(error): + return "Custom response serializer failed with error:\n\(error.localizedDescription)" + } + } +} + +extension AFError.ResponseValidationFailureReason { + var localizedDescription: String { + switch self { + case .dataFileNil: + return "Response could not be validated, data file was nil." + case let .dataFileReadFailed(url): + return "Response could not be validated, data file could not be read: \(url)." + case let .missingContentType(types): + return """ + Response Content-Type was missing and acceptable content types \ + (\(types.joined(separator: ","))) do not match "*/*". + """ + case let .unacceptableContentType(acceptableTypes, responseType): + return """ + Response Content-Type "\(responseType)" does not match any acceptable types: \ + \(acceptableTypes.joined(separator: ",")). + """ + case let .unacceptableStatusCode(code): + return "Response status code was unacceptable: \(code)." + case let .customValidationFailed(error): + return "Custom response validation failed with error: \(error.localizedDescription)" + } + } +} + +#if !(os(Linux) || os(Windows)) +extension AFError.ServerTrustFailureReason { + var localizedDescription: String { + switch self { + case let .noRequiredEvaluator(host): + return "A ServerTrustEvaluating value is required for host \(host) but none was found." + case .noCertificatesFound: + return "No certificates were found or provided for evaluation." + case .noPublicKeysFound: + return "No public keys were found or provided for evaluation." + case .policyApplicationFailed: + return "Attempting to set a SecPolicy failed." + case .settingAnchorCertificatesFailed: + return "Attempting to set the provided certificates as anchor certificates failed." + case .revocationPolicyCreationFailed: + return "Attempting to create a revocation policy failed." + case let .trustEvaluationFailed(error): + return "SecTrust evaluation failed with error: \(error?.localizedDescription ?? "None")" + case let .defaultEvaluationFailed(output): + return "Default evaluation failed for host \(output.host)." + case let .hostValidationFailed(output): + return "Host validation failed for host \(output.host)." + case let .revocationCheckFailed(output, _): + return "Revocation check failed for host \(output.host)." + case let .certificatePinningFailed(host, _, _, _): + return "Certificate pinning failed for host \(host)." + case let .publicKeyPinningFailed(host, _, _, _): + return "Public key pinning failed for host \(host)." + case let .customEvaluationFailed(error): + return "Custom trust evaluation failed with error: \(error.localizedDescription)" + } + } +} +#endif + +extension AFError.URLRequestValidationFailureReason { + var localizedDescription: String { + switch self { + case let .bodyDataInGETRequest(data): + return """ + Invalid URLRequest: Requests with GET method cannot have body data: + \(String(decoding: data, as: UTF8.self)) + """ + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/Alamofire.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Alamofire.swift new file mode 100644 index 0000000..d6fc39e --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Alamofire.swift @@ -0,0 +1,40 @@ +// +// Alamofire.swift +// +// Copyright (c) 2014-2021 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Dispatch +import Foundation +#if canImport(FoundationNetworking) +@_exported import FoundationNetworking +#endif + +// Enforce minimum Swift version for all platforms and build systems. +#if swift(<5.3) +#error("Alamofire doesn't support Swift versions below 5.3.") +#endif + +/// Reference to `Session.default` for quick bootstrapping and examples. +public let AF = Session.default + +/// Current Alamofire version. Necessary since SPM doesn't use dynamic libraries. Plus this will be more accurate. +let version = "5.6.1" diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/AlamofireExtended.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/AlamofireExtended.swift new file mode 100644 index 0000000..280c6de --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/AlamofireExtended.swift @@ -0,0 +1,61 @@ +// +// AlamofireExtended.swift +// +// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +/// Type that acts as a generic extension point for all `AlamofireExtended` types. +public struct AlamofireExtension { + /// Stores the type or meta-type of any extended type. + public private(set) var type: ExtendedType + + /// Create an instance from the provided value. + /// + /// - Parameter type: Instance being extended. + public init(_ type: ExtendedType) { + self.type = type + } +} + +/// Protocol describing the `af` extension points for Alamofire extended types. +public protocol AlamofireExtended { + /// Type being extended. + associatedtype ExtendedType + + /// Static Alamofire extension point. + static var af: AlamofireExtension.Type { get set } + /// Instance Alamofire extension point. + var af: AlamofireExtension { get set } +} + +extension AlamofireExtended { + /// Static Alamofire extension point. + public static var af: AlamofireExtension.Type { + get { AlamofireExtension.self } + set {} + } + + /// Instance Alamofire extension point. + public var af: AlamofireExtension { + get { AlamofireExtension(self) } + set {} + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/AuthenticationInterceptor.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/AuthenticationInterceptor.swift new file mode 100644 index 0000000..c3a3f31 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/AuthenticationInterceptor.swift @@ -0,0 +1,403 @@ +// +// AuthenticationInterceptor.swift +// +// Copyright (c) 2020 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Types adopting the `AuthenticationCredential` protocol can be used to authenticate `URLRequest`s. +/// +/// One common example of an `AuthenticationCredential` is an OAuth2 credential containing an access token used to +/// authenticate all requests on behalf of a user. The access token generally has an expiration window of 60 minutes +/// which will then require a refresh of the credential using the refresh token to generate a new access token. +public protocol AuthenticationCredential { + /// Whether the credential requires a refresh. This property should always return `true` when the credential is + /// expired. It is also wise to consider returning `true` when the credential will expire in several seconds or + /// minutes depending on the expiration window of the credential. + /// + /// For example, if the credential is valid for 60 minutes, then it would be wise to return `true` when the + /// credential is only valid for 5 minutes or less. That ensures the credential will not expire as it is passed + /// around backend services. + var requiresRefresh: Bool { get } +} + +// MARK: - + +/// Types adopting the `Authenticator` protocol can be used to authenticate `URLRequest`s with an +/// `AuthenticationCredential` as well as refresh the `AuthenticationCredential` when required. +public protocol Authenticator: AnyObject { + /// The type of credential associated with the `Authenticator` instance. + associatedtype Credential: AuthenticationCredential + + /// Applies the `Credential` to the `URLRequest`. + /// + /// In the case of OAuth2, the access token of the `Credential` would be added to the `URLRequest` as a Bearer + /// token to the `Authorization` header. + /// + /// - Parameters: + /// - credential: The `Credential`. + /// - urlRequest: The `URLRequest`. + func apply(_ credential: Credential, to urlRequest: inout URLRequest) + + /// Refreshes the `Credential` and executes the `completion` closure with the `Result` once complete. + /// + /// Refresh can be called in one of two ways. It can be called before the `Request` is actually executed due to + /// a `requiresRefresh` returning `true` during the adapt portion of the `Request` creation process. It can also + /// be triggered by a failed `Request` where the authentication server denied access due to an expired or + /// invalidated access token. + /// + /// In the case of OAuth2, this method would use the refresh token of the `Credential` to generate a new + /// `Credential` using the authentication service. Once complete, the `completion` closure should be called with + /// the new `Credential`, or the error that occurred. + /// + /// In general, if the refresh call fails with certain status codes from the authentication server (commonly a 401), + /// the refresh token in the `Credential` can no longer be used to generate a valid `Credential`. In these cases, + /// you will need to reauthenticate the user with their username / password. + /// + /// Please note, these are just general examples of common use cases. They are not meant to solve your specific + /// authentication server challenges. Please work with your authentication server team to ensure your + /// `Authenticator` logic matches their expectations. + /// + /// - Parameters: + /// - credential: The `Credential` to refresh. + /// - session: The `Session` requiring the refresh. + /// - completion: The closure to be executed once the refresh is complete. + func refresh(_ credential: Credential, for session: Session, completion: @escaping (Result) -> Void) + + /// Determines whether the `URLRequest` failed due to an authentication error based on the `HTTPURLResponse`. + /// + /// If the authentication server **CANNOT** invalidate credentials after they are issued, then simply return `false` + /// for this method. If the authentication server **CAN** invalidate credentials due to security breaches, then you + /// will need to work with your authentication server team to understand how to identify when this occurs. + /// + /// In the case of OAuth2, where an authentication server can invalidate credentials, you will need to inspect the + /// `HTTPURLResponse` or possibly the `Error` for when this occurs. This is commonly handled by the authentication + /// server returning a 401 status code and some additional header to indicate an OAuth2 failure occurred. + /// + /// It is very important to understand how your authentication server works to be able to implement this correctly. + /// For example, if your authentication server returns a 401 when an OAuth2 error occurs, and your downstream + /// service also returns a 401 when you are not authorized to perform that operation, how do you know which layer + /// of the backend returned you a 401? You do not want to trigger a refresh unless you know your authentication + /// server is actually the layer rejecting the request. Again, work with your authentication server team to understand + /// how to identify an OAuth2 401 error vs. a downstream 401 error to avoid endless refresh loops. + /// + /// - Parameters: + /// - urlRequest: The `URLRequest`. + /// - response: The `HTTPURLResponse`. + /// - error: The `Error`. + /// + /// - Returns: `true` if the `URLRequest` failed due to an authentication error, `false` otherwise. + func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool + + /// Determines whether the `URLRequest` is authenticated with the `Credential`. + /// + /// If the authentication server **CANNOT** invalidate credentials after they are issued, then simply return `true` + /// for this method. If the authentication server **CAN** invalidate credentials due to security breaches, then + /// read on. + /// + /// When an authentication server can invalidate credentials, it means that you may have a non-expired credential + /// that appears to be valid, but will be rejected by the authentication server when used. Generally when this + /// happens, a number of requests are all sent when the application is foregrounded, and all of them will be + /// rejected by the authentication server in the order they are received. The first failed request will trigger a + /// refresh internally, which will update the credential, and then retry all the queued requests with the new + /// credential. However, it is possible that some of the original requests will not return from the authentication + /// server until the refresh has completed. This is where this method comes in. + /// + /// When the authentication server rejects a credential, we need to check to make sure we haven't refreshed the + /// credential while the request was in flight. If it has already refreshed, then we don't need to trigger an + /// additional refresh. If it hasn't refreshed, then we need to refresh. + /// + /// Now that it is understood how the result of this method is used in the refresh lifecyle, let's walk through how + /// to implement it. You should return `true` in this method if the `URLRequest` is authenticated in a way that + /// matches the values in the `Credential`. In the case of OAuth2, this would mean that the Bearer token in the + /// `Authorization` header of the `URLRequest` matches the access token in the `Credential`. If it matches, then we + /// know the `Credential` was used to authenticate the `URLRequest` and should return `true`. If the Bearer token + /// did not match the access token, then you should return `false`. + /// + /// - Parameters: + /// - urlRequest: The `URLRequest`. + /// - credential: The `Credential`. + /// + /// - Returns: `true` if the `URLRequest` is authenticated with the `Credential`, `false` otherwise. + func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: Credential) -> Bool +} + +// MARK: - + +/// Represents various authentication failures that occur when using the `AuthenticationInterceptor`. All errors are +/// still vended from Alamofire as `AFError` types. The `AuthenticationError` instances will be embedded within +/// `AFError` `.requestAdaptationFailed` or `.requestRetryFailed` cases. +public enum AuthenticationError: Error { + /// The credential was missing so the request could not be authenticated. + case missingCredential + /// The credential was refreshed too many times within the `RefreshWindow`. + case excessiveRefresh +} + +// MARK: - + +/// The `AuthenticationInterceptor` class manages the queuing and threading complexity of authenticating requests. +/// It relies on an `Authenticator` type to handle the actual `URLRequest` authentication and `Credential` refresh. +public class AuthenticationInterceptor: RequestInterceptor where AuthenticatorType: Authenticator { + // MARK: Typealiases + + /// Type of credential used to authenticate requests. + public typealias Credential = AuthenticatorType.Credential + + // MARK: Helper Types + + /// Type that defines a time window used to identify excessive refresh calls. When enabled, prior to executing a + /// refresh, the `AuthenticationInterceptor` compares the timestamp history of previous refresh calls against the + /// `RefreshWindow`. If more refreshes have occurred within the refresh window than allowed, the refresh is + /// cancelled and an `AuthorizationError.excessiveRefresh` error is thrown. + public struct RefreshWindow { + /// `TimeInterval` defining the duration of the time window before the current time in which the number of + /// refresh attempts is compared against `maximumAttempts`. For example, if `interval` is 30 seconds, then the + /// `RefreshWindow` represents the past 30 seconds. If more attempts occurred in the past 30 seconds than + /// `maximumAttempts`, an `.excessiveRefresh` error will be thrown. + public let interval: TimeInterval + + /// Total refresh attempts allowed within `interval` before throwing an `.excessiveRefresh` error. + public let maximumAttempts: Int + + /// Creates a `RefreshWindow` instance from the specified `interval` and `maximumAttempts`. + /// + /// - Parameters: + /// - interval: `TimeInterval` defining the duration of the time window before the current time. + /// - maximumAttempts: The maximum attempts allowed within the `TimeInterval`. + public init(interval: TimeInterval = 30.0, maximumAttempts: Int = 5) { + self.interval = interval + self.maximumAttempts = maximumAttempts + } + } + + private struct AdaptOperation { + let urlRequest: URLRequest + let session: Session + let completion: (Result) -> Void + } + + private enum AdaptResult { + case adapt(Credential) + case doNotAdapt(AuthenticationError) + case adaptDeferred + } + + private struct MutableState { + var credential: Credential? + + var isRefreshing = false + var refreshTimestamps: [TimeInterval] = [] + var refreshWindow: RefreshWindow? + + var adaptOperations: [AdaptOperation] = [] + var requestsToRetry: [(RetryResult) -> Void] = [] + } + + // MARK: Properties + + /// The `Credential` used to authenticate requests. + public var credential: Credential? { + get { $mutableState.credential } + set { $mutableState.credential = newValue } + } + + let authenticator: AuthenticatorType + let queue = DispatchQueue(label: "org.alamofire.authentication.inspector") + + @Protected + private var mutableState: MutableState + + // MARK: Initialization + + /// Creates an `AuthenticationInterceptor` instance from the specified parameters. + /// + /// A `nil` `RefreshWindow` will result in the `AuthenticationInterceptor` not checking for excessive refresh calls. + /// It is recommended to always use a `RefreshWindow` to avoid endless refresh cycles. + /// + /// - Parameters: + /// - authenticator: The `Authenticator` type. + /// - credential: The `Credential` if it exists. `nil` by default. + /// - refreshWindow: The `RefreshWindow` used to identify excessive refresh calls. `RefreshWindow()` by default. + public init(authenticator: AuthenticatorType, + credential: Credential? = nil, + refreshWindow: RefreshWindow? = RefreshWindow()) { + self.authenticator = authenticator + mutableState = MutableState(credential: credential, refreshWindow: refreshWindow) + } + + // MARK: Adapt + + public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + let adaptResult: AdaptResult = $mutableState.write { mutableState in + // Queue the adapt operation if a refresh is already in place. + guard !mutableState.isRefreshing else { + let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion) + mutableState.adaptOperations.append(operation) + return .adaptDeferred + } + + // Throw missing credential error is the credential is missing. + guard let credential = mutableState.credential else { + let error = AuthenticationError.missingCredential + return .doNotAdapt(error) + } + + // Queue the adapt operation and trigger refresh operation if credential requires refresh. + guard !credential.requiresRefresh else { + let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion) + mutableState.adaptOperations.append(operation) + refresh(credential, for: session, insideLock: &mutableState) + return .adaptDeferred + } + + return .adapt(credential) + } + + switch adaptResult { + case let .adapt(credential): + var authenticatedRequest = urlRequest + authenticator.apply(credential, to: &authenticatedRequest) + completion(.success(authenticatedRequest)) + + case let .doNotAdapt(adaptError): + completion(.failure(adaptError)) + + case .adaptDeferred: + // No-op: adapt operation captured during refresh. + break + } + } + + // MARK: Retry + + public func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) { + // Do not attempt retry if there was not an original request and response from the server. + guard let urlRequest = request.request, let response = request.response else { + completion(.doNotRetry) + return + } + + // Do not attempt retry unless the `Authenticator` verifies failure was due to authentication error (i.e. 401 status code). + guard authenticator.didRequest(urlRequest, with: response, failDueToAuthenticationError: error) else { + completion(.doNotRetry) + return + } + + // Do not attempt retry if there is no credential. + guard let credential = credential else { + let error = AuthenticationError.missingCredential + completion(.doNotRetryWithError(error)) + return + } + + // Retry the request if the `Authenticator` verifies it was authenticated with a previous credential. + guard authenticator.isRequest(urlRequest, authenticatedWith: credential) else { + completion(.retry) + return + } + + $mutableState.write { mutableState in + mutableState.requestsToRetry.append(completion) + + guard !mutableState.isRefreshing else { return } + + refresh(credential, for: session, insideLock: &mutableState) + } + } + + // MARK: Refresh + + private func refresh(_ credential: Credential, for session: Session, insideLock mutableState: inout MutableState) { + guard !isRefreshExcessive(insideLock: &mutableState) else { + let error = AuthenticationError.excessiveRefresh + handleRefreshFailure(error, insideLock: &mutableState) + return + } + + mutableState.refreshTimestamps.append(ProcessInfo.processInfo.systemUptime) + mutableState.isRefreshing = true + + // Dispatch to queue to hop out of the lock in case authenticator.refresh is implemented synchronously. + queue.async { + self.authenticator.refresh(credential, for: session) { result in + self.$mutableState.write { mutableState in + switch result { + case let .success(credential): + self.handleRefreshSuccess(credential, insideLock: &mutableState) + case let .failure(error): + self.handleRefreshFailure(error, insideLock: &mutableState) + } + } + } + } + } + + private func isRefreshExcessive(insideLock mutableState: inout MutableState) -> Bool { + guard let refreshWindow = mutableState.refreshWindow else { return false } + + let refreshWindowMin = ProcessInfo.processInfo.systemUptime - refreshWindow.interval + + let refreshAttemptsWithinWindow = mutableState.refreshTimestamps.reduce(into: 0) { attempts, refreshTimestamp in + guard refreshWindowMin <= refreshTimestamp else { return } + attempts += 1 + } + + let isRefreshExcessive = refreshAttemptsWithinWindow >= refreshWindow.maximumAttempts + + return isRefreshExcessive + } + + private func handleRefreshSuccess(_ credential: Credential, insideLock mutableState: inout MutableState) { + mutableState.credential = credential + + let adaptOperations = mutableState.adaptOperations + let requestsToRetry = mutableState.requestsToRetry + + mutableState.adaptOperations.removeAll() + mutableState.requestsToRetry.removeAll() + + mutableState.isRefreshing = false + + // Dispatch to queue to hop out of the mutable state lock + queue.async { + adaptOperations.forEach { self.adapt($0.urlRequest, for: $0.session, completion: $0.completion) } + requestsToRetry.forEach { $0(.retry) } + } + } + + private func handleRefreshFailure(_ error: Error, insideLock mutableState: inout MutableState) { + let adaptOperations = mutableState.adaptOperations + let requestsToRetry = mutableState.requestsToRetry + + mutableState.adaptOperations.removeAll() + mutableState.requestsToRetry.removeAll() + + mutableState.isRefreshing = false + + // Dispatch to queue to hop out of the mutable state lock + queue.async { + adaptOperations.forEach { $0.completion(.failure(error)) } + requestsToRetry.forEach { $0(.doNotRetryWithError(error)) } + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/CachedResponseHandler.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/CachedResponseHandler.swift new file mode 100644 index 0000000..e7d0060 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/CachedResponseHandler.swift @@ -0,0 +1,109 @@ +// +// CachedResponseHandler.swift +// +// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A type that handles whether the data task should store the HTTP response in the cache. +public protocol CachedResponseHandler { + /// Determines whether the HTTP response should be stored in the cache. + /// + /// The `completion` closure should be passed one of three possible options: + /// + /// 1. The cached response provided by the server (this is the most common use case). + /// 2. A modified version of the cached response (you may want to modify it in some way before caching). + /// 3. A `nil` value to prevent the cached response from being stored in the cache. + /// + /// - Parameters: + /// - task: The data task whose request resulted in the cached response. + /// - response: The cached response to potentially store in the cache. + /// - completion: The closure to execute containing cached response, a modified response, or `nil`. + func dataTask(_ task: URLSessionDataTask, + willCacheResponse response: CachedURLResponse, + completion: @escaping (CachedURLResponse?) -> Void) +} + +// MARK: - + +/// `ResponseCacher` is a convenience `CachedResponseHandler` making it easy to cache, not cache, or modify a cached +/// response. +public struct ResponseCacher { + /// Defines the behavior of the `ResponseCacher` type. + public enum Behavior { + /// Stores the cached response in the cache. + case cache + /// Prevents the cached response from being stored in the cache. + case doNotCache + /// Modifies the cached response before storing it in the cache. + case modify((URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?) + } + + /// Returns a `ResponseCacher` with a `.cache` `Behavior`. + public static let cache = ResponseCacher(behavior: .cache) + /// Returns a `ResponseCacher` with a `.doNotCache` `Behavior`. + public static let doNotCache = ResponseCacher(behavior: .doNotCache) + + /// The `Behavior` of the `ResponseCacher`. + public let behavior: Behavior + + /// Creates a `ResponseCacher` instance from the `Behavior`. + /// + /// - Parameter behavior: The `Behavior`. + public init(behavior: Behavior) { + self.behavior = behavior + } +} + +extension ResponseCacher: CachedResponseHandler { + public func dataTask(_ task: URLSessionDataTask, + willCacheResponse response: CachedURLResponse, + completion: @escaping (CachedURLResponse?) -> Void) { + switch behavior { + case .cache: + completion(response) + case .doNotCache: + completion(nil) + case let .modify(closure): + let response = closure(task, response) + completion(response) + } + } +} + +#if swift(>=5.5) +extension CachedResponseHandler where Self == ResponseCacher { + /// Provides a `ResponseCacher` which caches the response, if allowed. Equivalent to `ResponseCacher.cache`. + public static var cache: ResponseCacher { .cache } + + /// Provides a `ResponseCacher` which does not cache the response. Equivalent to `ResponseCacher.doNotCache`. + public static var doNotCache: ResponseCacher { .doNotCache } + + /// Creates a `ResponseCacher` which modifies the proposed `CachedURLResponse` using the provided closure. + /// + /// - Parameter closure: Closure used to modify the `CachedURLResponse`. + /// - Returns: The `ResponseCacher`. + public static func modify(using closure: @escaping ((URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)) -> ResponseCacher { + ResponseCacher(behavior: .modify(closure)) + } +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/Combine.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Combine.swift new file mode 100644 index 0000000..066ba47 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Combine.swift @@ -0,0 +1,655 @@ +// +// Combine.swift +// +// Copyright (c) 2020 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if !((os(iOS) && (arch(i386) || arch(arm))) || os(Windows) || os(Linux)) + +import Combine +import Dispatch +import Foundation + +// MARK: - DataRequest / UploadRequest + +/// A Combine `Publisher` that publishes the `DataResponse` of the provided `DataRequest`. +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +public struct DataResponsePublisher: Publisher { + public typealias Output = DataResponse + public typealias Failure = Never + + private typealias Handler = (@escaping (_ response: DataResponse) -> Void) -> DataRequest + + private let request: DataRequest + private let responseHandler: Handler + + /// Creates an instance which will serialize responses using the provided `ResponseSerializer`. + /// + /// - Parameters: + /// - request: `DataRequest` for which to publish the response. + /// - queue: `DispatchQueue` on which the `DataResponse` value will be published. `.main` by default. + /// - serializer: `ResponseSerializer` used to produce the published `DataResponse`. + public init(_ request: DataRequest, queue: DispatchQueue, serializer: Serializer) + where Value == Serializer.SerializedObject { + self.request = request + responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) } + } + + /// Creates an instance which will serialize responses using the provided `DataResponseSerializerProtocol`. + /// + /// - Parameters: + /// - request: `DataRequest` for which to publish the response. + /// - queue: `DispatchQueue` on which the `DataResponse` value will be published. `.main` by default. + /// - serializer: `DataResponseSerializerProtocol` used to produce the published `DataResponse`. + public init(_ request: DataRequest, + queue: DispatchQueue, + serializer: Serializer) + where Value == Serializer.SerializedObject { + self.request = request + responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) } + } + + /// Publishes only the `Result` of the `DataResponse` value. + /// + /// - Returns: The `AnyPublisher` publishing the `Result` value. + public func result() -> AnyPublisher, Never> { + map(\.result).eraseToAnyPublisher() + } + + /// Publishes the `Result` of the `DataResponse` as a single `Value` or fail with the `AFError` instance. + /// + /// - Returns: The `AnyPublisher` publishing the stream. + public func value() -> AnyPublisher { + setFailureType(to: AFError.self).flatMap(\.result.publisher).eraseToAnyPublisher() + } + + public func receive(subscriber: S) where S: Subscriber, DataResponsePublisher.Failure == S.Failure, DataResponsePublisher.Output == S.Input { + subscriber.receive(subscription: Inner(request: request, + responseHandler: responseHandler, + downstream: subscriber)) + } + + private final class Inner: Subscription, Cancellable + where Downstream.Input == Output { + typealias Failure = Downstream.Failure + + @Protected + private var downstream: Downstream? + private let request: DataRequest + private let responseHandler: Handler + + init(request: DataRequest, responseHandler: @escaping Handler, downstream: Downstream) { + self.request = request + self.responseHandler = responseHandler + self.downstream = downstream + } + + func request(_ demand: Subscribers.Demand) { + assert(demand > 0) + + guard let downstream = downstream else { return } + + self.downstream = nil + responseHandler { response in + _ = downstream.receive(response) + downstream.receive(completion: .finished) + }.resume() + } + + func cancel() { + request.cancel() + downstream = nil + } + } +} + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +extension DataResponsePublisher where Value == Data? { + /// Creates an instance which publishes a `DataResponse` value without serialization. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public init(_ request: DataRequest, queue: DispatchQueue) { + self.request = request + responseHandler = { request.response(queue: queue, completionHandler: $0) } + } +} + +extension DataRequest { + /// Creates a `DataResponsePublisher` for this instance using the given `ResponseSerializer` and `DispatchQueue`. + /// + /// - Parameters: + /// - serializer: `ResponseSerializer` used to serialize response `Data`. + /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. + /// + /// - Returns: The `DataResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishResponse(using serializer: Serializer, on queue: DispatchQueue = .main) -> DataResponsePublisher + where Serializer.SerializedObject == T { + DataResponsePublisher(self, queue: queue, serializer: serializer) + } + + /// Creates a `DataResponsePublisher` for this instance and uses a `DataResponseSerializer` to serialize the + /// response. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. + /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()` + /// by default. + /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by + /// default. + /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless of + /// status code. `[.head]` by default. + /// - Returns: The `DataResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishData(queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) -> DataResponsePublisher { + publishResponse(using: DataResponseSerializer(dataPreprocessor: preprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + on: queue) + } + + /// Creates a `DataResponsePublisher` for this instance and uses a `StringResponseSerializer` to serialize the + /// response. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. + /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()` + /// by default. + /// - encoding: `String.Encoding` to parse the response. `nil` by default, in which case the encoding + /// will be determined by the server response, falling back to the default HTTP character + /// set, `ISO-8859-1`. + /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by + /// default. + /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless of + /// status code. `[.head]` by default. + /// + /// - Returns: The `DataResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishString(queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) -> DataResponsePublisher { + publishResponse(using: StringResponseSerializer(dataPreprocessor: preprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + on: queue) + } + + @_disfavoredOverload + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + @available(*, deprecated, message: "Renamed publishDecodable(type:queue:preprocessor:decoder:emptyResponseCodes:emptyRequestMethods).") + public func publishDecodable(type: T.Type = T.self, + queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DataResponsePublisher { + publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyResponseMethods), + on: queue) + } + + /// Creates a `DataResponsePublisher` for this instance and uses a `DecodableResponseSerializer` to serialize the + /// response. + /// + /// - Parameters: + /// - type: `Decodable` type to which to decode response `Data`. Inferred from the context by + /// default. + /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. + /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. + /// `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` instance used to decode response `Data`. `JSONDecoder()` by default. + /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by + /// default. + /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless of + /// status code. `[.head]` by default. + /// + /// - Returns: The `DataResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishDecodable(type: T.Type = T.self, + queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DataResponsePublisher { + publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + on: queue) + } + + /// Creates a `DataResponsePublisher` for this instance which does not serialize the response before publishing. + /// + /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. + /// + /// - Returns: The `DataResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishUnserialized(queue: DispatchQueue = .main) -> DataResponsePublisher { + DataResponsePublisher(self, queue: queue) + } +} + +// A Combine `Publisher` that publishes a sequence of `Stream` values received by the provided `DataStreamRequest`. +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +public struct DataStreamPublisher: Publisher { + public typealias Output = DataStreamRequest.Stream + public typealias Failure = Never + + private typealias Handler = (@escaping DataStreamRequest.Handler) -> DataStreamRequest + + private let request: DataStreamRequest + private let streamHandler: Handler + + /// Creates an instance which will serialize responses using the provided `DataStreamSerializer`. + /// + /// - Parameters: + /// - request: `DataStreamRequest` for which to publish the response. + /// - queue: `DispatchQueue` on which the `Stream` values will be published. `.main` by + /// default. + /// - serializer: `DataStreamSerializer` used to produce the published `Stream` values. + public init(_ request: DataStreamRequest, queue: DispatchQueue, serializer: Serializer) + where Value == Serializer.SerializedObject { + self.request = request + streamHandler = { request.responseStream(using: serializer, on: queue, stream: $0) } + } + + /// Publishes only the `Result` of the `DataStreamRequest.Stream`'s `Event`s. + /// + /// - Returns: The `AnyPublisher` publishing the `Result` value. + public func result() -> AnyPublisher, Never> { + compactMap { stream in + switch stream.event { + case let .stream(result): + return result + // If the stream has completed with an error, send the error value downstream as a `.failure`. + case let .complete(completion): + return completion.error.map(Result.failure) + } + } + .eraseToAnyPublisher() + } + + /// Publishes the streamed values of the `DataStreamRequest.Stream` as a sequence of `Value` or fail with the + /// `AFError` instance. + /// + /// - Returns: The `AnyPublisher` publishing the stream. + public func value() -> AnyPublisher { + result().setFailureType(to: AFError.self).flatMap(\.publisher).eraseToAnyPublisher() + } + + public func receive(subscriber: S) where S: Subscriber, DataStreamPublisher.Failure == S.Failure, DataStreamPublisher.Output == S.Input { + subscriber.receive(subscription: Inner(request: request, + streamHandler: streamHandler, + downstream: subscriber)) + } + + private final class Inner: Subscription, Cancellable + where Downstream.Input == Output { + typealias Failure = Downstream.Failure + + @Protected + private var downstream: Downstream? + private let request: DataStreamRequest + private let streamHandler: Handler + + init(request: DataStreamRequest, streamHandler: @escaping Handler, downstream: Downstream) { + self.request = request + self.streamHandler = streamHandler + self.downstream = downstream + } + + func request(_ demand: Subscribers.Demand) { + assert(demand > 0) + + guard let downstream = downstream else { return } + + self.downstream = nil + streamHandler { stream in + _ = downstream.receive(stream) + if case .complete = stream.event { + downstream.receive(completion: .finished) + } + }.resume() + } + + func cancel() { + request.cancel() + downstream = nil + } + } +} + +extension DataStreamRequest { + /// Creates a `DataStreamPublisher` for this instance using the given `DataStreamSerializer` and `DispatchQueue`. + /// + /// - Parameters: + /// - serializer: `DataStreamSerializer` used to serialize the streamed `Data`. + /// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default. + /// - Returns: The `DataStreamPublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishStream(using serializer: Serializer, + on queue: DispatchQueue = .main) -> DataStreamPublisher { + DataStreamPublisher(self, queue: queue, serializer: serializer) + } + + /// Creates a `DataStreamPublisher` for this instance which uses a `PassthroughStreamSerializer` to stream `Data` + /// unserialized. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default. + /// - Returns: The `DataStreamPublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishData(queue: DispatchQueue = .main) -> DataStreamPublisher { + publishStream(using: PassthroughStreamSerializer(), on: queue) + } + + /// Creates a `DataStreamPublisher` for this instance which uses a `StringStreamSerializer` to serialize stream + /// `Data` values into `String` values. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default. + /// - Returns: The `DataStreamPublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishString(queue: DispatchQueue = .main) -> DataStreamPublisher { + publishStream(using: StringStreamSerializer(), on: queue) + } + + /// Creates a `DataStreamPublisher` for this instance which uses a `DecodableStreamSerializer` with the provided + /// parameters to serialize stream `Data` values into the provided type. + /// + /// - Parameters: + /// - type: `Decodable` type to which to decode stream `Data`. Inferred from the context by default. + /// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default. + /// - decoder: `DataDecoder` instance used to decode stream `Data`. `JSONDecoder()` by default. + /// - preprocessor: `DataPreprocessor` which filters incoming stream `Data` before serialization. + /// `PassthroughPreprocessor()` by default. + /// - Returns: The `DataStreamPublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishDecodable(type: T.Type = T.self, + queue: DispatchQueue = .main, + decoder: DataDecoder = JSONDecoder(), + preprocessor: DataPreprocessor = PassthroughPreprocessor()) -> DataStreamPublisher { + publishStream(using: DecodableStreamSerializer(decoder: decoder, + dataPreprocessor: preprocessor), + on: queue) + } +} + +/// A Combine `Publisher` that publishes the `DownloadResponse` of the provided `DownloadRequest`. +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +public struct DownloadResponsePublisher: Publisher { + public typealias Output = DownloadResponse + public typealias Failure = Never + + private typealias Handler = (@escaping (_ response: DownloadResponse) -> Void) -> DownloadRequest + + private let request: DownloadRequest + private let responseHandler: Handler + + /// Creates an instance which will serialize responses using the provided `ResponseSerializer`. + /// + /// - Parameters: + /// - request: `DownloadRequest` for which to publish the response. + /// - queue: `DispatchQueue` on which the `DownloadResponse` value will be published. `.main` by default. + /// - serializer: `ResponseSerializer` used to produce the published `DownloadResponse`. + public init(_ request: DownloadRequest, queue: DispatchQueue, serializer: Serializer) + where Value == Serializer.SerializedObject { + self.request = request + responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) } + } + + /// Creates an instance which will serialize responses using the provided `DownloadResponseSerializerProtocol` value. + /// + /// - Parameters: + /// - request: `DownloadRequest` for which to publish the response. + /// - queue: `DispatchQueue` on which the `DataResponse` value will be published. `.main` by default. + /// - serializer: `DownloadResponseSerializerProtocol` used to produce the published `DownloadResponse`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public init(_ request: DownloadRequest, + queue: DispatchQueue, + serializer: Serializer) + where Value == Serializer.SerializedObject { + self.request = request + responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) } + } + + /// Publishes only the `Result` of the `DownloadResponse` value. + /// + /// - Returns: The `AnyPublisher` publishing the `Result` value. + public func result() -> AnyPublisher, Never> { + map(\.result).eraseToAnyPublisher() + } + + /// Publishes the `Result` of the `DownloadResponse` as a single `Value` or fail with the `AFError` instance. + /// + /// - Returns: The `AnyPublisher` publishing the stream. + public func value() -> AnyPublisher { + setFailureType(to: AFError.self).flatMap(\.result.publisher).eraseToAnyPublisher() + } + + public func receive(subscriber: S) where S: Subscriber, DownloadResponsePublisher.Failure == S.Failure, DownloadResponsePublisher.Output == S.Input { + subscriber.receive(subscription: Inner(request: request, + responseHandler: responseHandler, + downstream: subscriber)) + } + + private final class Inner: Subscription, Cancellable + where Downstream.Input == Output { + typealias Failure = Downstream.Failure + + @Protected + private var downstream: Downstream? + private let request: DownloadRequest + private let responseHandler: Handler + + init(request: DownloadRequest, responseHandler: @escaping Handler, downstream: Downstream) { + self.request = request + self.responseHandler = responseHandler + self.downstream = downstream + } + + func request(_ demand: Subscribers.Demand) { + assert(demand > 0) + + guard let downstream = downstream else { return } + + self.downstream = nil + responseHandler { response in + _ = downstream.receive(response) + downstream.receive(completion: .finished) + }.resume() + } + + func cancel() { + request.cancel() + downstream = nil + } + } +} + +extension DownloadRequest { + /// Creates a `DownloadResponsePublisher` for this instance using the given `ResponseSerializer` and `DispatchQueue`. + /// + /// - Parameters: + /// - serializer: `ResponseSerializer` used to serialize the response `Data` from disk. + /// - queue: `DispatchQueue` on which the `DownloadResponse` will be published.`.main` by default. + /// + /// - Returns: The `DownloadResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishResponse(using serializer: Serializer, on queue: DispatchQueue = .main) -> DownloadResponsePublisher + where Serializer.SerializedObject == T { + DownloadResponsePublisher(self, queue: queue, serializer: serializer) + } + + /// Creates a `DownloadResponsePublisher` for this instance using the given `DownloadResponseSerializerProtocol` and + /// `DispatchQueue`. + /// + /// - Parameters: + /// - serializer: `DownloadResponseSerializer` used to serialize the response `Data` from disk. + /// - queue: `DispatchQueue` on which the `DownloadResponse` will be published.`.main` by default. + /// + /// - Returns: The `DownloadResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishResponse(using serializer: Serializer, on queue: DispatchQueue = .main) -> DownloadResponsePublisher + where Serializer.SerializedObject == T { + DownloadResponsePublisher(self, queue: queue, serializer: serializer) + } + + /// Creates a `DownloadResponsePublisher` for this instance and uses a `URLResponseSerializer` to serialize the + /// response. + /// + /// - Parameter queue: `DispatchQueue` on which the `DownloadResponse` will be published. `.main` by default. + /// + /// - Returns: The `DownloadResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishURL(queue: DispatchQueue = .main) -> DownloadResponsePublisher { + publishResponse(using: URLResponseSerializer(), on: queue) + } + + /// Creates a `DownloadResponsePublisher` for this instance and uses a `DataResponseSerializer` to serialize the + /// response. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the `DownloadResponse` will be published. `.main` by default. + /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()` + /// by default. + /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by + /// default. + /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless of + /// status code. `[.head]` by default. + /// + /// - Returns: The `DownloadResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishData(queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) -> DownloadResponsePublisher { + publishResponse(using: DataResponseSerializer(dataPreprocessor: preprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + on: queue) + } + + /// Creates a `DownloadResponsePublisher` for this instance and uses a `StringResponseSerializer` to serialize the + /// response. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. + /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()` + /// by default. + /// - encoding: `String.Encoding` to parse the response. `nil` by default, in which case the encoding + /// will be determined by the server response, falling back to the default HTTP character + /// set, `ISO-8859-1`. + /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by + /// default. + /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless of + /// status code. `[.head]` by default. + /// + /// - Returns: The `DownloadResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishString(queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) -> DownloadResponsePublisher { + publishResponse(using: StringResponseSerializer(dataPreprocessor: preprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + on: queue) + } + + @_disfavoredOverload + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + @available(*, deprecated, message: "Renamed publishDecodable(type:queue:preprocessor:decoder:emptyResponseCodes:emptyRequestMethods).") + public func publishDecodable(type: T.Type = T.self, + queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyResponseMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DownloadResponsePublisher { + publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyResponseMethods), + on: queue) + } + + /// Creates a `DownloadResponsePublisher` for this instance and uses a `DecodableResponseSerializer` to serialize + /// the response. + /// + /// - Parameters: + /// - type: `Decodable` type to which to decode response `Data`. Inferred from the context by default. + /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. + /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. + /// `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` instance used to decode response `Data`. `JSONDecoder()` by default. + /// - emptyResponseCodes: `Set` of HTTP status codes for which empty responses are allowed. `[204, 205]` by + /// default. + /// - emptyRequestMethods: `Set` of `HTTPMethod`s for which empty responses are allowed, regardless + /// of status code. `[.head]` by default. + /// + /// - Returns: The `DownloadResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishDecodable(type: T.Type = T.self, + queue: DispatchQueue = .main, + preprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DownloadResponsePublisher { + publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + on: queue) + } +} + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +extension DownloadResponsePublisher where Value == URL? { + /// Creates an instance which publishes a `DownloadResponse` value without serialization. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public init(_ request: DownloadRequest, queue: DispatchQueue) { + self.request = request + responseHandler = { request.response(queue: queue, completionHandler: $0) } + } +} + +extension DownloadRequest { + /// Creates a `DownloadResponsePublisher` for this instance which does not serialize the response before publishing. + /// + /// - Parameter queue: `DispatchQueue` on which the `DownloadResponse` will be published. `.main` by default. + /// + /// - Returns: The `DownloadResponsePublisher`. + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public func publishUnserialized(on queue: DispatchQueue = .main) -> DownloadResponsePublisher { + DownloadResponsePublisher(self, queue: queue) + } +} + +#endif diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/Concurrency.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Concurrency.swift new file mode 100644 index 0000000..a5621f3 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Concurrency.swift @@ -0,0 +1,704 @@ +// +// Concurrency.swift +// +// Copyright (c) 2021 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if compiler(>=5.6.0) && canImport(_Concurrency) + +import Foundation + +// MARK: - Request Event Streams + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Request { + /// Creates a `StreamOf` for the instance's upload progress. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func uploadProgress(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + uploadProgress(queue: .singleEventQueue) { progress in + continuation.yield(progress) + } + } + } + + /// Creates a `StreamOf` for the instance's download progress. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func downloadProgress(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + downloadProgress(queue: .singleEventQueue) { progress in + continuation.yield(progress) + } + } + } + + /// Creates a `StreamOf` for the `URLRequest`s produced for the instance. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func urlRequests(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + onURLRequestCreation(on: .singleEventQueue) { request in + continuation.yield(request) + } + } + } + + /// Creates a `StreamOf` for the `URLSessionTask`s produced for the instance. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func urlSessionTasks(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + onURLSessionTaskCreation(on: .singleEventQueue) { task in + continuation.yield(task) + } + } + } + + /// Creates a `StreamOf` for the cURL descriptions produced for the instance. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func cURLDescriptions(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + cURLDescription(on: .singleEventQueue) { description in + continuation.yield(description) + } + } + } + + private func stream(of type: T.Type = T.self, + bufferingPolicy: StreamOf.BufferingPolicy = .unbounded, + yielder: @escaping (StreamOf.Continuation) -> Void) -> StreamOf { + StreamOf(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + yielder(continuation) + // Must come after serializers run in order to catch retry progress. + onFinish { + continuation.finish() + } + } + } +} + +// MARK: - DataTask + +/// Value used to `await` a `DataResponse` and associated values. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct DataTask { + /// `DataResponse` produced by the `DataRequest` and its response handler. + public var response: DataResponse { + get async { + if shouldAutomaticallyCancel { + return await withTaskCancellationHandler { + self.cancel() + } operation: { + await task.value + } + } else { + return await task.value + } + } + } + + /// `Result` of any response serialization performed for the `response`. + public var result: Result { + get async { await response.result } + } + + /// `Value` returned by the `response`. + public var value: Value { + get async throws { + try await result.get() + } + } + + private let request: DataRequest + private let task: Task, Never> + private let shouldAutomaticallyCancel: Bool + + fileprivate init(request: DataRequest, task: Task, Never>, shouldAutomaticallyCancel: Bool) { + self.request = request + self.task = task + self.shouldAutomaticallyCancel = shouldAutomaticallyCancel + } + + /// Cancel the underlying `DataRequest` and `Task`. + public func cancel() { + task.cancel() + } + + /// Resume the underlying `DataRequest`. + public func resume() { + request.resume() + } + + /// Suspend the underlying `DataRequest`. + public func suspend() { + request.suspend() + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension DataRequest { + /// Creates a `DataTask` to `await` a `Data` value. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before completion. + /// - emptyResponseCodes: HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DataTask`. + public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) -> DataTask { + serializingResponse(using: DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DataTask` to `await` serialization of a `Decodable` value. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer. + /// `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DataTask`. + public func serializingDecodable(_ type: Value.Type = Value.self, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DataTask { + serializingResponse(using: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DataTask` to `await` serialization of a `String` value. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer. + /// `PassthroughPreprocessor()` by default. + /// - encoding: `String.Encoding` to use during serialization. Defaults to `nil`, in which case + /// the encoding will be determined from the server response, falling back to the + /// default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DataTask`. + public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) -> DataTask { + serializingResponse(using: StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DataTask` to `await` serialization using the provided `ResponseSerializer` instance. + /// + /// - Parameters: + /// - serializer: `ResponseSerializer` responsible for serializing the request, response, and data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// + /// - Returns: The `DataTask`. + public func serializingResponse(using serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false) + -> DataTask { + dataTask(automaticallyCancelling: shouldAutomaticallyCancel) { + self.response(queue: .singleEventQueue, + responseSerializer: serializer, + completionHandler: $0) + } + } + + /// Creates a `DataTask` to `await` serialization using the provided `DataResponseSerializerProtocol` instance. + /// + /// - Parameters: + /// - serializer: `DataResponseSerializerProtocol` responsible for serializing the request, + /// response, and data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// + /// - Returns: The `DataTask`. + public func serializingResponse(using serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false) + -> DataTask { + dataTask(automaticallyCancelling: shouldAutomaticallyCancel) { + self.response(queue: .singleEventQueue, + responseSerializer: serializer, + completionHandler: $0) + } + } + + private func dataTask(automaticallyCancelling shouldAutomaticallyCancel: Bool, + forResponse onResponse: @escaping (@escaping (DataResponse) -> Void) -> Void) + -> DataTask { + let task = Task { + await withTaskCancellationHandler { + self.cancel() + } operation: { + await withCheckedContinuation { continuation in + onResponse { + continuation.resume(returning: $0) + } + } + } + } + + return DataTask(request: self, task: task, shouldAutomaticallyCancel: shouldAutomaticallyCancel) + } +} + +// MARK: - DownloadTask + +/// Value used to `await` a `DownloadResponse` and associated values. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct DownloadTask { + /// `DownloadResponse` produced by the `DownloadRequest` and its response handler. + public var response: DownloadResponse { + get async { + if shouldAutomaticallyCancel { + return await withTaskCancellationHandler { + self.cancel() + } operation: { + await task.value + } + } else { + return await task.value + } + } + } + + /// `Result` of any response serialization performed for the `response`. + public var result: Result { + get async { await response.result } + } + + /// `Value` returned by the `response`. + public var value: Value { + get async throws { + try await result.get() + } + } + + private let task: Task, Never> + private let request: DownloadRequest + private let shouldAutomaticallyCancel: Bool + + fileprivate init(request: DownloadRequest, task: Task, Never>, shouldAutomaticallyCancel: Bool) { + self.request = request + self.task = task + self.shouldAutomaticallyCancel = shouldAutomaticallyCancel + } + + /// Cancel the underlying `DownloadRequest` and `Task`. + public func cancel() { + task.cancel() + } + + /// Resume the underlying `DownloadRequest`. + public func resume() { + request.resume() + } + + /// Suspend the underlying `DownloadRequest`. + public func suspend() { + request.suspend() + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension DownloadRequest { + /// Creates a `DownloadTask` to `await` a `Data` value. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before completion. + /// - emptyResponseCodes: HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask { + serializingDownload(using: DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DownloadTask` to `await` serialization of a `Decodable` value. + /// + /// - Note: This serializer reads the entire response into memory before parsing. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer. + /// `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingDecodable(_ type: Value.Type = Value.self, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask { + serializingDownload(using: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DownloadTask` to `await` serialization of the downloaded file's `URL` on disk. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingDownloadedFileURL(automaticallyCancelling shouldAutomaticallyCancel: Bool = false) -> DownloadTask { + serializingDownload(using: URLResponseSerializer(), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DownloadTask` to `await` serialization of a `String` value. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// serializer. `PassthroughPreprocessor()` by default. + /// - encoding: `String.Encoding` to use during serialization. Defaults to `nil`, in which case + /// the encoding will be determined from the server response, falling back to the + /// default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask { + serializingDownload(using: StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DownloadTask` to `await` serialization using the provided `ResponseSerializer` instance. + /// + /// - Parameters: + /// - serializer: `ResponseSerializer` responsible for serializing the request, response, and data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingDownload(using serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false) + -> DownloadTask { + downloadTask(automaticallyCancelling: shouldAutomaticallyCancel) { + self.response(queue: .singleEventQueue, + responseSerializer: serializer, + completionHandler: $0) + } + } + + /// Creates a `DownloadTask` to `await` serialization using the provided `DownloadResponseSerializerProtocol` + /// instance. + /// + /// - Parameters: + /// - serializer: `DownloadResponseSerializerProtocol` responsible for serializing the request, + /// response, and data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingDownload(using serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false) + -> DownloadTask { + downloadTask(automaticallyCancelling: shouldAutomaticallyCancel) { + self.response(queue: .singleEventQueue, + responseSerializer: serializer, + completionHandler: $0) + } + } + + private func downloadTask(automaticallyCancelling shouldAutomaticallyCancel: Bool, + forResponse onResponse: @escaping (@escaping (DownloadResponse) -> Void) -> Void) + -> DownloadTask { + let task = Task { + await withTaskCancellationHandler { + self.cancel() + } operation: { + await withCheckedContinuation { continuation in + onResponse { + continuation.resume(returning: $0) + } + } + } + } + + return DownloadTask(request: self, task: task, shouldAutomaticallyCancel: shouldAutomaticallyCancel) + } +} + +// MARK: - DataStreamTask + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct DataStreamTask { + // Type of created streams. + public typealias Stream = StreamOf> + + private let request: DataStreamRequest + + fileprivate init(request: DataStreamRequest) { + self.request = request + } + + /// Creates a `Stream` of `Data` values from the underlying `DataStreamRequest`. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled + /// which observation of the stream stops. `true` by default. + /// - bufferingPolicy: ` BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `Stream`. + public func streamingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, bufferingPolicy: Stream.BufferingPolicy = .unbounded) -> Stream { + createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in + self.request.responseStream(on: .streamCompletionQueue(forRequestID: request.id), stream: onStream) + } + } + + /// Creates a `Stream` of `UTF-8` `String`s from the underlying `DataStreamRequest`. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled + /// which observation of the stream stops. `true` by default. + /// - bufferingPolicy: ` BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// - Returns: + public func streamingStrings(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, bufferingPolicy: Stream.BufferingPolicy = .unbounded) -> Stream { + createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in + self.request.responseStreamString(on: .streamCompletionQueue(forRequestID: request.id), stream: onStream) + } + } + + /// Creates a `Stream` of `Decodable` values from the underlying `DataStreamRequest`. + /// + /// - Parameters: + /// - type: `Decodable` type to be serialized from stream payloads. + /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled + /// which observation of the stream stops. `true` by default. + /// - bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `Stream`. + public func streamingDecodables(_ type: T.Type = T.self, + automaticallyCancelling shouldAutomaticallyCancel: Bool = true, + bufferingPolicy: Stream.BufferingPolicy = .unbounded) + -> Stream where T: Decodable { + streamingResponses(serializedUsing: DecodableStreamSerializer(), + automaticallyCancelling: shouldAutomaticallyCancel, + bufferingPolicy: bufferingPolicy) + } + + /// Creates a `Stream` of values using the provided `DataStreamSerializer` from the underlying `DataStreamRequest`. + /// + /// - Parameters: + /// - serializer: `DataStreamSerializer` to use to serialize incoming `Data`. + /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled + /// which observation of the stream stops. `true` by default. + /// - bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `Stream`. + public func streamingResponses(serializedUsing serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = true, + bufferingPolicy: Stream.BufferingPolicy = .unbounded) + -> Stream { + createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in + self.request.responseStream(using: serializer, + on: .streamCompletionQueue(forRequestID: request.id), + stream: onStream) + } + } + + private func createStream(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, + bufferingPolicy: Stream.BufferingPolicy = .unbounded, + forResponse onResponse: @escaping (@escaping (DataStreamRequest.Stream) -> Void) -> Void) + -> Stream { + StreamOf(bufferingPolicy: bufferingPolicy) { + guard shouldAutomaticallyCancel, + request.isInitialized || request.isResumed || request.isSuspended else { return } + + cancel() + } builder: { continuation in + onResponse { stream in + continuation.yield(stream) + if case .complete = stream.event { + continuation.finish() + } + } + } + } + + /// Cancel the underlying `DataStreamRequest`. + public func cancel() { + request.cancel() + } + + /// Resume the underlying `DataStreamRequest`. + public func resume() { + request.resume() + } + + /// Suspend the underlying `DataStreamRequest`. + public func suspend() { + request.suspend() + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension DataStreamRequest { + /// Creates a `DataStreamTask` used to `await` streams of serialized values. + /// + /// - Returns: The `DataStreamTask`. + public func streamTask() -> DataStreamTask { + DataStreamTask(request: self) + } +} + +extension DispatchQueue { + fileprivate static let singleEventQueue = DispatchQueue(label: "org.alamofire.concurrencySingleEventQueue", + attributes: .concurrent) + + fileprivate static func streamCompletionQueue(forRequestID id: UUID) -> DispatchQueue { + DispatchQueue(label: "org.alamofire.concurrencyStreamCompletionQueue-\(id)", target: .singleEventQueue) + } +} + +/// An asynchronous sequence generated from an underlying `AsyncStream`. Only produced by Alamofire. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct StreamOf: AsyncSequence { + public typealias AsyncIterator = Iterator + public typealias BufferingPolicy = AsyncStream.Continuation.BufferingPolicy + fileprivate typealias Continuation = AsyncStream.Continuation + + private let bufferingPolicy: BufferingPolicy + private let onTermination: (() -> Void)? + private let builder: (Continuation) -> Void + + fileprivate init(bufferingPolicy: BufferingPolicy = .unbounded, + onTermination: (() -> Void)? = nil, + builder: @escaping (Continuation) -> Void) { + self.bufferingPolicy = bufferingPolicy + self.onTermination = onTermination + self.builder = builder + } + + public func makeAsyncIterator() -> Iterator { + var continuation: AsyncStream.Continuation? + let stream = AsyncStream { innerContinuation in + continuation = innerContinuation + builder(innerContinuation) + } + + return Iterator(iterator: stream.makeAsyncIterator()) { + continuation?.finish() + self.onTermination?() + } + } + + public struct Iterator: AsyncIteratorProtocol { + private final class Token { + private let onDeinit: () -> Void + + init(onDeinit: @escaping () -> Void) { + self.onDeinit = onDeinit + } + + deinit { + onDeinit() + } + } + + private var iterator: AsyncStream.AsyncIterator + private let token: Token + + init(iterator: AsyncStream.AsyncIterator, onCancellation: @escaping () -> Void) { + self.iterator = iterator + token = Token(onDeinit: onCancellation) + } + + public mutating func next() async -> Element? { + await iterator.next() + } + } +} + +#endif diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/DispatchQueue+Alamofire.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/DispatchQueue+Alamofire.swift new file mode 100644 index 0000000..10cd273 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/DispatchQueue+Alamofire.swift @@ -0,0 +1,37 @@ +// +// DispatchQueue+Alamofire.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Dispatch +import Foundation + +extension DispatchQueue { + /// Execute the provided closure after a `TimeInterval`. + /// + /// - Parameters: + /// - delay: `TimeInterval` to delay execution. + /// - closure: Closure to execute. + func after(_ delay: TimeInterval, execute closure: @escaping () -> Void) { + asyncAfter(deadline: .now() + delay, execute: closure) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/EventMonitor.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/EventMonitor.swift new file mode 100644 index 0000000..3b09671 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/EventMonitor.swift @@ -0,0 +1,892 @@ +// +// EventMonitor.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Protocol outlining the lifetime events inside Alamofire. It includes both events received from the various +/// `URLSession` delegate protocols as well as various events from the lifetime of `Request` and its subclasses. +public protocol EventMonitor { + /// The `DispatchQueue` onto which Alamofire's root `CompositeEventMonitor` will dispatch events. `.main` by default. + var queue: DispatchQueue { get } + + // MARK: - URLSession Events + + // MARK: URLSessionDelegate Events + + /// Event called during `URLSessionDelegate`'s `urlSession(_:didBecomeInvalidWithError:)` method. + func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) + + // MARK: URLSessionTaskDelegate Events + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didReceive:completionHandler:)` method. + func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` method. + func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:needNewBodyStream:)` method. + func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` method. + func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didFinishCollecting:)` method. + func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didCompleteWithError:)` method. + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) + + /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:taskIsWaitingForConnectivity:)` method. + @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) + func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) + + // MARK: URLSessionDataDelegate Events + + /// Event called during `URLSessionDataDelegate`'s `urlSession(_:dataTask:didReceive:)` method. + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) + + /// Event called during `URLSessionDataDelegate`'s `urlSession(_:dataTask:willCacheResponse:completionHandler:)` method. + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse) + + // MARK: URLSessionDownloadDelegate Events + + /// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)` method. + func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) + + /// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)` method. + func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) + + /// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didFinishDownloadingTo:)` method. + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) + + // MARK: - Request Events + + /// Event called when a `URLRequest` is first created for a `Request`. If a `RequestAdapter` is active, the + /// `URLRequest` will be adapted before being issued. + func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) + + /// Event called when the attempt to create a `URLRequest` from a `Request`'s original `URLRequestConvertible` value fails. + func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) + + /// Event called when a `RequestAdapter` adapts the `Request`'s initial `URLRequest`. + func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) + + /// Event called when a `RequestAdapter` fails to adapt the `Request`'s initial `URLRequest`. + func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: AFError) + + /// Event called when a final `URLRequest` is created for a `Request`. + func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) + + /// Event called when a `URLSessionTask` subclass instance is created for a `Request`. + func request(_ request: Request, didCreateTask task: URLSessionTask) + + /// Event called when a `Request` receives a `URLSessionTaskMetrics` value. + func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) + + /// Event called when a `Request` fails due to an error created by Alamofire. e.g. When certificate pinning fails. + func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) + + /// Event called when a `Request`'s task completes, possibly with an error. A `Request` may receive this event + /// multiple times if it is retried. + func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) + + /// Event called when a `Request` is about to be retried. + func requestIsRetrying(_ request: Request) + + /// Event called when a `Request` finishes and response serializers are being called. + func requestDidFinish(_ request: Request) + + /// Event called when a `Request` receives a `resume` call. + func requestDidResume(_ request: Request) + + /// Event called when a `Request`'s associated `URLSessionTask` is resumed. + func request(_ request: Request, didResumeTask task: URLSessionTask) + + /// Event called when a `Request` receives a `suspend` call. + func requestDidSuspend(_ request: Request) + + /// Event called when a `Request`'s associated `URLSessionTask` is suspended. + func request(_ request: Request, didSuspendTask task: URLSessionTask) + + /// Event called when a `Request` receives a `cancel` call. + func requestDidCancel(_ request: Request) + + /// Event called when a `Request`'s associated `URLSessionTask` is cancelled. + func request(_ request: Request, didCancelTask task: URLSessionTask) + + // MARK: DataRequest Events + + /// Event called when a `DataRequest` calls a `Validation`. + func request(_ request: DataRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + data: Data?, + withResult result: Request.ValidationResult) + + /// Event called when a `DataRequest` creates a `DataResponse` value without calling a `ResponseSerializer`. + func request(_ request: DataRequest, didParseResponse response: DataResponse) + + /// Event called when a `DataRequest` calls a `ResponseSerializer` and creates a generic `DataResponse`. + func request(_ request: DataRequest, didParseResponse response: DataResponse) + + // MARK: DataStreamRequest Events + + /// Event called when a `DataStreamRequest` calls a `Validation` closure. + /// + /// - Parameters: + /// - request: `DataStreamRequest` which is calling the `Validation`. + /// - urlRequest: `URLRequest` of the request being validated. + /// - response: `HTTPURLResponse` of the request being validated. + /// - result: Produced `ValidationResult`. + func request(_ request: DataStreamRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + withResult result: Request.ValidationResult) + + /// Event called when a `DataStreamSerializer` produces a value from streamed `Data`. + /// + /// - Parameters: + /// - request: `DataStreamRequest` for which the value was serialized. + /// - result: `Result` of the serialization attempt. + func request(_ request: DataStreamRequest, didParseStream result: Result) + + // MARK: UploadRequest Events + + /// Event called when an `UploadRequest` creates its `Uploadable` value, indicating the type of upload it represents. + func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) + + /// Event called when an `UploadRequest` failed to create its `Uploadable` value due to an error. + func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) + + /// Event called when an `UploadRequest` provides the `InputStream` from its `Uploadable` value. This only occurs if + /// the `InputStream` does not wrap a `Data` value or file `URL`. + func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) + + // MARK: DownloadRequest Events + + /// Event called when a `DownloadRequest`'s `URLSessionDownloadTask` finishes and the temporary file has been moved. + func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result) + + /// Event called when a `DownloadRequest`'s `Destination` closure is called and creates the destination URL the + /// downloaded file will be moved to. + func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) + + /// Event called when a `DownloadRequest` calls a `Validation`. + func request(_ request: DownloadRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + fileURL: URL?, + withResult result: Request.ValidationResult) + + /// Event called when a `DownloadRequest` creates a `DownloadResponse` without calling a `ResponseSerializer`. + func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) + + /// Event called when a `DownloadRequest` calls a `DownloadResponseSerializer` and creates a generic `DownloadResponse` + func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) +} + +extension EventMonitor { + /// The default queue on which `CompositeEventMonitor`s will call the `EventMonitor` methods. `.main` by default. + public var queue: DispatchQueue { .main } + + // MARK: Default Implementations + + public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {} + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge) {} + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) {} + public func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) {} + public func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest) {} + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didFinishCollecting metrics: URLSessionTaskMetrics) {} + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {} + public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {} + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {} + public func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + willCacheResponse proposedResponse: CachedURLResponse) {} + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) {} + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) {} + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didFinishDownloadingTo location: URL) {} + public func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) {} + public func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) {} + public func request(_ request: Request, + didAdaptInitialRequest initialRequest: URLRequest, + to adaptedRequest: URLRequest) {} + public func request(_ request: Request, + didFailToAdaptURLRequest initialRequest: URLRequest, + withError error: AFError) {} + public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) {} + public func request(_ request: Request, didCreateTask task: URLSessionTask) {} + public func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) {} + public func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) {} + public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) {} + public func requestIsRetrying(_ request: Request) {} + public func requestDidFinish(_ request: Request) {} + public func requestDidResume(_ request: Request) {} + public func request(_ request: Request, didResumeTask task: URLSessionTask) {} + public func requestDidSuspend(_ request: Request) {} + public func request(_ request: Request, didSuspendTask task: URLSessionTask) {} + public func requestDidCancel(_ request: Request) {} + public func request(_ request: Request, didCancelTask task: URLSessionTask) {} + public func request(_ request: DataRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + data: Data?, + withResult result: Request.ValidationResult) {} + public func request(_ request: DataRequest, didParseResponse response: DataResponse) {} + public func request(_ request: DataRequest, didParseResponse response: DataResponse) {} + public func request(_ request: DataStreamRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + withResult result: Request.ValidationResult) {} + public func request(_ request: DataStreamRequest, didParseStream result: Result) {} + public func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) {} + public func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) {} + public func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) {} + public func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result) {} + public func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) {} + public func request(_ request: DownloadRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + fileURL: URL?, + withResult result: Request.ValidationResult) {} + public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) {} + public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) {} +} + +/// An `EventMonitor` which can contain multiple `EventMonitor`s and calls their methods on their queues. +public final class CompositeEventMonitor: EventMonitor { + public let queue = DispatchQueue(label: "org.alamofire.compositeEventMonitor", qos: .utility) + + let monitors: [EventMonitor] + + init(monitors: [EventMonitor]) { + self.monitors = monitors + } + + func performEvent(_ event: @escaping (EventMonitor) -> Void) { + queue.async { + for monitor in self.monitors { + monitor.queue.async { event(monitor) } + } + } + } + + public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + performEvent { $0.urlSession(session, didBecomeInvalidWithError: error) } + } + + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge) { + performEvent { $0.urlSession(session, task: task, didReceive: challenge) } + } + + public func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) { + performEvent { + $0.urlSession(session, + task: task, + didSendBodyData: bytesSent, + totalBytesSent: totalBytesSent, + totalBytesExpectedToSend: totalBytesExpectedToSend) + } + } + + public func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) { + performEvent { + $0.urlSession(session, taskNeedsNewBodyStream: task) + } + } + + public func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest) { + performEvent { + $0.urlSession(session, + task: task, + willPerformHTTPRedirection: response, + newRequest: request) + } + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + performEvent { $0.urlSession(session, task: task, didFinishCollecting: metrics) } + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + performEvent { $0.urlSession(session, task: task, didCompleteWithError: error) } + } + + @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) + public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { + performEvent { $0.urlSession(session, taskIsWaitingForConnectivity: task) } + } + + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + performEvent { $0.urlSession(session, dataTask: dataTask, didReceive: data) } + } + + public func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + willCacheResponse proposedResponse: CachedURLResponse) { + performEvent { $0.urlSession(session, dataTask: dataTask, willCacheResponse: proposedResponse) } + } + + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) { + performEvent { + $0.urlSession(session, + downloadTask: downloadTask, + didResumeAtOffset: fileOffset, + expectedTotalBytes: expectedTotalBytes) + } + } + + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) { + performEvent { + $0.urlSession(session, + downloadTask: downloadTask, + didWriteData: bytesWritten, + totalBytesWritten: totalBytesWritten, + totalBytesExpectedToWrite: totalBytesExpectedToWrite) + } + } + + public func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didFinishDownloadingTo location: URL) { + performEvent { $0.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location) } + } + + public func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) { + performEvent { $0.request(request, didCreateInitialURLRequest: urlRequest) } + } + + public func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) { + performEvent { $0.request(request, didFailToCreateURLRequestWithError: error) } + } + + public func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) { + performEvent { $0.request(request, didAdaptInitialRequest: initialRequest, to: adaptedRequest) } + } + + public func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: AFError) { + performEvent { $0.request(request, didFailToAdaptURLRequest: initialRequest, withError: error) } + } + + public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) { + performEvent { $0.request(request, didCreateURLRequest: urlRequest) } + } + + public func request(_ request: Request, didCreateTask task: URLSessionTask) { + performEvent { $0.request(request, didCreateTask: task) } + } + + public func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) { + performEvent { $0.request(request, didGatherMetrics: metrics) } + } + + public func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) { + performEvent { $0.request(request, didFailTask: task, earlyWithError: error) } + } + + public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) { + performEvent { $0.request(request, didCompleteTask: task, with: error) } + } + + public func requestIsRetrying(_ request: Request) { + performEvent { $0.requestIsRetrying(request) } + } + + public func requestDidFinish(_ request: Request) { + performEvent { $0.requestDidFinish(request) } + } + + public func requestDidResume(_ request: Request) { + performEvent { $0.requestDidResume(request) } + } + + public func request(_ request: Request, didResumeTask task: URLSessionTask) { + performEvent { $0.request(request, didResumeTask: task) } + } + + public func requestDidSuspend(_ request: Request) { + performEvent { $0.requestDidSuspend(request) } + } + + public func request(_ request: Request, didSuspendTask task: URLSessionTask) { + performEvent { $0.request(request, didSuspendTask: task) } + } + + public func requestDidCancel(_ request: Request) { + performEvent { $0.requestDidCancel(request) } + } + + public func request(_ request: Request, didCancelTask task: URLSessionTask) { + performEvent { $0.request(request, didCancelTask: task) } + } + + public func request(_ request: DataRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + data: Data?, + withResult result: Request.ValidationResult) { + performEvent { $0.request(request, + didValidateRequest: urlRequest, + response: response, + data: data, + withResult: result) + } + } + + public func request(_ request: DataRequest, didParseResponse response: DataResponse) { + performEvent { $0.request(request, didParseResponse: response) } + } + + public func request(_ request: DataRequest, didParseResponse response: DataResponse) { + performEvent { $0.request(request, didParseResponse: response) } + } + + public func request(_ request: DataStreamRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + withResult result: Request.ValidationResult) { + performEvent { $0.request(request, + didValidateRequest: urlRequest, + response: response, + withResult: result) + } + } + + public func request(_ request: DataStreamRequest, didParseStream result: Result) { + performEvent { $0.request(request, didParseStream: result) } + } + + public func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) { + performEvent { $0.request(request, didCreateUploadable: uploadable) } + } + + public func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) { + performEvent { $0.request(request, didFailToCreateUploadableWithError: error) } + } + + public func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) { + performEvent { $0.request(request, didProvideInputStream: stream) } + } + + public func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result) { + performEvent { $0.request(request, didFinishDownloadingUsing: task, with: result) } + } + + public func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) { + performEvent { $0.request(request, didCreateDestinationURL: url) } + } + + public func request(_ request: DownloadRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + fileURL: URL?, + withResult result: Request.ValidationResult) { + performEvent { $0.request(request, + didValidateRequest: urlRequest, + response: response, + fileURL: fileURL, + withResult: result) } + } + + public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { + performEvent { $0.request(request, didParseResponse: response) } + } + + public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { + performEvent { $0.request(request, didParseResponse: response) } + } +} + +/// `EventMonitor` that allows optional closures to be set to receive events. +open class ClosureEventMonitor: EventMonitor { + /// Closure called on the `urlSession(_:didBecomeInvalidWithError:)` event. + open var sessionDidBecomeInvalidWithError: ((URLSession, Error?) -> Void)? + + /// Closure called on the `urlSession(_:task:didReceive:completionHandler:)`. + open var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> Void)? + + /// Closure that receives `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` event. + open var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)? + + /// Closure called on the `urlSession(_:task:needNewBodyStream:)` event. + open var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> Void)? + + /// Closure called on the `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` event. + open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> Void)? + + /// Closure called on the `urlSession(_:task:didFinishCollecting:)` event. + open var taskDidFinishCollectingMetrics: ((URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void)? + + /// Closure called on the `urlSession(_:task:didCompleteWithError:)` event. + open var taskDidComplete: ((URLSession, URLSessionTask, Error?) -> Void)? + + /// Closure called on the `urlSession(_:taskIsWaitingForConnectivity:)` event. + open var taskIsWaitingForConnectivity: ((URLSession, URLSessionTask) -> Void)? + + /// Closure that receives the `urlSession(_:dataTask:didReceive:)` event. + open var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)? + + /// Closure called on the `urlSession(_:dataTask:willCacheResponse:completionHandler:)` event. + open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> Void)? + + /// Closure called on the `urlSession(_:downloadTask:didFinishDownloadingTo:)` event. + open var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> Void)? + + /// Closure called on the `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)` + /// event. + open var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? + + /// Closure called on the `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)` event. + open var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)? + + // MARK: - Request Events + + /// Closure called on the `request(_:didCreateInitialURLRequest:)` event. + open var requestDidCreateInitialURLRequest: ((Request, URLRequest) -> Void)? + + /// Closure called on the `request(_:didFailToCreateURLRequestWithError:)` event. + open var requestDidFailToCreateURLRequestWithError: ((Request, AFError) -> Void)? + + /// Closure called on the `request(_:didAdaptInitialRequest:to:)` event. + open var requestDidAdaptInitialRequestToAdaptedRequest: ((Request, URLRequest, URLRequest) -> Void)? + + /// Closure called on the `request(_:didFailToAdaptURLRequest:withError:)` event. + open var requestDidFailToAdaptURLRequestWithError: ((Request, URLRequest, AFError) -> Void)? + + /// Closure called on the `request(_:didCreateURLRequest:)` event. + open var requestDidCreateURLRequest: ((Request, URLRequest) -> Void)? + + /// Closure called on the `request(_:didCreateTask:)` event. + open var requestDidCreateTask: ((Request, URLSessionTask) -> Void)? + + /// Closure called on the `request(_:didGatherMetrics:)` event. + open var requestDidGatherMetrics: ((Request, URLSessionTaskMetrics) -> Void)? + + /// Closure called on the `request(_:didFailTask:earlyWithError:)` event. + open var requestDidFailTaskEarlyWithError: ((Request, URLSessionTask, AFError) -> Void)? + + /// Closure called on the `request(_:didCompleteTask:with:)` event. + open var requestDidCompleteTaskWithError: ((Request, URLSessionTask, AFError?) -> Void)? + + /// Closure called on the `requestIsRetrying(_:)` event. + open var requestIsRetrying: ((Request) -> Void)? + + /// Closure called on the `requestDidFinish(_:)` event. + open var requestDidFinish: ((Request) -> Void)? + + /// Closure called on the `requestDidResume(_:)` event. + open var requestDidResume: ((Request) -> Void)? + + /// Closure called on the `request(_:didResumeTask:)` event. + open var requestDidResumeTask: ((Request, URLSessionTask) -> Void)? + + /// Closure called on the `requestDidSuspend(_:)` event. + open var requestDidSuspend: ((Request) -> Void)? + + /// Closure called on the `request(_:didSuspendTask:)` event. + open var requestDidSuspendTask: ((Request, URLSessionTask) -> Void)? + + /// Closure called on the `requestDidCancel(_:)` event. + open var requestDidCancel: ((Request) -> Void)? + + /// Closure called on the `request(_:didCancelTask:)` event. + open var requestDidCancelTask: ((Request, URLSessionTask) -> Void)? + + /// Closure called on the `request(_:didValidateRequest:response:data:withResult:)` event. + open var requestDidValidateRequestResponseDataWithResult: ((DataRequest, URLRequest?, HTTPURLResponse, Data?, Request.ValidationResult) -> Void)? + + /// Closure called on the `request(_:didParseResponse:)` event. + open var requestDidParseResponse: ((DataRequest, DataResponse) -> Void)? + + /// Closure called on the `request(_:didValidateRequest:response:withResult:)` event. + open var requestDidValidateRequestResponseWithResult: ((DataStreamRequest, URLRequest?, HTTPURLResponse, Request.ValidationResult) -> Void)? + + /// Closure called on the `request(_:didCreateUploadable:)` event. + open var requestDidCreateUploadable: ((UploadRequest, UploadRequest.Uploadable) -> Void)? + + /// Closure called on the `request(_:didFailToCreateUploadableWithError:)` event. + open var requestDidFailToCreateUploadableWithError: ((UploadRequest, AFError) -> Void)? + + /// Closure called on the `request(_:didProvideInputStream:)` event. + open var requestDidProvideInputStream: ((UploadRequest, InputStream) -> Void)? + + /// Closure called on the `request(_:didFinishDownloadingUsing:with:)` event. + open var requestDidFinishDownloadingUsingTaskWithResult: ((DownloadRequest, URLSessionTask, Result) -> Void)? + + /// Closure called on the `request(_:didCreateDestinationURL:)` event. + open var requestDidCreateDestinationURL: ((DownloadRequest, URL) -> Void)? + + /// Closure called on the `request(_:didValidateRequest:response:temporaryURL:destinationURL:withResult:)` event. + open var requestDidValidateRequestResponseFileURLWithResult: ((DownloadRequest, URLRequest?, HTTPURLResponse, URL?, Request.ValidationResult) -> Void)? + + /// Closure called on the `request(_:didParseResponse:)` event. + open var requestDidParseDownloadResponse: ((DownloadRequest, DownloadResponse) -> Void)? + + public let queue: DispatchQueue + + /// Creates an instance using the provided queue. + /// + /// - Parameter queue: `DispatchQueue` on which events will fired. `.main` by default. + public init(queue: DispatchQueue = .main) { + self.queue = queue + } + + open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + sessionDidBecomeInvalidWithError?(session, error) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) { + taskDidReceiveChallenge?(session, task, challenge) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) { + taskDidSendBodyData?(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend) + } + + open func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) { + taskNeedNewBodyStream?(session, task) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest) { + taskWillPerformHTTPRedirection?(session, task, response, request) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + taskDidFinishCollectingMetrics?(session, task, metrics) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + taskDidComplete?(session, task, error) + } + + open func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { + taskIsWaitingForConnectivity?(session, task) + } + + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + dataTaskDidReceiveData?(session, dataTask, data) + } + + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse) { + dataTaskWillCacheResponse?(session, dataTask, proposedResponse) + } + + open func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) { + downloadTaskDidResumeAtOffset?(session, downloadTask, fileOffset, expectedTotalBytes) + } + + open func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) { + downloadTaskDidWriteData?(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) + } + + open func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + downloadTaskDidFinishDownloadingToURL?(session, downloadTask, location) + } + + // MARK: Request Events + + open func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) { + requestDidCreateInitialURLRequest?(request, urlRequest) + } + + open func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) { + requestDidFailToCreateURLRequestWithError?(request, error) + } + + open func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) { + requestDidAdaptInitialRequestToAdaptedRequest?(request, initialRequest, adaptedRequest) + } + + open func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: AFError) { + requestDidFailToAdaptURLRequestWithError?(request, initialRequest, error) + } + + open func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) { + requestDidCreateURLRequest?(request, urlRequest) + } + + open func request(_ request: Request, didCreateTask task: URLSessionTask) { + requestDidCreateTask?(request, task) + } + + open func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) { + requestDidGatherMetrics?(request, metrics) + } + + open func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) { + requestDidFailTaskEarlyWithError?(request, task, error) + } + + open func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) { + requestDidCompleteTaskWithError?(request, task, error) + } + + open func requestIsRetrying(_ request: Request) { + requestIsRetrying?(request) + } + + open func requestDidFinish(_ request: Request) { + requestDidFinish?(request) + } + + open func requestDidResume(_ request: Request) { + requestDidResume?(request) + } + + public func request(_ request: Request, didResumeTask task: URLSessionTask) { + requestDidResumeTask?(request, task) + } + + open func requestDidSuspend(_ request: Request) { + requestDidSuspend?(request) + } + + public func request(_ request: Request, didSuspendTask task: URLSessionTask) { + requestDidSuspendTask?(request, task) + } + + open func requestDidCancel(_ request: Request) { + requestDidCancel?(request) + } + + public func request(_ request: Request, didCancelTask task: URLSessionTask) { + requestDidCancelTask?(request, task) + } + + open func request(_ request: DataRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + data: Data?, + withResult result: Request.ValidationResult) { + requestDidValidateRequestResponseDataWithResult?(request, urlRequest, response, data, result) + } + + open func request(_ request: DataRequest, didParseResponse response: DataResponse) { + requestDidParseResponse?(request, response) + } + + public func request(_ request: DataStreamRequest, didValidateRequest urlRequest: URLRequest?, response: HTTPURLResponse, withResult result: Request.ValidationResult) { + requestDidValidateRequestResponseWithResult?(request, urlRequest, response, result) + } + + open func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) { + requestDidCreateUploadable?(request, uploadable) + } + + open func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) { + requestDidFailToCreateUploadableWithError?(request, error) + } + + open func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) { + requestDidProvideInputStream?(request, stream) + } + + open func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result) { + requestDidFinishDownloadingUsingTaskWithResult?(request, task, result) + } + + open func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) { + requestDidCreateDestinationURL?(request, url) + } + + open func request(_ request: DownloadRequest, + didValidateRequest urlRequest: URLRequest?, + response: HTTPURLResponse, + fileURL: URL?, + withResult result: Request.ValidationResult) { + requestDidValidateRequestResponseFileURLWithResult?(request, + urlRequest, + response, + fileURL, + result) + } + + open func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { + requestDidParseDownloadResponse?(request, response) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/HTTPHeaders.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/HTTPHeaders.swift new file mode 100644 index 0000000..cdbdbc6 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/HTTPHeaders.swift @@ -0,0 +1,447 @@ +// +// HTTPHeaders.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// An order-preserving and case-insensitive representation of HTTP headers. +public struct HTTPHeaders { + private var headers: [HTTPHeader] = [] + + /// Creates an empty instance. + public init() {} + + /// Creates an instance from an array of `HTTPHeader`s. Duplicate case-insensitive names are collapsed into the last + /// name and value encountered. + public init(_ headers: [HTTPHeader]) { + self.init() + + headers.forEach { update($0) } + } + + /// Creates an instance from a `[String: String]`. Duplicate case-insensitive names are collapsed into the last name + /// and value encountered. + public init(_ dictionary: [String: String]) { + self.init() + + dictionary.forEach { update(HTTPHeader(name: $0.key, value: $0.value)) } + } + + /// Case-insensitively updates or appends an `HTTPHeader` into the instance using the provided `name` and `value`. + /// + /// - Parameters: + /// - name: The `HTTPHeader` name. + /// - value: The `HTTPHeader value. + public mutating func add(name: String, value: String) { + update(HTTPHeader(name: name, value: value)) + } + + /// Case-insensitively updates or appends the provided `HTTPHeader` into the instance. + /// + /// - Parameter header: The `HTTPHeader` to update or append. + public mutating func add(_ header: HTTPHeader) { + update(header) + } + + /// Case-insensitively updates or appends an `HTTPHeader` into the instance using the provided `name` and `value`. + /// + /// - Parameters: + /// - name: The `HTTPHeader` name. + /// - value: The `HTTPHeader value. + public mutating func update(name: String, value: String) { + update(HTTPHeader(name: name, value: value)) + } + + /// Case-insensitively updates or appends the provided `HTTPHeader` into the instance. + /// + /// - Parameter header: The `HTTPHeader` to update or append. + public mutating func update(_ header: HTTPHeader) { + guard let index = headers.index(of: header.name) else { + headers.append(header) + return + } + + headers.replaceSubrange(index...index, with: [header]) + } + + /// Case-insensitively removes an `HTTPHeader`, if it exists, from the instance. + /// + /// - Parameter name: The name of the `HTTPHeader` to remove. + public mutating func remove(name: String) { + guard let index = headers.index(of: name) else { return } + + headers.remove(at: index) + } + + /// Sort the current instance by header name, case insensitively. + public mutating func sort() { + headers.sort { $0.name.lowercased() < $1.name.lowercased() } + } + + /// Returns an instance sorted by header name. + /// + /// - Returns: A copy of the current instance sorted by name. + public func sorted() -> HTTPHeaders { + var headers = self + headers.sort() + + return headers + } + + /// Case-insensitively find a header's value by name. + /// + /// - Parameter name: The name of the header to search for, case-insensitively. + /// + /// - Returns: The value of header, if it exists. + public func value(for name: String) -> String? { + guard let index = headers.index(of: name) else { return nil } + + return headers[index].value + } + + /// Case-insensitively access the header with the given name. + /// + /// - Parameter name: The name of the header. + public subscript(_ name: String) -> String? { + get { value(for: name) } + set { + if let value = newValue { + update(name: name, value: value) + } else { + remove(name: name) + } + } + } + + /// The dictionary representation of all headers. + /// + /// This representation does not preserve the current order of the instance. + public var dictionary: [String: String] { + let namesAndValues = headers.map { ($0.name, $0.value) } + + return Dictionary(namesAndValues, uniquingKeysWith: { _, last in last }) + } +} + +extension HTTPHeaders: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, String)...) { + self.init() + + elements.forEach { update(name: $0.0, value: $0.1) } + } +} + +extension HTTPHeaders: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: HTTPHeader...) { + self.init(elements) + } +} + +extension HTTPHeaders: Sequence { + public func makeIterator() -> IndexingIterator<[HTTPHeader]> { + headers.makeIterator() + } +} + +extension HTTPHeaders: Collection { + public var startIndex: Int { + headers.startIndex + } + + public var endIndex: Int { + headers.endIndex + } + + public subscript(position: Int) -> HTTPHeader { + headers[position] + } + + public func index(after i: Int) -> Int { + headers.index(after: i) + } +} + +extension HTTPHeaders: CustomStringConvertible { + public var description: String { + headers.map(\.description) + .joined(separator: "\n") + } +} + +// MARK: - HTTPHeader + +/// A representation of a single HTTP header's name / value pair. +public struct HTTPHeader: Hashable { + /// Name of the header. + public let name: String + + /// Value of the header. + public let value: String + + /// Creates an instance from the given `name` and `value`. + /// + /// - Parameters: + /// - name: The name of the header. + /// - value: The value of the header. + public init(name: String, value: String) { + self.name = name + self.value = value + } +} + +extension HTTPHeader: CustomStringConvertible { + public var description: String { + "\(name): \(value)" + } +} + +extension HTTPHeader { + /// Returns an `Accept` header. + /// + /// - Parameter value: The `Accept` value. + /// - Returns: The header. + public static func accept(_ value: String) -> HTTPHeader { + HTTPHeader(name: "Accept", value: value) + } + + /// Returns an `Accept-Charset` header. + /// + /// - Parameter value: The `Accept-Charset` value. + /// - Returns: The header. + public static func acceptCharset(_ value: String) -> HTTPHeader { + HTTPHeader(name: "Accept-Charset", value: value) + } + + /// Returns an `Accept-Language` header. + /// + /// Alamofire offers a default Accept-Language header that accumulates and encodes the system's preferred languages. + /// Use `HTTPHeader.defaultAcceptLanguage`. + /// + /// - Parameter value: The `Accept-Language` value. + /// + /// - Returns: The header. + public static func acceptLanguage(_ value: String) -> HTTPHeader { + HTTPHeader(name: "Accept-Language", value: value) + } + + /// Returns an `Accept-Encoding` header. + /// + /// Alamofire offers a default accept encoding value that provides the most common values. Use + /// `HTTPHeader.defaultAcceptEncoding`. + /// + /// - Parameter value: The `Accept-Encoding` value. + /// + /// - Returns: The header + public static func acceptEncoding(_ value: String) -> HTTPHeader { + HTTPHeader(name: "Accept-Encoding", value: value) + } + + /// Returns a `Basic` `Authorization` header using the `username` and `password` provided. + /// + /// - Parameters: + /// - username: The username of the header. + /// - password: The password of the header. + /// + /// - Returns: The header. + public static func authorization(username: String, password: String) -> HTTPHeader { + let credential = Data("\(username):\(password)".utf8).base64EncodedString() + + return authorization("Basic \(credential)") + } + + /// Returns a `Bearer` `Authorization` header using the `bearerToken` provided + /// + /// - Parameter bearerToken: The bearer token. + /// + /// - Returns: The header. + public static func authorization(bearerToken: String) -> HTTPHeader { + authorization("Bearer \(bearerToken)") + } + + /// Returns an `Authorization` header. + /// + /// Alamofire provides built-in methods to produce `Authorization` headers. For a Basic `Authorization` header use + /// `HTTPHeader.authorization(username:password:)`. For a Bearer `Authorization` header, use + /// `HTTPHeader.authorization(bearerToken:)`. + /// + /// - Parameter value: The `Authorization` value. + /// + /// - Returns: The header. + public static func authorization(_ value: String) -> HTTPHeader { + HTTPHeader(name: "Authorization", value: value) + } + + /// Returns a `Content-Disposition` header. + /// + /// - Parameter value: The `Content-Disposition` value. + /// + /// - Returns: The header. + public static func contentDisposition(_ value: String) -> HTTPHeader { + HTTPHeader(name: "Content-Disposition", value: value) + } + + /// Returns a `Content-Type` header. + /// + /// All Alamofire `ParameterEncoding`s and `ParameterEncoder`s set the `Content-Type` of the request, so it may not be necessary to manually + /// set this value. + /// + /// - Parameter value: The `Content-Type` value. + /// + /// - Returns: The header. + public static func contentType(_ value: String) -> HTTPHeader { + HTTPHeader(name: "Content-Type", value: value) + } + + /// Returns a `User-Agent` header. + /// + /// - Parameter value: The `User-Agent` value. + /// + /// - Returns: The header. + public static func userAgent(_ value: String) -> HTTPHeader { + HTTPHeader(name: "User-Agent", value: value) + } +} + +extension Array where Element == HTTPHeader { + /// Case-insensitively finds the index of an `HTTPHeader` with the provided name, if it exists. + func index(of name: String) -> Int? { + let lowercasedName = name.lowercased() + return firstIndex { $0.name.lowercased() == lowercasedName } + } +} + +// MARK: - Defaults + +extension HTTPHeaders { + /// The default set of `HTTPHeaders` used by Alamofire. Includes `Accept-Encoding`, `Accept-Language`, and + /// `User-Agent`. + public static let `default`: HTTPHeaders = [.defaultAcceptEncoding, + .defaultAcceptLanguage, + .defaultUserAgent] +} + +extension HTTPHeader { + /// Returns Alamofire's default `Accept-Encoding` header, appropriate for the encodings supported by particular OS + /// versions. + /// + /// See the [Accept-Encoding HTTP header documentation](https://tools.ietf.org/html/rfc7230#section-4.2.3) . + public static let defaultAcceptEncoding: HTTPHeader = { + let encodings: [String] + if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) { + encodings = ["br", "gzip", "deflate"] + } else { + encodings = ["gzip", "deflate"] + } + + return .acceptEncoding(encodings.qualityEncoded()) + }() + + /// Returns Alamofire's default `Accept-Language` header, generated by querying `Locale` for the user's + /// `preferredLanguages`. + /// + /// See the [Accept-Language HTTP header documentation](https://tools.ietf.org/html/rfc7231#section-5.3.5). + public static let defaultAcceptLanguage: HTTPHeader = .acceptLanguage(Locale.preferredLanguages.prefix(6).qualityEncoded()) + + /// Returns Alamofire's default `User-Agent` header. + /// + /// See the [User-Agent header documentation](https://tools.ietf.org/html/rfc7231#section-5.5.3). + /// + /// Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 13.0.0) Alamofire/5.0.0` + public static let defaultUserAgent: HTTPHeader = { + let info = Bundle.main.infoDictionary + let executable = (info?["CFBundleExecutable"] as? String) ?? + (ProcessInfo.processInfo.arguments.first?.split(separator: "/").last.map(String.init)) ?? + "Unknown" + let bundle = info?["CFBundleIdentifier"] as? String ?? "Unknown" + let appVersion = info?["CFBundleShortVersionString"] as? String ?? "Unknown" + let appBuild = info?["CFBundleVersion"] as? String ?? "Unknown" + + let osNameVersion: String = { + let version = ProcessInfo.processInfo.operatingSystemVersion + let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" + let osName: String = { + #if os(iOS) + #if targetEnvironment(macCatalyst) + return "macOS(Catalyst)" + #else + return "iOS" + #endif + #elseif os(watchOS) + return "watchOS" + #elseif os(tvOS) + return "tvOS" + #elseif os(macOS) + return "macOS" + #elseif os(Linux) + return "Linux" + #elseif os(Windows) + return "Windows" + #else + return "Unknown" + #endif + }() + + return "\(osName) \(versionString)" + }() + + let alamofireVersion = "Alamofire/\(version)" + + let userAgent = "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)" + + return .userAgent(userAgent) + }() +} + +extension Collection where Element == String { + func qualityEncoded() -> String { + enumerated().map { index, encoding in + let quality = 1.0 - (Double(index) * 0.1) + return "\(encoding);q=\(quality)" + }.joined(separator: ", ") + } +} + +// MARK: - System Type Extensions + +extension URLRequest { + /// Returns `allHTTPHeaderFields` as `HTTPHeaders`. + public var headers: HTTPHeaders { + get { allHTTPHeaderFields.map(HTTPHeaders.init) ?? HTTPHeaders() } + set { allHTTPHeaderFields = newValue.dictionary } + } +} + +extension HTTPURLResponse { + /// Returns `allHeaderFields` as `HTTPHeaders`. + public var headers: HTTPHeaders { + (allHeaderFields as? [String: String]).map(HTTPHeaders.init) ?? HTTPHeaders() + } +} + +extension URLSessionConfiguration { + /// Returns `httpAdditionalHeaders` as `HTTPHeaders`. + public var headers: HTTPHeaders { + get { (httpAdditionalHeaders as? [String: String]).map(HTTPHeaders.init) ?? HTTPHeaders() } + set { httpAdditionalHeaders = newValue.dictionary } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/HTTPMethod.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/HTTPMethod.swift new file mode 100644 index 0000000..539d214 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/HTTPMethod.swift @@ -0,0 +1,56 @@ +// +// HTTPMethod.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +/// Type representing HTTP methods. Raw `String` value is stored and compared case-sensitively, so +/// `HTTPMethod.get != HTTPMethod(rawValue: "get")`. +/// +/// See https://tools.ietf.org/html/rfc7231#section-4.3 +public struct HTTPMethod: RawRepresentable, Equatable, Hashable { + /// `CONNECT` method. + public static let connect = HTTPMethod(rawValue: "CONNECT") + /// `DELETE` method. + public static let delete = HTTPMethod(rawValue: "DELETE") + /// `GET` method. + public static let get = HTTPMethod(rawValue: "GET") + /// `HEAD` method. + public static let head = HTTPMethod(rawValue: "HEAD") + /// `OPTIONS` method. + public static let options = HTTPMethod(rawValue: "OPTIONS") + /// `PATCH` method. + public static let patch = HTTPMethod(rawValue: "PATCH") + /// `POST` method. + public static let post = HTTPMethod(rawValue: "POST") + /// `PUT` method. + public static let put = HTTPMethod(rawValue: "PUT") + /// `QUERY` method. + public static let query = HTTPMethod(rawValue: "QUERY") + /// `TRACE` method. + public static let trace = HTTPMethod(rawValue: "TRACE") + + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/MultipartFormData.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/MultipartFormData.swift new file mode 100644 index 0000000..364b614 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/MultipartFormData.swift @@ -0,0 +1,584 @@ +// +// MultipartFormData.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +#if os(iOS) || os(watchOS) || os(tvOS) +import MobileCoreServices +#elseif os(macOS) +import CoreServices +#endif + +/// Constructs `multipart/form-data` for uploads within an HTTP or HTTPS body. There are currently two ways to encode +/// multipart form data. The first way is to encode the data directly in memory. This is very efficient, but can lead +/// to memory issues if the dataset is too large. The second way is designed for larger datasets and will write all the +/// data to a single file on disk with all the proper boundary segmentation. The second approach MUST be used for +/// larger datasets such as video content, otherwise your app may run out of memory when trying to encode the dataset. +/// +/// For more information on `multipart/form-data` in general, please refer to the RFC-2388 and RFC-2045 specs as well +/// and the w3 form documentation. +/// +/// - https://www.ietf.org/rfc/rfc2388.txt +/// - https://www.ietf.org/rfc/rfc2045.txt +/// - https://www.w3.org/TR/html401/interact/forms.html#h-17.13 +open class MultipartFormData { + // MARK: - Helper Types + + enum EncodingCharacters { + static let crlf = "\r\n" + } + + enum BoundaryGenerator { + enum BoundaryType { + case initial, encapsulated, final + } + + static func randomBoundary() -> String { + let first = UInt32.random(in: UInt32.min...UInt32.max) + let second = UInt32.random(in: UInt32.min...UInt32.max) + + return String(format: "alamofire.boundary.%08x%08x", first, second) + } + + static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data { + let boundaryText: String + + switch boundaryType { + case .initial: + boundaryText = "--\(boundary)\(EncodingCharacters.crlf)" + case .encapsulated: + boundaryText = "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)" + case .final: + boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)" + } + + return Data(boundaryText.utf8) + } + } + + class BodyPart { + let headers: HTTPHeaders + let bodyStream: InputStream + let bodyContentLength: UInt64 + var hasInitialBoundary = false + var hasFinalBoundary = false + + init(headers: HTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64) { + self.headers = headers + self.bodyStream = bodyStream + self.bodyContentLength = bodyContentLength + } + } + + // MARK: - Properties + + /// Default memory threshold used when encoding `MultipartFormData`, in bytes. + public static let encodingMemoryThreshold: UInt64 = 10_000_000 + + /// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`. + open lazy var contentType: String = "multipart/form-data; boundary=\(self.boundary)" + + /// The content length of all body parts used to generate the `multipart/form-data` not including the boundaries. + public var contentLength: UInt64 { bodyParts.reduce(0) { $0 + $1.bodyContentLength } } + + /// The boundary used to separate the body parts in the encoded form data. + public let boundary: String + + let fileManager: FileManager + + private var bodyParts: [BodyPart] + private var bodyPartError: AFError? + private let streamBufferSize: Int + + // MARK: - Lifecycle + + /// Creates an instance. + /// + /// - Parameters: + /// - fileManager: `FileManager` to use for file operations, if needed. + /// - boundary: Boundary `String` used to separate body parts. + public init(fileManager: FileManager = .default, boundary: String? = nil) { + self.fileManager = fileManager + self.boundary = boundary ?? BoundaryGenerator.randomBoundary() + bodyParts = [] + + // + // The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more + // information, please refer to the following article: + // - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html + // + streamBufferSize = 1024 + } + + // MARK: - Body Parts + + /// Creates a body part from the data and appends it to the instance. + /// + /// The body part data will be encoded using the following format: + /// + /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header) + /// - `Content-Type: #{mimeType}` (HTTP Header) + /// - Encoded file data + /// - Multipart form boundary + /// + /// - Parameters: + /// - data: `Data` to encoding into the instance. + /// - name: Name to associate with the `Data` in the `Content-Disposition` HTTP header. + /// - fileName: Filename to associate with the `Data` in the `Content-Disposition` HTTP header. + /// - mimeType: MIME type to associate with the data in the `Content-Type` HTTP header. + public func append(_ data: Data, withName name: String, fileName: String? = nil, mimeType: String? = nil) { + let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) + let stream = InputStream(data: data) + let length = UInt64(data.count) + + append(stream, withLength: length, headers: headers) + } + + /// Creates a body part from the file and appends it to the instance. + /// + /// The body part data will be encoded using the following format: + /// + /// - `Content-Disposition: form-data; name=#{name}; filename=#{generated filename}` (HTTP Header) + /// - `Content-Type: #{generated mimeType}` (HTTP Header) + /// - Encoded file data + /// - Multipart form boundary + /// + /// The filename in the `Content-Disposition` HTTP header is generated from the last path component of the + /// `fileURL`. The `Content-Type` HTTP header MIME type is generated by mapping the `fileURL` extension to the + /// system associated MIME type. + /// + /// - Parameters: + /// - fileURL: `URL` of the file whose content will be encoded into the instance. + /// - name: Name to associate with the file content in the `Content-Disposition` HTTP header. + public func append(_ fileURL: URL, withName name: String) { + let fileName = fileURL.lastPathComponent + let pathExtension = fileURL.pathExtension + + if !fileName.isEmpty && !pathExtension.isEmpty { + let mime = mimeType(forPathExtension: pathExtension) + append(fileURL, withName: name, fileName: fileName, mimeType: mime) + } else { + setBodyPartError(withReason: .bodyPartFilenameInvalid(in: fileURL)) + } + } + + /// Creates a body part from the file and appends it to the instance. + /// + /// The body part data will be encoded using the following format: + /// + /// - Content-Disposition: form-data; name=#{name}; filename=#{filename} (HTTP Header) + /// - Content-Type: #{mimeType} (HTTP Header) + /// - Encoded file data + /// - Multipart form boundary + /// + /// - Parameters: + /// - fileURL: `URL` of the file whose content will be encoded into the instance. + /// - name: Name to associate with the file content in the `Content-Disposition` HTTP header. + /// - fileName: Filename to associate with the file content in the `Content-Disposition` HTTP header. + /// - mimeType: MIME type to associate with the file content in the `Content-Type` HTTP header. + public func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String) { + let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) + + //============================================================ + // Check 1 - is file URL? + //============================================================ + + guard fileURL.isFileURL else { + setBodyPartError(withReason: .bodyPartURLInvalid(url: fileURL)) + return + } + + //============================================================ + // Check 2 - is file URL reachable? + //============================================================ + + #if !(os(Linux) || os(Windows)) + do { + let isReachable = try fileURL.checkPromisedItemIsReachable() + guard isReachable else { + setBodyPartError(withReason: .bodyPartFileNotReachable(at: fileURL)) + return + } + } catch { + setBodyPartError(withReason: .bodyPartFileNotReachableWithError(atURL: fileURL, error: error)) + return + } + #endif + + //============================================================ + // Check 3 - is file URL a directory? + //============================================================ + + var isDirectory: ObjCBool = false + let path = fileURL.path + + guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory.boolValue else { + setBodyPartError(withReason: .bodyPartFileIsDirectory(at: fileURL)) + return + } + + //============================================================ + // Check 4 - can the file size be extracted? + //============================================================ + + let bodyContentLength: UInt64 + + do { + guard let fileSize = try fileManager.attributesOfItem(atPath: path)[.size] as? NSNumber else { + setBodyPartError(withReason: .bodyPartFileSizeNotAvailable(at: fileURL)) + return + } + + bodyContentLength = fileSize.uint64Value + } catch { + setBodyPartError(withReason: .bodyPartFileSizeQueryFailedWithError(forURL: fileURL, error: error)) + return + } + + //============================================================ + // Check 5 - can a stream be created from file URL? + //============================================================ + + guard let stream = InputStream(url: fileURL) else { + setBodyPartError(withReason: .bodyPartInputStreamCreationFailed(for: fileURL)) + return + } + + append(stream, withLength: bodyContentLength, headers: headers) + } + + /// Creates a body part from the stream and appends it to the instance. + /// + /// The body part data will be encoded using the following format: + /// + /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header) + /// - `Content-Type: #{mimeType}` (HTTP Header) + /// - Encoded stream data + /// - Multipart form boundary + /// + /// - Parameters: + /// - stream: `InputStream` to encode into the instance. + /// - length: Length, in bytes, of the stream. + /// - name: Name to associate with the stream content in the `Content-Disposition` HTTP header. + /// - fileName: Filename to associate with the stream content in the `Content-Disposition` HTTP header. + /// - mimeType: MIME type to associate with the stream content in the `Content-Type` HTTP header. + public func append(_ stream: InputStream, + withLength length: UInt64, + name: String, + fileName: String, + mimeType: String) { + let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) + append(stream, withLength: length, headers: headers) + } + + /// Creates a body part with the stream, length, and headers and appends it to the instance. + /// + /// The body part data will be encoded using the following format: + /// + /// - HTTP headers + /// - Encoded stream data + /// - Multipart form boundary + /// + /// - Parameters: + /// - stream: `InputStream` to encode into the instance. + /// - length: Length, in bytes, of the stream. + /// - headers: `HTTPHeaders` for the body part. + public func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPHeaders) { + let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length) + bodyParts.append(bodyPart) + } + + // MARK: - Data Encoding + + /// Encodes all appended body parts into a single `Data` value. + /// + /// - Note: This method will load all the appended body parts into memory all at the same time. This method should + /// only be used when the encoded data will have a small memory footprint. For large data cases, please use + /// the `writeEncodedData(to:))` method. + /// + /// - Returns: The encoded `Data`, if encoding is successful. + /// - Throws: An `AFError` if encoding encounters an error. + public func encode() throws -> Data { + if let bodyPartError = bodyPartError { + throw bodyPartError + } + + var encoded = Data() + + bodyParts.first?.hasInitialBoundary = true + bodyParts.last?.hasFinalBoundary = true + + for bodyPart in bodyParts { + let encodedData = try encode(bodyPart) + encoded.append(encodedData) + } + + return encoded + } + + /// Writes all appended body parts to the given file `URL`. + /// + /// This process is facilitated by reading and writing with input and output streams, respectively. Thus, + /// this approach is very memory efficient and should be used for large body part data. + /// + /// - Parameter fileURL: File `URL` to which to write the form data. + /// - Throws: An `AFError` if encoding encounters an error. + public func writeEncodedData(to fileURL: URL) throws { + if let bodyPartError = bodyPartError { + throw bodyPartError + } + + if fileManager.fileExists(atPath: fileURL.path) { + throw AFError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL)) + } else if !fileURL.isFileURL { + throw AFError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL)) + } + + guard let outputStream = OutputStream(url: fileURL, append: false) else { + throw AFError.multipartEncodingFailed(reason: .outputStreamCreationFailed(for: fileURL)) + } + + outputStream.open() + defer { outputStream.close() } + + bodyParts.first?.hasInitialBoundary = true + bodyParts.last?.hasFinalBoundary = true + + for bodyPart in bodyParts { + try write(bodyPart, to: outputStream) + } + } + + // MARK: - Private - Body Part Encoding + + private func encode(_ bodyPart: BodyPart) throws -> Data { + var encoded = Data() + + let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData() + encoded.append(initialData) + + let headerData = encodeHeaders(for: bodyPart) + encoded.append(headerData) + + let bodyStreamData = try encodeBodyStream(for: bodyPart) + encoded.append(bodyStreamData) + + if bodyPart.hasFinalBoundary { + encoded.append(finalBoundaryData()) + } + + return encoded + } + + private func encodeHeaders(for bodyPart: BodyPart) -> Data { + let headerText = bodyPart.headers.map { "\($0.name): \($0.value)\(EncodingCharacters.crlf)" } + .joined() + + EncodingCharacters.crlf + + return Data(headerText.utf8) + } + + private func encodeBodyStream(for bodyPart: BodyPart) throws -> Data { + let inputStream = bodyPart.bodyStream + inputStream.open() + defer { inputStream.close() } + + var encoded = Data() + + while inputStream.hasBytesAvailable { + var buffer = [UInt8](repeating: 0, count: streamBufferSize) + let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize) + + if let error = inputStream.streamError { + throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error)) + } + + if bytesRead > 0 { + encoded.append(buffer, count: bytesRead) + } else { + break + } + } + + guard UInt64(encoded.count) == bodyPart.bodyContentLength else { + let error = AFError.UnexpectedInputStreamLength(bytesExpected: bodyPart.bodyContentLength, + bytesRead: UInt64(encoded.count)) + throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error)) + } + + return encoded + } + + // MARK: - Private - Writing Body Part to Output Stream + + private func write(_ bodyPart: BodyPart, to outputStream: OutputStream) throws { + try writeInitialBoundaryData(for: bodyPart, to: outputStream) + try writeHeaderData(for: bodyPart, to: outputStream) + try writeBodyStream(for: bodyPart, to: outputStream) + try writeFinalBoundaryData(for: bodyPart, to: outputStream) + } + + private func writeInitialBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws { + let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData() + return try write(initialData, to: outputStream) + } + + private func writeHeaderData(for bodyPart: BodyPart, to outputStream: OutputStream) throws { + let headerData = encodeHeaders(for: bodyPart) + return try write(headerData, to: outputStream) + } + + private func writeBodyStream(for bodyPart: BodyPart, to outputStream: OutputStream) throws { + let inputStream = bodyPart.bodyStream + + inputStream.open() + defer { inputStream.close() } + + while inputStream.hasBytesAvailable { + var buffer = [UInt8](repeating: 0, count: streamBufferSize) + let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize) + + if let streamError = inputStream.streamError { + throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: streamError)) + } + + if bytesRead > 0 { + if buffer.count != bytesRead { + buffer = Array(buffer[0.. 0, outputStream.hasSpaceAvailable { + let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite) + + if let error = outputStream.streamError { + throw AFError.multipartEncodingFailed(reason: .outputStreamWriteFailed(error: error)) + } + + bytesToWrite -= bytesWritten + + if bytesToWrite > 0 { + buffer = Array(buffer[bytesWritten.. HTTPHeaders { + var disposition = "form-data; name=\"\(name)\"" + if let fileName = fileName { disposition += "; filename=\"\(fileName)\"" } + + var headers: HTTPHeaders = [.contentDisposition(disposition)] + if let mimeType = mimeType { headers.add(.contentType(mimeType)) } + + return headers + } + + // MARK: - Private - Boundary Encoding + + private func initialBoundaryData() -> Data { + BoundaryGenerator.boundaryData(forBoundaryType: .initial, boundary: boundary) + } + + private func encapsulatedBoundaryData() -> Data { + BoundaryGenerator.boundaryData(forBoundaryType: .encapsulated, boundary: boundary) + } + + private func finalBoundaryData() -> Data { + BoundaryGenerator.boundaryData(forBoundaryType: .final, boundary: boundary) + } + + // MARK: - Private - Errors + + private func setBodyPartError(withReason reason: AFError.MultipartEncodingFailureReason) { + guard bodyPartError == nil else { return } + bodyPartError = AFError.multipartEncodingFailed(reason: reason) + } +} + +#if canImport(UniformTypeIdentifiers) +import UniformTypeIdentifiers + +extension MultipartFormData { + // MARK: - Private - Mime Type + + private func mimeType(forPathExtension pathExtension: String) -> String { + if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) { + return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream" + } else { + if + let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(), + let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() { + return contentType as String + } + + return "application/octet-stream" + } + } +} + +#else + +extension MultipartFormData { + // MARK: - Private - Mime Type + + private func mimeType(forPathExtension pathExtension: String) -> String { + #if !(os(Linux) || os(Windows)) + if + let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(), + let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() { + return contentType as String + } + #endif + + return "application/octet-stream" + } +} + +#endif diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/MultipartUpload.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/MultipartUpload.swift new file mode 100644 index 0000000..ceda21f --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/MultipartUpload.swift @@ -0,0 +1,89 @@ +// +// MultipartUpload.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Internal type which encapsulates a `MultipartFormData` upload. +final class MultipartUpload { + lazy var result = Result { try build() } + + @Protected + private(set) var multipartFormData: MultipartFormData + let encodingMemoryThreshold: UInt64 + let request: URLRequestConvertible + let fileManager: FileManager + + init(encodingMemoryThreshold: UInt64, + request: URLRequestConvertible, + multipartFormData: MultipartFormData) { + self.encodingMemoryThreshold = encodingMemoryThreshold + self.request = request + fileManager = multipartFormData.fileManager + self.multipartFormData = multipartFormData + } + + func build() throws -> UploadRequest.Uploadable { + let uploadable: UploadRequest.Uploadable + if $multipartFormData.contentLength < encodingMemoryThreshold { + let data = try $multipartFormData.read { try $0.encode() } + + uploadable = .data(data) + } else { + let tempDirectoryURL = fileManager.temporaryDirectory + let directoryURL = tempDirectoryURL.appendingPathComponent("org.alamofire.manager/multipart.form.data") + let fileName = UUID().uuidString + let fileURL = directoryURL.appendingPathComponent(fileName) + + try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) + + do { + try $multipartFormData.read { try $0.writeEncodedData(to: fileURL) } + } catch { + // Cleanup after attempted write if it fails. + try? fileManager.removeItem(at: fileURL) + throw error + } + + uploadable = .file(fileURL, shouldRemove: true) + } + + return uploadable + } +} + +extension MultipartUpload: UploadConvertible { + func asURLRequest() throws -> URLRequest { + var urlRequest = try request.asURLRequest() + + $multipartFormData.read { multipartFormData in + urlRequest.headers.add(.contentType(multipartFormData.contentType)) + } + + return urlRequest + } + + func createUploadable() throws -> UploadRequest.Uploadable { + try result.get() + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/NetworkReachabilityManager.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/NetworkReachabilityManager.swift new file mode 100644 index 0000000..deeb3a4 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/NetworkReachabilityManager.swift @@ -0,0 +1,267 @@ +// +// NetworkReachabilityManager.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if !(os(watchOS) || os(Linux) || os(Windows)) + +import Foundation +import SystemConfiguration + +/// The `NetworkReachabilityManager` class listens for reachability changes of hosts and addresses for both cellular and +/// WiFi network interfaces. +/// +/// Reachability can be used to determine background information about why a network operation failed, or to retry +/// network requests when a connection is established. It should not be used to prevent a user from initiating a network +/// request, as it's possible that an initial request may be required to establish reachability. +open class NetworkReachabilityManager { + /// Defines the various states of network reachability. + public enum NetworkReachabilityStatus { + /// It is unknown whether the network is reachable. + case unknown + /// The network is not reachable. + case notReachable + /// The network is reachable on the associated `ConnectionType`. + case reachable(ConnectionType) + + init(_ flags: SCNetworkReachabilityFlags) { + guard flags.isActuallyReachable else { self = .notReachable; return } + + var networkStatus: NetworkReachabilityStatus = .reachable(.ethernetOrWiFi) + + if flags.isCellular { networkStatus = .reachable(.cellular) } + + self = networkStatus + } + + /// Defines the various connection types detected by reachability flags. + public enum ConnectionType { + /// The connection type is either over Ethernet or WiFi. + case ethernetOrWiFi + /// The connection type is a cellular connection. + case cellular + } + } + + /// A closure executed when the network reachability status changes. The closure takes a single argument: the + /// network reachability status. + public typealias Listener = (NetworkReachabilityStatus) -> Void + + /// Default `NetworkReachabilityManager` for the zero address and a `listenerQueue` of `.main`. + public static let `default` = NetworkReachabilityManager() + + // MARK: - Properties + + /// Whether the network is currently reachable. + open var isReachable: Bool { isReachableOnCellular || isReachableOnEthernetOrWiFi } + + /// Whether the network is currently reachable over the cellular interface. + /// + /// - Note: Using this property to decide whether to make a high or low bandwidth request is not recommended. + /// Instead, set the `allowsCellularAccess` on any `URLRequest`s being issued. + /// + open var isReachableOnCellular: Bool { status == .reachable(.cellular) } + + /// Whether the network is currently reachable over Ethernet or WiFi interface. + open var isReachableOnEthernetOrWiFi: Bool { status == .reachable(.ethernetOrWiFi) } + + /// `DispatchQueue` on which reachability will update. + public let reachabilityQueue = DispatchQueue(label: "org.alamofire.reachabilityQueue") + + /// Flags of the current reachability type, if any. + open var flags: SCNetworkReachabilityFlags? { + var flags = SCNetworkReachabilityFlags() + + return (SCNetworkReachabilityGetFlags(reachability, &flags)) ? flags : nil + } + + /// The current network reachability status. + open var status: NetworkReachabilityStatus { + flags.map(NetworkReachabilityStatus.init) ?? .unknown + } + + /// Mutable state storage. + struct MutableState { + /// A closure executed when the network reachability status changes. + var listener: Listener? + /// `DispatchQueue` on which listeners will be called. + var listenerQueue: DispatchQueue? + /// Previously calculated status. + var previousStatus: NetworkReachabilityStatus? + } + + /// `SCNetworkReachability` instance providing notifications. + private let reachability: SCNetworkReachability + + /// Protected storage for mutable state. + @Protected + private var mutableState = MutableState() + + // MARK: - Initialization + + /// Creates an instance with the specified host. + /// + /// - Note: The `host` value must *not* contain a scheme, just the hostname. + /// + /// - Parameters: + /// - host: Host used to evaluate network reachability. Must *not* include the scheme (e.g. `https`). + public convenience init?(host: String) { + guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil } + + self.init(reachability: reachability) + } + + /// Creates an instance that monitors the address 0.0.0.0. + /// + /// Reachability treats the 0.0.0.0 address as a special token that causes it to monitor the general routing + /// status of the device, both IPv4 and IPv6. + public convenience init?() { + var zero = sockaddr() + zero.sa_len = UInt8(MemoryLayout.size) + zero.sa_family = sa_family_t(AF_INET) + + guard let reachability = SCNetworkReachabilityCreateWithAddress(nil, &zero) else { return nil } + + self.init(reachability: reachability) + } + + private init(reachability: SCNetworkReachability) { + self.reachability = reachability + } + + deinit { + stopListening() + } + + // MARK: - Listening + + /// Starts listening for changes in network reachability status. + /// + /// - Note: Stops and removes any existing listener. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which to call the `listener` closure. `.main` by default. + /// - listener: `Listener` closure called when reachability changes. + /// + /// - Returns: `true` if listening was started successfully, `false` otherwise. + @discardableResult + open func startListening(onQueue queue: DispatchQueue = .main, + onUpdatePerforming listener: @escaping Listener) -> Bool { + stopListening() + + $mutableState.write { state in + state.listenerQueue = queue + state.listener = listener + } + + var context = SCNetworkReachabilityContext(version: 0, + info: Unmanaged.passUnretained(self).toOpaque(), + retain: nil, + release: nil, + copyDescription: nil) + let callback: SCNetworkReachabilityCallBack = { _, flags, info in + guard let info = info else { return } + + let instance = Unmanaged.fromOpaque(info).takeUnretainedValue() + instance.notifyListener(flags) + } + + let queueAdded = SCNetworkReachabilitySetDispatchQueue(reachability, reachabilityQueue) + let callbackAdded = SCNetworkReachabilitySetCallback(reachability, callback, &context) + + // Manually call listener to give initial state, since the framework may not. + if let currentFlags = flags { + reachabilityQueue.async { + self.notifyListener(currentFlags) + } + } + + return callbackAdded && queueAdded + } + + /// Stops listening for changes in network reachability status. + open func stopListening() { + SCNetworkReachabilitySetCallback(reachability, nil, nil) + SCNetworkReachabilitySetDispatchQueue(reachability, nil) + $mutableState.write { state in + state.listener = nil + state.listenerQueue = nil + state.previousStatus = nil + } + } + + // MARK: - Internal - Listener Notification + + /// Calls the `listener` closure of the `listenerQueue` if the computed status hasn't changed. + /// + /// - Note: Should only be called from the `reachabilityQueue`. + /// + /// - Parameter flags: `SCNetworkReachabilityFlags` to use to calculate the status. + func notifyListener(_ flags: SCNetworkReachabilityFlags) { + let newStatus = NetworkReachabilityStatus(flags) + + $mutableState.write { state in + guard state.previousStatus != newStatus else { return } + + state.previousStatus = newStatus + + let listener = state.listener + state.listenerQueue?.async { listener?(newStatus) } + } + } +} + +// MARK: - + +extension NetworkReachabilityManager.NetworkReachabilityStatus: Equatable {} + +extension SCNetworkReachabilityFlags { + var isReachable: Bool { contains(.reachable) } + var isConnectionRequired: Bool { contains(.connectionRequired) } + var canConnectAutomatically: Bool { contains(.connectionOnDemand) || contains(.connectionOnTraffic) } + var canConnectWithoutUserInteraction: Bool { canConnectAutomatically && !contains(.interventionRequired) } + var isActuallyReachable: Bool { isReachable && (!isConnectionRequired || canConnectWithoutUserInteraction) } + var isCellular: Bool { + #if os(iOS) || os(tvOS) + return contains(.isWWAN) + #else + return false + #endif + } + + /// Human readable `String` for all states, to help with debugging. + var readableDescription: String { + let W = isCellular ? "W" : "-" + let R = isReachable ? "R" : "-" + let c = isConnectionRequired ? "c" : "-" + let t = contains(.transientConnection) ? "t" : "-" + let i = contains(.interventionRequired) ? "i" : "-" + let C = contains(.connectionOnTraffic) ? "C" : "-" + let D = contains(.connectionOnDemand) ? "D" : "-" + let l = contains(.isLocalAddress) ? "l" : "-" + let d = contains(.isDirect) ? "d" : "-" + let a = contains(.connectionAutomatic) ? "a" : "-" + + return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)\(a)" + } +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/Notifications.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Notifications.swift new file mode 100644 index 0000000..66434b6 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Notifications.swift @@ -0,0 +1,115 @@ +// +// Notifications.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension Request { + /// Posted when a `Request` is resumed. The `Notification` contains the resumed `Request`. + public static let didResumeNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didResume") + /// Posted when a `Request` is suspended. The `Notification` contains the suspended `Request`. + public static let didSuspendNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didSuspend") + /// Posted when a `Request` is cancelled. The `Notification` contains the cancelled `Request`. + public static let didCancelNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCancel") + /// Posted when a `Request` is finished. The `Notification` contains the completed `Request`. + public static let didFinishNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didFinish") + + /// Posted when a `URLSessionTask` is resumed. The `Notification` contains the `Request` associated with the `URLSessionTask`. + public static let didResumeTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didResumeTask") + /// Posted when a `URLSessionTask` is suspended. The `Notification` contains the `Request` associated with the `URLSessionTask`. + public static let didSuspendTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didSuspendTask") + /// Posted when a `URLSessionTask` is cancelled. The `Notification` contains the `Request` associated with the `URLSessionTask`. + public static let didCancelTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCancelTask") + /// Posted when a `URLSessionTask` is completed. The `Notification` contains the `Request` associated with the `URLSessionTask`. + public static let didCompleteTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCompleteTask") +} + +// MARK: - + +extension Notification { + /// The `Request` contained by the instance's `userInfo`, `nil` otherwise. + public var request: Request? { + userInfo?[String.requestKey] as? Request + } + + /// Convenience initializer for a `Notification` containing a `Request` payload. + /// + /// - Parameters: + /// - name: The name of the notification. + /// - request: The `Request` payload. + init(name: Notification.Name, request: Request) { + self.init(name: name, object: nil, userInfo: [String.requestKey: request]) + } +} + +extension NotificationCenter { + /// Convenience function for posting notifications with `Request` payloads. + /// + /// - Parameters: + /// - name: The name of the notification. + /// - request: The `Request` payload. + func postNotification(named name: Notification.Name, with request: Request) { + let notification = Notification(name: name, request: request) + post(notification) + } +} + +extension String { + /// User info dictionary key representing the `Request` associated with the notification. + fileprivate static let requestKey = "org.alamofire.notification.key.request" +} + +/// `EventMonitor` that provides Alamofire's notifications. +public final class AlamofireNotifications: EventMonitor { + public func requestDidResume(_ request: Request) { + NotificationCenter.default.postNotification(named: Request.didResumeNotification, with: request) + } + + public func requestDidSuspend(_ request: Request) { + NotificationCenter.default.postNotification(named: Request.didSuspendNotification, with: request) + } + + public func requestDidCancel(_ request: Request) { + NotificationCenter.default.postNotification(named: Request.didCancelNotification, with: request) + } + + public func requestDidFinish(_ request: Request) { + NotificationCenter.default.postNotification(named: Request.didFinishNotification, with: request) + } + + public func request(_ request: Request, didResumeTask task: URLSessionTask) { + NotificationCenter.default.postNotification(named: Request.didResumeTaskNotification, with: request) + } + + public func request(_ request: Request, didSuspendTask task: URLSessionTask) { + NotificationCenter.default.postNotification(named: Request.didSuspendTaskNotification, with: request) + } + + public func request(_ request: Request, didCancelTask task: URLSessionTask) { + NotificationCenter.default.postNotification(named: Request.didCancelTaskNotification, with: request) + } + + public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) { + NotificationCenter.default.postNotification(named: Request.didCompleteTaskNotification, with: request) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/OperationQueue+Alamofire.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/OperationQueue+Alamofire.swift new file mode 100644 index 0000000..b06a0cc --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/OperationQueue+Alamofire.swift @@ -0,0 +1,49 @@ +// +// OperationQueue+Alamofire.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension OperationQueue { + /// Creates an instance using the provided parameters. + /// + /// - Parameters: + /// - qualityOfService: `QualityOfService` to be applied to the queue. `.default` by default. + /// - maxConcurrentOperationCount: Maximum concurrent operations. + /// `OperationQueue.defaultMaxConcurrentOperationCount` by default. + /// - underlyingQueue: Underlying `DispatchQueue`. `nil` by default. + /// - name: Name for the queue. `nil` by default. + /// - startSuspended: Whether the queue starts suspended. `false` by default. + convenience init(qualityOfService: QualityOfService = .default, + maxConcurrentOperationCount: Int = OperationQueue.defaultMaxConcurrentOperationCount, + underlyingQueue: DispatchQueue? = nil, + name: String? = nil, + startSuspended: Bool = false) { + self.init() + self.qualityOfService = qualityOfService + self.maxConcurrentOperationCount = maxConcurrentOperationCount + self.underlyingQueue = underlyingQueue + self.name = name + isSuspended = startSuspended + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/ParameterEncoder.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/ParameterEncoder.swift new file mode 100644 index 0000000..2263660 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/ParameterEncoder.swift @@ -0,0 +1,217 @@ +// +// ParameterEncoder.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A type that can encode any `Encodable` type into a `URLRequest`. +public protocol ParameterEncoder { + /// Encode the provided `Encodable` parameters into `request`. + /// + /// - Parameters: + /// - parameters: The `Encodable` parameter value. + /// - request: The `URLRequest` into which to encode the parameters. + /// + /// - Returns: A `URLRequest` with the result of the encoding. + /// - Throws: An `Error` when encoding fails. For Alamofire provided encoders, this will be an instance of + /// `AFError.parameterEncoderFailed` with an associated `ParameterEncoderFailureReason`. + func encode(_ parameters: Parameters?, into request: URLRequest) throws -> URLRequest +} + +/// A `ParameterEncoder` that encodes types as JSON body data. +/// +/// If no `Content-Type` header is already set on the provided `URLRequest`s, it's set to `application/json`. +open class JSONParameterEncoder: ParameterEncoder { + /// Returns an encoder with default parameters. + public static var `default`: JSONParameterEncoder { JSONParameterEncoder() } + + /// Returns an encoder with `JSONEncoder.outputFormatting` set to `.prettyPrinted`. + public static var prettyPrinted: JSONParameterEncoder { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + return JSONParameterEncoder(encoder: encoder) + } + + /// Returns an encoder with `JSONEncoder.outputFormatting` set to `.sortedKeys`. + @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) + public static var sortedKeys: JSONParameterEncoder { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + + return JSONParameterEncoder(encoder: encoder) + } + + /// `JSONEncoder` used to encode parameters. + public let encoder: JSONEncoder + + /// Creates an instance with the provided `JSONEncoder`. + /// + /// - Parameter encoder: The `JSONEncoder`. `JSONEncoder()` by default. + public init(encoder: JSONEncoder = JSONEncoder()) { + self.encoder = encoder + } + + open func encode(_ parameters: Parameters?, + into request: URLRequest) throws -> URLRequest { + guard let parameters = parameters else { return request } + + var request = request + + do { + let data = try encoder.encode(parameters) + request.httpBody = data + if request.headers["Content-Type"] == nil { + request.headers.update(.contentType("application/json")) + } + } catch { + throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) + } + + return request + } +} + +#if swift(>=5.5) +extension ParameterEncoder where Self == JSONParameterEncoder { + /// Provides a default `JSONParameterEncoder` instance. + public static var json: JSONParameterEncoder { JSONParameterEncoder() } + + /// Creates a `JSONParameterEncoder` using the provided `JSONEncoder`. + /// + /// - Parameter encoder: `JSONEncoder` used to encode parameters. `JSONEncoder()` by default. + /// - Returns: The `JSONParameterEncoder`. + public static func json(encoder: JSONEncoder = JSONEncoder()) -> JSONParameterEncoder { + JSONParameterEncoder(encoder: encoder) + } +} +#endif + +/// A `ParameterEncoder` that encodes types as URL-encoded query strings to be set on the URL or as body data, depending +/// on the `Destination` set. +/// +/// If no `Content-Type` header is already set on the provided `URLRequest`s, it will be set to +/// `application/x-www-form-urlencoded; charset=utf-8`. +/// +/// Encoding behavior can be customized by passing an instance of `URLEncodedFormEncoder` to the initializer. +open class URLEncodedFormParameterEncoder: ParameterEncoder { + /// Defines where the URL-encoded string should be set for each `URLRequest`. + public enum Destination { + /// Applies the encoded query string to any existing query string for `.get`, `.head`, and `.delete` request. + /// Sets it to the `httpBody` for all other methods. + case methodDependent + /// Applies the encoded query string to any existing query string from the `URLRequest`. + case queryString + /// Applies the encoded query string to the `httpBody` of the `URLRequest`. + case httpBody + + /// Determines whether the URL-encoded string should be applied to the `URLRequest`'s `url`. + /// + /// - Parameter method: The `HTTPMethod`. + /// + /// - Returns: Whether the URL-encoded string should be applied to a `URL`. + func encodesParametersInURL(for method: HTTPMethod) -> Bool { + switch self { + case .methodDependent: return [.get, .head, .delete].contains(method) + case .queryString: return true + case .httpBody: return false + } + } + } + + /// Returns an encoder with default parameters. + public static var `default`: URLEncodedFormParameterEncoder { URLEncodedFormParameterEncoder() } + + /// The `URLEncodedFormEncoder` to use. + public let encoder: URLEncodedFormEncoder + + /// The `Destination` for the URL-encoded string. + public let destination: Destination + + /// Creates an instance with the provided `URLEncodedFormEncoder` instance and `Destination` value. + /// + /// - Parameters: + /// - encoder: The `URLEncodedFormEncoder`. `URLEncodedFormEncoder()` by default. + /// - destination: The `Destination`. `.methodDependent` by default. + public init(encoder: URLEncodedFormEncoder = URLEncodedFormEncoder(), destination: Destination = .methodDependent) { + self.encoder = encoder + self.destination = destination + } + + open func encode(_ parameters: Parameters?, + into request: URLRequest) throws -> URLRequest { + guard let parameters = parameters else { return request } + + var request = request + + guard let url = request.url else { + throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.url)) + } + + guard let method = request.method else { + let rawValue = request.method?.rawValue ?? "nil" + throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.httpMethod(rawValue: rawValue))) + } + + if destination.encodesParametersInURL(for: method), + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { + let query: String = try Result { try encoder.encode(parameters) } + .mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.get() + let newQueryString = [components.percentEncodedQuery, query].compactMap { $0 }.joinedWithAmpersands() + components.percentEncodedQuery = newQueryString.isEmpty ? nil : newQueryString + + guard let newURL = components.url else { + throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.url)) + } + + request.url = newURL + } else { + if request.headers["Content-Type"] == nil { + request.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8")) + } + + request.httpBody = try Result { try encoder.encode(parameters) } + .mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.get() + } + + return request + } +} + +#if swift(>=5.5) +extension ParameterEncoder where Self == URLEncodedFormParameterEncoder { + /// Provides a default `URLEncodedFormParameterEncoder` instance. + public static var urlEncodedForm: URLEncodedFormParameterEncoder { URLEncodedFormParameterEncoder() } + + /// Creates a `URLEncodedFormParameterEncoder` with the provided encoder and destination. + /// + /// - Parameters: + /// - encoder: `URLEncodedFormEncoder` used to encode the parameters. `URLEncodedFormEncoder()` by default. + /// - destination: `Destination` to which to encode the parameters. `.methodDependent` by default. + /// - Returns: The `URLEncodedFormParameterEncoder`. + public static func urlEncodedForm(encoder: URLEncodedFormEncoder = URLEncodedFormEncoder(), + destination: URLEncodedFormParameterEncoder.Destination = .methodDependent) -> URLEncodedFormParameterEncoder { + URLEncodedFormParameterEncoder(encoder: encoder, destination: destination) + } +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/ParameterEncoding.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/ParameterEncoding.swift new file mode 100644 index 0000000..e7fa452 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/ParameterEncoding.swift @@ -0,0 +1,321 @@ +// +// ParameterEncoding.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A dictionary of parameters to apply to a `URLRequest`. +public typealias Parameters = [String: Any] + +/// A type used to define how a set of parameters are applied to a `URLRequest`. +public protocol ParameterEncoding { + /// Creates a `URLRequest` by encoding parameters and applying them on the passed request. + /// + /// - Parameters: + /// - urlRequest: `URLRequestConvertible` value onto which parameters will be encoded. + /// - parameters: `Parameters` to encode onto the request. + /// + /// - Returns: The encoded `URLRequest`. + /// - Throws: Any `Error` produced during parameter encoding. + func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest +} + +// MARK: - + +/// Creates a url-encoded query string to be set as or appended to any existing URL query string or set as the HTTP +/// body of the URL request. Whether the query string is set or appended to any existing URL query string or set as +/// the HTTP body depends on the destination of the encoding. +/// +/// The `Content-Type` HTTP header field of an encoded request with HTTP body is set to +/// `application/x-www-form-urlencoded; charset=utf-8`. +/// +/// There is no published specification for how to encode collection types. By default the convention of appending +/// `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for +/// nested dictionary values (`foo[bar]=baz`) is used. Optionally, `ArrayEncoding` can be used to omit the +/// square brackets appended to array keys. +/// +/// `BoolEncoding` can be used to configure how boolean values are encoded. The default behavior is to encode +/// `true` as 1 and `false` as 0. +public struct URLEncoding: ParameterEncoding { + // MARK: Helper Types + + /// Defines whether the url-encoded query string is applied to the existing query string or HTTP body of the + /// resulting URL request. + public enum Destination { + /// Applies encoded query string result to existing query string for `GET`, `HEAD` and `DELETE` requests and + /// sets as the HTTP body for requests with any other HTTP method. + case methodDependent + /// Sets or appends encoded query string result to existing query string. + case queryString + /// Sets encoded query string result as the HTTP body of the URL request. + case httpBody + + func encodesParametersInURL(for method: HTTPMethod) -> Bool { + switch self { + case .methodDependent: return [.get, .head, .delete].contains(method) + case .queryString: return true + case .httpBody: return false + } + } + } + + /// Configures how `Array` parameters are encoded. + public enum ArrayEncoding { + /// An empty set of square brackets is appended to the key for every value. This is the default behavior. + case brackets + /// No brackets are appended. The key is encoded as is. + case noBrackets + /// Brackets containing the item index are appended. This matches the jQuery and Node.js behavior. + case indexInBrackets + + func encode(key: String, atIndex index: Int) -> String { + switch self { + case .brackets: + return "\(key)[]" + case .noBrackets: + return key + case .indexInBrackets: + return "\(key)[\(index)]" + } + } + } + + /// Configures how `Bool` parameters are encoded. + public enum BoolEncoding { + /// Encode `true` as `1` and `false` as `0`. This is the default behavior. + case numeric + /// Encode `true` and `false` as string literals. + case literal + + func encode(value: Bool) -> String { + switch self { + case .numeric: + return value ? "1" : "0" + case .literal: + return value ? "true" : "false" + } + } + } + + // MARK: Properties + + /// Returns a default `URLEncoding` instance with a `.methodDependent` destination. + public static var `default`: URLEncoding { URLEncoding() } + + /// Returns a `URLEncoding` instance with a `.queryString` destination. + public static var queryString: URLEncoding { URLEncoding(destination: .queryString) } + + /// Returns a `URLEncoding` instance with an `.httpBody` destination. + public static var httpBody: URLEncoding { URLEncoding(destination: .httpBody) } + + /// The destination defining where the encoded query string is to be applied to the URL request. + public let destination: Destination + + /// The encoding to use for `Array` parameters. + public let arrayEncoding: ArrayEncoding + + /// The encoding to use for `Bool` parameters. + public let boolEncoding: BoolEncoding + + // MARK: Initialization + + /// Creates an instance using the specified parameters. + /// + /// - Parameters: + /// - destination: `Destination` defining where the encoded query string will be applied. `.methodDependent` by + /// default. + /// - arrayEncoding: `ArrayEncoding` to use. `.brackets` by default. + /// - boolEncoding: `BoolEncoding` to use. `.numeric` by default. + public init(destination: Destination = .methodDependent, + arrayEncoding: ArrayEncoding = .brackets, + boolEncoding: BoolEncoding = .numeric) { + self.destination = destination + self.arrayEncoding = arrayEncoding + self.boolEncoding = boolEncoding + } + + // MARK: Encoding + + public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { + var urlRequest = try urlRequest.asURLRequest() + + guard let parameters = parameters else { return urlRequest } + + if let method = urlRequest.method, destination.encodesParametersInURL(for: method) { + guard let url = urlRequest.url else { + throw AFError.parameterEncodingFailed(reason: .missingURL) + } + + if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty { + let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters) + urlComponents.percentEncodedQuery = percentEncodedQuery + urlRequest.url = urlComponents.url + } + } else { + if urlRequest.headers["Content-Type"] == nil { + urlRequest.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8")) + } + + urlRequest.httpBody = Data(query(parameters).utf8) + } + + return urlRequest + } + + /// Creates a percent-escaped, URL encoded query string components from the given key-value pair recursively. + /// + /// - Parameters: + /// - key: Key of the query component. + /// - value: Value of the query component. + /// + /// - Returns: The percent-escaped, URL encoded query string components. + public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] { + var components: [(String, String)] = [] + switch value { + case let dictionary as [String: Any]: + for (nestedKey, value) in dictionary { + components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value) + } + case let array as [Any]: + for (index, value) in array.enumerated() { + components += queryComponents(fromKey: arrayEncoding.encode(key: key, atIndex: index), value: value) + } + case let number as NSNumber: + if number.isBool { + components.append((escape(key), escape(boolEncoding.encode(value: number.boolValue)))) + } else { + components.append((escape(key), escape("\(number)"))) + } + case let bool as Bool: + components.append((escape(key), escape(boolEncoding.encode(value: bool)))) + default: + components.append((escape(key), escape("\(value)"))) + } + return components + } + + /// Creates a percent-escaped string following RFC 3986 for a query string key or value. + /// + /// - Parameter string: `String` to be percent-escaped. + /// + /// - Returns: The percent-escaped `String`. + public func escape(_ string: String) -> String { + string.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? string + } + + private func query(_ parameters: [String: Any]) -> String { + var components: [(String, String)] = [] + + for key in parameters.keys.sorted(by: <) { + let value = parameters[key]! + components += queryComponents(fromKey: key, value: value) + } + return components.map { "\($0)=\($1)" }.joined(separator: "&") + } +} + +// MARK: - + +/// Uses `JSONSerialization` to create a JSON representation of the parameters object, which is set as the body of the +/// request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`. +public struct JSONEncoding: ParameterEncoding { + // MARK: Properties + + /// Returns a `JSONEncoding` instance with default writing options. + public static var `default`: JSONEncoding { JSONEncoding() } + + /// Returns a `JSONEncoding` instance with `.prettyPrinted` writing options. + public static var prettyPrinted: JSONEncoding { JSONEncoding(options: .prettyPrinted) } + + /// The options for writing the parameters as JSON data. + public let options: JSONSerialization.WritingOptions + + // MARK: Initialization + + /// Creates an instance using the specified `WritingOptions`. + /// + /// - Parameter options: `JSONSerialization.WritingOptions` to use. + public init(options: JSONSerialization.WritingOptions = []) { + self.options = options + } + + // MARK: Encoding + + public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { + var urlRequest = try urlRequest.asURLRequest() + + guard let parameters = parameters else { return urlRequest } + + do { + let data = try JSONSerialization.data(withJSONObject: parameters, options: options) + + if urlRequest.headers["Content-Type"] == nil { + urlRequest.headers.update(.contentType("application/json")) + } + + urlRequest.httpBody = data + } catch { + throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) + } + + return urlRequest + } + + /// Encodes any JSON compatible object into a `URLRequest`. + /// + /// - Parameters: + /// - urlRequest: `URLRequestConvertible` value into which the object will be encoded. + /// - jsonObject: `Any` value (must be JSON compatible` to be encoded into the `URLRequest`. `nil` by default. + /// + /// - Returns: The encoded `URLRequest`. + /// - Throws: Any `Error` produced during encoding. + public func encode(_ urlRequest: URLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest { + var urlRequest = try urlRequest.asURLRequest() + + guard let jsonObject = jsonObject else { return urlRequest } + + do { + let data = try JSONSerialization.data(withJSONObject: jsonObject, options: options) + + if urlRequest.headers["Content-Type"] == nil { + urlRequest.headers.update(.contentType("application/json")) + } + + urlRequest.httpBody = data + } catch { + throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) + } + + return urlRequest + } +} + +// MARK: - + +extension NSNumber { + fileprivate var isBool: Bool { + // Use Obj-C type encoding to check whether the underlying type is a `Bool`, as it's guaranteed as part of + // swift-corelibs-foundation, per [this discussion on the Swift forums](https://forums.swift.org/t/alamofire-on-linux-possible-but-not-release-ready/34553/22). + String(cString: objCType) == "c" + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/Protected.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Protected.swift new file mode 100644 index 0000000..2c056fa --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Protected.swift @@ -0,0 +1,161 @@ +// +// Protected.swift +// +// Copyright (c) 2014-2020 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +private protocol Lock { + func lock() + func unlock() +} + +extension Lock { + /// Executes a closure returning a value while acquiring the lock. + /// + /// - Parameter closure: The closure to run. + /// + /// - Returns: The value the closure generated. + func around(_ closure: () throws -> T) rethrows -> T { + lock(); defer { unlock() } + return try closure() + } + + /// Execute a closure while acquiring the lock. + /// + /// - Parameter closure: The closure to run. + func around(_ closure: () throws -> Void) rethrows { + lock(); defer { unlock() } + try closure() + } +} + +#if os(Linux) || os(Windows) + +extension NSLock: Lock {} + +#endif + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +/// An `os_unfair_lock` wrapper. +final class UnfairLock: Lock { + private let unfairLock: os_unfair_lock_t + + init() { + unfairLock = .allocate(capacity: 1) + unfairLock.initialize(to: os_unfair_lock()) + } + + deinit { + unfairLock.deinitialize(count: 1) + unfairLock.deallocate() + } + + fileprivate func lock() { + os_unfair_lock_lock(unfairLock) + } + + fileprivate func unlock() { + os_unfair_lock_unlock(unfairLock) + } +} +#endif + +/// A thread-safe wrapper around a value. +@propertyWrapper +@dynamicMemberLookup +final class Protected { + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + private let lock = UnfairLock() + #elseif os(Linux) || os(Windows) + private let lock = NSLock() + #endif + private var value: T + + init(_ value: T) { + self.value = value + } + + /// The contained value. Unsafe for anything more than direct read or write. + var wrappedValue: T { + get { lock.around { value } } + set { lock.around { value = newValue } } + } + + var projectedValue: Protected { self } + + init(wrappedValue: T) { + value = wrappedValue + } + + /// Synchronously read or transform the contained value. + /// + /// - Parameter closure: The closure to execute. + /// + /// - Returns: The return value of the closure passed. + func read(_ closure: (T) throws -> U) rethrows -> U { + try lock.around { try closure(self.value) } + } + + /// Synchronously modify the protected value. + /// + /// - Parameter closure: The closure to execute. + /// + /// - Returns: The modified value. + @discardableResult + func write(_ closure: (inout T) throws -> U) rethrows -> U { + try lock.around { try closure(&self.value) } + } + + subscript(dynamicMember keyPath: WritableKeyPath) -> Property { + get { lock.around { value[keyPath: keyPath] } } + set { lock.around { value[keyPath: keyPath] = newValue } } + } + + subscript(dynamicMember keyPath: KeyPath) -> Property { + lock.around { value[keyPath: keyPath] } + } +} + +extension Protected where T == Request.MutableState { + /// Attempts to transition to the passed `State`. + /// + /// - Parameter state: The `State` to attempt transition to. + /// + /// - Returns: Whether the transition occurred. + func attemptToTransitionTo(_ state: Request.State) -> Bool { + lock.around { + guard value.state.canTransitionTo(state) else { return false } + + value.state = state + + return true + } + } + + /// Perform a closure while locked with the provided `Request.State`. + /// + /// - Parameter perform: The closure to perform while locked. + func withState(perform: (Request.State) -> Void) { + lock.around { perform(value.state) } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/RedirectHandler.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/RedirectHandler.swift new file mode 100644 index 0000000..5c232b8 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/RedirectHandler.swift @@ -0,0 +1,113 @@ +// +// RedirectHandler.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A type that handles how an HTTP redirect response from a remote server should be redirected to the new request. +public protocol RedirectHandler { + /// Determines how the HTTP redirect response should be redirected to the new request. + /// + /// The `completion` closure should be passed one of three possible options: + /// + /// 1. The new request specified by the redirect (this is the most common use case). + /// 2. A modified version of the new request (you may want to route it somewhere else). + /// 3. A `nil` value to deny the redirect request and return the body of the redirect response. + /// + /// - Parameters: + /// - task: The `URLSessionTask` whose request resulted in a redirect. + /// - request: The `URLRequest` to the new location specified by the redirect response. + /// - response: The `HTTPURLResponse` containing the server's response to the original request. + /// - completion: The closure to execute containing the new `URLRequest`, a modified `URLRequest`, or `nil`. + func task(_ task: URLSessionTask, + willBeRedirectedTo request: URLRequest, + for response: HTTPURLResponse, + completion: @escaping (URLRequest?) -> Void) +} + +// MARK: - + +/// `Redirector` is a convenience `RedirectHandler` making it easy to follow, not follow, or modify a redirect. +public struct Redirector { + /// Defines the behavior of the `Redirector` type. + public enum Behavior { + /// Follow the redirect as defined in the response. + case follow + /// Do not follow the redirect defined in the response. + case doNotFollow + /// Modify the redirect request defined in the response. + case modify((URLSessionTask, URLRequest, HTTPURLResponse) -> URLRequest?) + } + + /// Returns a `Redirector` with a `.follow` `Behavior`. + public static let follow = Redirector(behavior: .follow) + /// Returns a `Redirector` with a `.doNotFollow` `Behavior`. + public static let doNotFollow = Redirector(behavior: .doNotFollow) + + /// The `Behavior` of the `Redirector`. + public let behavior: Behavior + + /// Creates a `Redirector` instance from the `Behavior`. + /// + /// - Parameter behavior: The `Behavior`. + public init(behavior: Behavior) { + self.behavior = behavior + } +} + +// MARK: - + +extension Redirector: RedirectHandler { + public func task(_ task: URLSessionTask, + willBeRedirectedTo request: URLRequest, + for response: HTTPURLResponse, + completion: @escaping (URLRequest?) -> Void) { + switch behavior { + case .follow: + completion(request) + case .doNotFollow: + completion(nil) + case let .modify(closure): + let request = closure(task, request, response) + completion(request) + } + } +} + +#if swift(>=5.5) +extension RedirectHandler where Self == Redirector { + /// Provides a `Redirector` which follows redirects. Equivalent to `Redirector.follow`. + public static var follow: Redirector { .follow } + + /// Provides a `Redirector` which does not follow redirects. Equivalent to `Redirector.doNotFollow`. + public static var doNotFollow: Redirector { .doNotFollow } + + /// Creates a `Redirector` which modifies the redirected `URLRequest` using the provided closure. + /// + /// - Parameter closure: Closure used to modify the redirect. + /// - Returns: The `Redirector`. + public static func modify(using closure: @escaping (URLSessionTask, URLRequest, HTTPURLResponse) -> URLRequest?) -> Redirector { + Redirector(behavior: .modify(closure)) + } +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/Request.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Request.swift new file mode 100644 index 0000000..7c6e5dc --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Request.swift @@ -0,0 +1,1912 @@ +// +// Request.swift +// +// Copyright (c) 2014-2020 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// `Request` is the common superclass of all Alamofire request types and provides common state, delegate, and callback +/// handling. +public class Request { + /// State of the `Request`, with managed transitions between states set when calling `resume()`, `suspend()`, or + /// `cancel()` on the `Request`. + public enum State { + /// Initial state of the `Request`. + case initialized + /// `State` set when `resume()` is called. Any tasks created for the `Request` will have `resume()` called on + /// them in this state. + case resumed + /// `State` set when `suspend()` is called. Any tasks created for the `Request` will have `suspend()` called on + /// them in this state. + case suspended + /// `State` set when `cancel()` is called. Any tasks created for the `Request` will have `cancel()` called on + /// them. Unlike `resumed` or `suspended`, once in the `cancelled` state, the `Request` can no longer transition + /// to any other state. + case cancelled + /// `State` set when all response serialization completion closures have been cleared on the `Request` and + /// enqueued on their respective queues. + case finished + + /// Determines whether `self` can be transitioned to the provided `State`. + func canTransitionTo(_ state: State) -> Bool { + switch (self, state) { + case (.initialized, _): + return true + case (_, .initialized), (.cancelled, _), (.finished, _): + return false + case (.resumed, .cancelled), (.suspended, .cancelled), (.resumed, .suspended), (.suspended, .resumed): + return true + case (.suspended, .suspended), (.resumed, .resumed): + return false + case (_, .finished): + return true + } + } + } + + // MARK: - Initial State + + /// `UUID` providing a unique identifier for the `Request`, used in the `Hashable` and `Equatable` conformances. + public let id: UUID + /// The serial queue for all internal async actions. + public let underlyingQueue: DispatchQueue + /// The queue used for all serialization actions. By default it's a serial queue that targets `underlyingQueue`. + public let serializationQueue: DispatchQueue + /// `EventMonitor` used for event callbacks. + public let eventMonitor: EventMonitor? + /// The `Request`'s interceptor. + public let interceptor: RequestInterceptor? + /// The `Request`'s delegate. + public private(set) weak var delegate: RequestDelegate? + + // MARK: - Mutable State + + /// Type encapsulating all mutable state that may need to be accessed from anything other than the `underlyingQueue`. + struct MutableState { + /// State of the `Request`. + var state: State = .initialized + /// `ProgressHandler` and `DispatchQueue` provided for upload progress callbacks. + var uploadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? + /// `ProgressHandler` and `DispatchQueue` provided for download progress callbacks. + var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? + /// `RedirectHandler` provided for to handle request redirection. + var redirectHandler: RedirectHandler? + /// `CachedResponseHandler` provided to handle response caching. + var cachedResponseHandler: CachedResponseHandler? + /// Queue and closure called when the `Request` is able to create a cURL description of itself. + var cURLHandler: (queue: DispatchQueue, handler: (String) -> Void)? + /// Queue and closure called when the `Request` creates a `URLRequest`. + var urlRequestHandler: (queue: DispatchQueue, handler: (URLRequest) -> Void)? + /// Queue and closure called when the `Request` creates a `URLSessionTask`. + var urlSessionTaskHandler: (queue: DispatchQueue, handler: (URLSessionTask) -> Void)? + /// Response serialization closures that handle response parsing. + var responseSerializers: [() -> Void] = [] + /// Response serialization completion closures executed once all response serializers are complete. + var responseSerializerCompletions: [() -> Void] = [] + /// Whether response serializer processing is finished. + var responseSerializerProcessingFinished = false + /// `URLCredential` used for authentication challenges. + var credential: URLCredential? + /// All `URLRequest`s created by Alamofire on behalf of the `Request`. + var requests: [URLRequest] = [] + /// All `URLSessionTask`s created by Alamofire on behalf of the `Request`. + var tasks: [URLSessionTask] = [] + /// All `URLSessionTaskMetrics` values gathered by Alamofire on behalf of the `Request`. Should correspond + /// exactly the the `tasks` created. + var metrics: [URLSessionTaskMetrics] = [] + /// Number of times any retriers provided retried the `Request`. + var retryCount = 0 + /// Final `AFError` for the `Request`, whether from various internal Alamofire calls or as a result of a `task`. + var error: AFError? + /// Whether the instance has had `finish()` called and is running the serializers. Should be replaced with a + /// representation in the state machine in the future. + var isFinishing = false + /// Actions to run when requests are finished. Use for concurrency support. + var finishHandlers: [() -> Void] = [] + } + + /// Protected `MutableState` value that provides thread-safe access to state values. + @Protected + fileprivate var mutableState = MutableState() + + /// `State` of the `Request`. + public var state: State { $mutableState.state } + /// Returns whether `state` is `.initialized`. + public var isInitialized: Bool { state == .initialized } + /// Returns whether `state is `.resumed`. + public var isResumed: Bool { state == .resumed } + /// Returns whether `state` is `.suspended`. + public var isSuspended: Bool { state == .suspended } + /// Returns whether `state` is `.cancelled`. + public var isCancelled: Bool { state == .cancelled } + /// Returns whether `state` is `.finished`. + public var isFinished: Bool { state == .finished } + + // MARK: Progress + + /// Closure type executed when monitoring the upload or download progress of a request. + public typealias ProgressHandler = (Progress) -> Void + + /// `Progress` of the upload of the body of the executed `URLRequest`. Reset to `0` if the `Request` is retried. + public let uploadProgress = Progress(totalUnitCount: 0) + /// `Progress` of the download of any response data. Reset to `0` if the `Request` is retried. + public let downloadProgress = Progress(totalUnitCount: 0) + /// `ProgressHandler` called when `uploadProgress` is updated, on the provided `DispatchQueue`. + private var uploadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? { + get { $mutableState.uploadProgressHandler } + set { $mutableState.uploadProgressHandler = newValue } + } + + /// `ProgressHandler` called when `downloadProgress` is updated, on the provided `DispatchQueue`. + fileprivate var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? { + get { $mutableState.downloadProgressHandler } + set { $mutableState.downloadProgressHandler = newValue } + } + + // MARK: Redirect Handling + + /// `RedirectHandler` set on the instance. + public private(set) var redirectHandler: RedirectHandler? { + get { $mutableState.redirectHandler } + set { $mutableState.redirectHandler = newValue } + } + + // MARK: Cached Response Handling + + /// `CachedResponseHandler` set on the instance. + public private(set) var cachedResponseHandler: CachedResponseHandler? { + get { $mutableState.cachedResponseHandler } + set { $mutableState.cachedResponseHandler = newValue } + } + + // MARK: URLCredential + + /// `URLCredential` used for authentication challenges. Created by calling one of the `authenticate` methods. + public private(set) var credential: URLCredential? { + get { $mutableState.credential } + set { $mutableState.credential = newValue } + } + + // MARK: Validators + + /// `Validator` callback closures that store the validation calls enqueued. + @Protected + fileprivate var validators: [() -> Void] = [] + + // MARK: URLRequests + + /// All `URLRequests` created on behalf of the `Request`, including original and adapted requests. + public var requests: [URLRequest] { $mutableState.requests } + /// First `URLRequest` created on behalf of the `Request`. May not be the first one actually executed. + public var firstRequest: URLRequest? { requests.first } + /// Last `URLRequest` created on behalf of the `Request`. + public var lastRequest: URLRequest? { requests.last } + /// Current `URLRequest` created on behalf of the `Request`. + public var request: URLRequest? { lastRequest } + + /// `URLRequest`s from all of the `URLSessionTask`s executed on behalf of the `Request`. May be different from + /// `requests` due to `URLSession` manipulation. + public var performedRequests: [URLRequest] { $mutableState.read { $0.tasks.compactMap(\.currentRequest) } } + + // MARK: HTTPURLResponse + + /// `HTTPURLResponse` received from the server, if any. If the `Request` was retried, this is the response of the + /// last `URLSessionTask`. + public var response: HTTPURLResponse? { lastTask?.response as? HTTPURLResponse } + + // MARK: Tasks + + /// All `URLSessionTask`s created on behalf of the `Request`. + public var tasks: [URLSessionTask] { $mutableState.tasks } + /// First `URLSessionTask` created on behalf of the `Request`. + public var firstTask: URLSessionTask? { tasks.first } + /// Last `URLSessionTask` crated on behalf of the `Request`. + public var lastTask: URLSessionTask? { tasks.last } + /// Current `URLSessionTask` created on behalf of the `Request`. + public var task: URLSessionTask? { lastTask } + + // MARK: Metrics + + /// All `URLSessionTaskMetrics` gathered on behalf of the `Request`. Should correspond to the `tasks` created. + public var allMetrics: [URLSessionTaskMetrics] { $mutableState.metrics } + /// First `URLSessionTaskMetrics` gathered on behalf of the `Request`. + public var firstMetrics: URLSessionTaskMetrics? { allMetrics.first } + /// Last `URLSessionTaskMetrics` gathered on behalf of the `Request`. + public var lastMetrics: URLSessionTaskMetrics? { allMetrics.last } + /// Current `URLSessionTaskMetrics` gathered on behalf of the `Request`. + public var metrics: URLSessionTaskMetrics? { lastMetrics } + + // MARK: Retry Count + + /// Number of times the `Request` has been retried. + public var retryCount: Int { $mutableState.retryCount } + + // MARK: Error + + /// `Error` returned from Alamofire internally, from the network request directly, or any validators executed. + public fileprivate(set) var error: AFError? { + get { $mutableState.error } + set { $mutableState.error = newValue } + } + + /// Default initializer for the `Request` superclass. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. + init(id: UUID = UUID(), + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + delegate: RequestDelegate) { + self.id = id + self.underlyingQueue = underlyingQueue + self.serializationQueue = serializationQueue + self.eventMonitor = eventMonitor + self.interceptor = interceptor + self.delegate = delegate + } + + // MARK: - Internal Event API + + // All API must be called from underlyingQueue. + + /// Called when an initial `URLRequest` has been created on behalf of the instance. If a `RequestAdapter` is active, + /// the `URLRequest` will be adapted before being issued. + /// + /// - Parameter request: The `URLRequest` created. + func didCreateInitialURLRequest(_ request: URLRequest) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + $mutableState.write { $0.requests.append(request) } + + eventMonitor?.request(self, didCreateInitialURLRequest: request) + } + + /// Called when initial `URLRequest` creation has failed, typically through a `URLRequestConvertible`. + /// + /// - Note: Triggers retry. + /// + /// - Parameter error: `AFError` thrown from the failed creation. + func didFailToCreateURLRequest(with error: AFError) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + self.error = error + + eventMonitor?.request(self, didFailToCreateURLRequestWithError: error) + + callCURLHandlerIfNecessary() + + retryOrFinish(error: error) + } + + /// Called when a `RequestAdapter` has successfully adapted a `URLRequest`. + /// + /// - Parameters: + /// - initialRequest: The `URLRequest` that was adapted. + /// - adaptedRequest: The `URLRequest` returned by the `RequestAdapter`. + func didAdaptInitialRequest(_ initialRequest: URLRequest, to adaptedRequest: URLRequest) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + $mutableState.write { $0.requests.append(adaptedRequest) } + + eventMonitor?.request(self, didAdaptInitialRequest: initialRequest, to: adaptedRequest) + } + + /// Called when a `RequestAdapter` fails to adapt a `URLRequest`. + /// + /// - Note: Triggers retry. + /// + /// - Parameters: + /// - request: The `URLRequest` the adapter was called with. + /// - error: The `AFError` returned by the `RequestAdapter`. + func didFailToAdaptURLRequest(_ request: URLRequest, withError error: AFError) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + self.error = error + + eventMonitor?.request(self, didFailToAdaptURLRequest: request, withError: error) + + callCURLHandlerIfNecessary() + + retryOrFinish(error: error) + } + + /// Final `URLRequest` has been created for the instance. + /// + /// - Parameter request: The `URLRequest` created. + func didCreateURLRequest(_ request: URLRequest) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + $mutableState.read { state in + state.urlRequestHandler?.queue.async { state.urlRequestHandler?.handler(request) } + } + + eventMonitor?.request(self, didCreateURLRequest: request) + + callCURLHandlerIfNecessary() + } + + /// Asynchronously calls any stored `cURLHandler` and then removes it from `mutableState`. + private func callCURLHandlerIfNecessary() { + $mutableState.write { mutableState in + guard let cURLHandler = mutableState.cURLHandler else { return } + + cURLHandler.queue.async { cURLHandler.handler(self.cURLDescription()) } + + mutableState.cURLHandler = nil + } + } + + /// Called when a `URLSessionTask` is created on behalf of the instance. + /// + /// - Parameter task: The `URLSessionTask` created. + func didCreateTask(_ task: URLSessionTask) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + $mutableState.write { state in + state.tasks.append(task) + + guard let urlSessionTaskHandler = state.urlSessionTaskHandler else { return } + + urlSessionTaskHandler.queue.async { urlSessionTaskHandler.handler(task) } + } + + eventMonitor?.request(self, didCreateTask: task) + } + + /// Called when resumption is completed. + func didResume() { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.requestDidResume(self) + } + + /// Called when a `URLSessionTask` is resumed on behalf of the instance. + /// + /// - Parameter task: The `URLSessionTask` resumed. + func didResumeTask(_ task: URLSessionTask) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.request(self, didResumeTask: task) + } + + /// Called when suspension is completed. + func didSuspend() { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.requestDidSuspend(self) + } + + /// Called when a `URLSessionTask` is suspended on behalf of the instance. + /// + /// - Parameter task: The `URLSessionTask` suspended. + func didSuspendTask(_ task: URLSessionTask) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.request(self, didSuspendTask: task) + } + + /// Called when cancellation is completed, sets `error` to `AFError.explicitlyCancelled`. + func didCancel() { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + error = error ?? AFError.explicitlyCancelled + + eventMonitor?.requestDidCancel(self) + } + + /// Called when a `URLSessionTask` is cancelled on behalf of the instance. + /// + /// - Parameter task: The `URLSessionTask` cancelled. + func didCancelTask(_ task: URLSessionTask) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + eventMonitor?.request(self, didCancelTask: task) + } + + /// Called when a `URLSessionTaskMetrics` value is gathered on behalf of the instance. + /// + /// - Parameter metrics: The `URLSessionTaskMetrics` gathered. + func didGatherMetrics(_ metrics: URLSessionTaskMetrics) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + $mutableState.write { $0.metrics.append(metrics) } + + eventMonitor?.request(self, didGatherMetrics: metrics) + } + + /// Called when a `URLSessionTask` fails before it is finished, typically during certificate pinning. + /// + /// - Parameters: + /// - task: The `URLSessionTask` which failed. + /// - error: The early failure `AFError`. + func didFailTask(_ task: URLSessionTask, earlyWithError error: AFError) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + self.error = error + + // Task will still complete, so didCompleteTask(_:with:) will handle retry. + eventMonitor?.request(self, didFailTask: task, earlyWithError: error) + } + + /// Called when a `URLSessionTask` completes. All tasks will eventually call this method. + /// + /// - Note: Response validation is synchronously triggered in this step. + /// + /// - Parameters: + /// - task: The `URLSessionTask` which completed. + /// - error: The `AFError` `task` may have completed with. If `error` has already been set on the instance, this + /// value is ignored. + func didCompleteTask(_ task: URLSessionTask, with error: AFError?) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + self.error = self.error ?? error + + validators.forEach { $0() } + + eventMonitor?.request(self, didCompleteTask: task, with: error) + + retryOrFinish(error: self.error) + } + + /// Called when the `RequestDelegate` is going to retry this `Request`. Calls `reset()`. + func prepareForRetry() { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + $mutableState.write { $0.retryCount += 1 } + + reset() + + eventMonitor?.requestIsRetrying(self) + } + + /// Called to determine whether retry will be triggered for the particular error, or whether the instance should + /// call `finish()`. + /// + /// - Parameter error: The possible `AFError` which may trigger retry. + func retryOrFinish(error: AFError?) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + guard let error = error, let delegate = delegate else { finish(); return } + + delegate.retryResult(for: self, dueTo: error) { retryResult in + switch retryResult { + case .doNotRetry: + self.finish() + case let .doNotRetryWithError(retryError): + self.finish(error: retryError.asAFError(orFailWith: "Received retryError was not already AFError")) + case .retry, .retryWithDelay: + delegate.retryRequest(self, withDelay: retryResult.delay) + } + } + } + + /// Finishes this `Request` and starts the response serializers. + /// + /// - Parameter error: The possible `Error` with which the instance will finish. + func finish(error: AFError? = nil) { + dispatchPrecondition(condition: .onQueue(underlyingQueue)) + + guard !$mutableState.isFinishing else { return } + + $mutableState.isFinishing = true + + if let error = error { self.error = error } + + // Start response handlers + processNextResponseSerializer() + + eventMonitor?.requestDidFinish(self) + } + + /// Appends the response serialization closure to the instance. + /// + /// - Note: This method will also `resume` the instance if `delegate.startImmediately` returns `true`. + /// + /// - Parameter closure: The closure containing the response serialization call. + func appendResponseSerializer(_ closure: @escaping () -> Void) { + $mutableState.write { mutableState in + mutableState.responseSerializers.append(closure) + + if mutableState.state == .finished { + mutableState.state = .resumed + } + + if mutableState.responseSerializerProcessingFinished { + underlyingQueue.async { self.processNextResponseSerializer() } + } + + if mutableState.state.canTransitionTo(.resumed) { + underlyingQueue.async { if self.delegate?.startImmediately == true { self.resume() } } + } + } + } + + /// Returns the next response serializer closure to execute if there's one left. + /// + /// - Returns: The next response serialization closure, if there is one. + func nextResponseSerializer() -> (() -> Void)? { + var responseSerializer: (() -> Void)? + + $mutableState.write { mutableState in + let responseSerializerIndex = mutableState.responseSerializerCompletions.count + + if responseSerializerIndex < mutableState.responseSerializers.count { + responseSerializer = mutableState.responseSerializers[responseSerializerIndex] + } + } + + return responseSerializer + } + + /// Processes the next response serializer and calls all completions if response serialization is complete. + func processNextResponseSerializer() { + guard let responseSerializer = nextResponseSerializer() else { + // Execute all response serializer completions and clear them + var completions: [() -> Void] = [] + + $mutableState.write { mutableState in + completions = mutableState.responseSerializerCompletions + + // Clear out all response serializers and response serializer completions in mutable state since the + // request is complete. It's important to do this prior to calling the completion closures in case + // the completions call back into the request triggering a re-processing of the response serializers. + // An example of how this can happen is by calling cancel inside a response completion closure. + mutableState.responseSerializers.removeAll() + mutableState.responseSerializerCompletions.removeAll() + + if mutableState.state.canTransitionTo(.finished) { + mutableState.state = .finished + } + + mutableState.responseSerializerProcessingFinished = true + mutableState.isFinishing = false + } + + completions.forEach { $0() } + + // Cleanup the request + cleanup() + + return + } + + serializationQueue.async { responseSerializer() } + } + + /// Notifies the `Request` that the response serializer is complete. + /// + /// - Parameter completion: The completion handler provided with the response serializer, called when all serializers + /// are complete. + func responseSerializerDidComplete(completion: @escaping () -> Void) { + $mutableState.write { $0.responseSerializerCompletions.append(completion) } + processNextResponseSerializer() + } + + /// Resets all task and response serializer related state for retry. + func reset() { + error = nil + + uploadProgress.totalUnitCount = 0 + uploadProgress.completedUnitCount = 0 + downloadProgress.totalUnitCount = 0 + downloadProgress.completedUnitCount = 0 + + $mutableState.write { state in + state.isFinishing = false + state.responseSerializerCompletions = [] + } + } + + /// Called when updating the upload progress. + /// + /// - Parameters: + /// - totalBytesSent: Total bytes sent so far. + /// - totalBytesExpectedToSend: Total bytes expected to send. + func updateUploadProgress(totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + uploadProgress.totalUnitCount = totalBytesExpectedToSend + uploadProgress.completedUnitCount = totalBytesSent + + uploadProgressHandler?.queue.async { self.uploadProgressHandler?.handler(self.uploadProgress) } + } + + /// Perform a closure on the current `state` while locked. + /// + /// - Parameter perform: The closure to perform. + func withState(perform: (State) -> Void) { + $mutableState.withState(perform: perform) + } + + // MARK: Task Creation + + /// Called when creating a `URLSessionTask` for this `Request`. Subclasses must override. + /// + /// - Parameters: + /// - request: `URLRequest` to use to create the `URLSessionTask`. + /// - session: `URLSession` which creates the `URLSessionTask`. + /// + /// - Returns: The `URLSessionTask` created. + func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + fatalError("Subclasses must override.") + } + + // MARK: - Public API + + // These APIs are callable from any queue. + + // MARK: State + + /// Cancels the instance. Once cancelled, a `Request` can no longer be resumed or suspended. + /// + /// - Returns: The instance. + @discardableResult + public func cancel() -> Self { + $mutableState.write { mutableState in + guard mutableState.state.canTransitionTo(.cancelled) else { return } + + mutableState.state = .cancelled + + underlyingQueue.async { self.didCancel() } + + guard let task = mutableState.tasks.last, task.state != .completed else { + underlyingQueue.async { self.finish() } + return + } + + // Resume to ensure metrics are gathered. + task.resume() + task.cancel() + underlyingQueue.async { self.didCancelTask(task) } + } + + return self + } + + /// Suspends the instance. + /// + /// - Returns: The instance. + @discardableResult + public func suspend() -> Self { + $mutableState.write { mutableState in + guard mutableState.state.canTransitionTo(.suspended) else { return } + + mutableState.state = .suspended + + underlyingQueue.async { self.didSuspend() } + + guard let task = mutableState.tasks.last, task.state != .completed else { return } + + task.suspend() + underlyingQueue.async { self.didSuspendTask(task) } + } + + return self + } + + /// Resumes the instance. + /// + /// - Returns: The instance. + @discardableResult + public func resume() -> Self { + $mutableState.write { mutableState in + guard mutableState.state.canTransitionTo(.resumed) else { return } + + mutableState.state = .resumed + + underlyingQueue.async { self.didResume() } + + guard let task = mutableState.tasks.last, task.state != .completed else { return } + + task.resume() + underlyingQueue.async { self.didResumeTask(task) } + } + + return self + } + + // MARK: - Closure API + + /// Associates a credential using the provided values with the instance. + /// + /// - Parameters: + /// - username: The username. + /// - password: The password. + /// - persistence: The `URLCredential.Persistence` for the created `URLCredential`. `.forSession` by default. + /// + /// - Returns: The instance. + @discardableResult + public func authenticate(username: String, password: String, persistence: URLCredential.Persistence = .forSession) -> Self { + let credential = URLCredential(user: username, password: password, persistence: persistence) + + return authenticate(with: credential) + } + + /// Associates the provided credential with the instance. + /// + /// - Parameter credential: The `URLCredential`. + /// + /// - Returns: The instance. + @discardableResult + public func authenticate(with credential: URLCredential) -> Self { + $mutableState.credential = credential + + return self + } + + /// Sets a closure to be called periodically during the lifecycle of the instance as data is read from the server. + /// + /// - Note: Only the last closure provided is used. + /// + /// - Parameters: + /// - queue: The `DispatchQueue` to execute the closure on. `.main` by default. + /// - closure: The closure to be executed periodically as data is read from the server. + /// + /// - Returns: The instance. + @discardableResult + public func downloadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self { + $mutableState.downloadProgressHandler = (handler: closure, queue: queue) + + return self + } + + /// Sets a closure to be called periodically during the lifecycle of the instance as data is sent to the server. + /// + /// - Note: Only the last closure provided is used. + /// + /// - Parameters: + /// - queue: The `DispatchQueue` to execute the closure on. `.main` by default. + /// - closure: The closure to be executed periodically as data is sent to the server. + /// + /// - Returns: The instance. + @discardableResult + public func uploadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self { + $mutableState.uploadProgressHandler = (handler: closure, queue: queue) + + return self + } + + // MARK: Redirects + + /// Sets the redirect handler for the instance which will be used if a redirect response is encountered. + /// + /// - Note: Attempting to set the redirect handler more than once is a logic error and will crash. + /// + /// - Parameter handler: The `RedirectHandler`. + /// + /// - Returns: The instance. + @discardableResult + public func redirect(using handler: RedirectHandler) -> Self { + $mutableState.write { mutableState in + precondition(mutableState.redirectHandler == nil, "Redirect handler has already been set.") + mutableState.redirectHandler = handler + } + + return self + } + + // MARK: Cached Responses + + /// Sets the cached response handler for the `Request` which will be used when attempting to cache a response. + /// + /// - Note: Attempting to set the cache handler more than once is a logic error and will crash. + /// + /// - Parameter handler: The `CachedResponseHandler`. + /// + /// - Returns: The instance. + @discardableResult + public func cacheResponse(using handler: CachedResponseHandler) -> Self { + $mutableState.write { mutableState in + precondition(mutableState.cachedResponseHandler == nil, "Cached response handler has already been set.") + mutableState.cachedResponseHandler = handler + } + + return self + } + + // MARK: - Lifetime APIs + + /// Sets a handler to be called when the cURL description of the request is available. + /// + /// - Note: When waiting for a `Request`'s `URLRequest` to be created, only the last `handler` will be called. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which `handler` will be called. + /// - handler: Closure to be called when the cURL description is available. + /// + /// - Returns: The instance. + @discardableResult + public func cURLDescription(on queue: DispatchQueue, calling handler: @escaping (String) -> Void) -> Self { + $mutableState.write { mutableState in + if mutableState.requests.last != nil { + queue.async { handler(self.cURLDescription()) } + } else { + mutableState.cURLHandler = (queue, handler) + } + } + + return self + } + + /// Sets a handler to be called when the cURL description of the request is available. + /// + /// - Note: When waiting for a `Request`'s `URLRequest` to be created, only the last `handler` will be called. + /// + /// - Parameter handler: Closure to be called when the cURL description is available. Called on the instance's + /// `underlyingQueue` by default. + /// + /// - Returns: The instance. + @discardableResult + public func cURLDescription(calling handler: @escaping (String) -> Void) -> Self { + $mutableState.write { mutableState in + if mutableState.requests.last != nil { + underlyingQueue.async { handler(self.cURLDescription()) } + } else { + mutableState.cURLHandler = (underlyingQueue, handler) + } + } + + return self + } + + /// Sets a closure to called whenever Alamofire creates a `URLRequest` for this instance. + /// + /// - Note: This closure will be called multiple times if the instance adapts incoming `URLRequest`s or is retried. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which `handler` will be called. `.main` by default. + /// - handler: Closure to be called when a `URLRequest` is available. + /// + /// - Returns: The instance. + @discardableResult + public func onURLRequestCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLRequest) -> Void) -> Self { + $mutableState.write { state in + if let request = state.requests.last { + queue.async { handler(request) } + } + + state.urlRequestHandler = (queue, handler) + } + + return self + } + + /// Sets a closure to be called whenever the instance creates a `URLSessionTask`. + /// + /// - Note: This API should only be used to provide `URLSessionTask`s to existing API, like `NSFileProvider`. It + /// **SHOULD NOT** be used to interact with tasks directly, as that may be break Alamofire features. + /// Additionally, this closure may be called multiple times if the instance is retried. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which `handler` will be called. `.main` by default. + /// - handler: Closure to be called when the `URLSessionTask` is available. + /// + /// - Returns: The instance. + @discardableResult + public func onURLSessionTaskCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLSessionTask) -> Void) -> Self { + $mutableState.write { state in + if let task = state.tasks.last { + queue.async { handler(task) } + } + + state.urlSessionTaskHandler = (queue, handler) + } + + return self + } + + // MARK: Cleanup + + /// Adds a `finishHandler` closure to be called when the request completes. + /// + /// - Parameter closure: Closure to be called when the request finishes. + func onFinish(perform finishHandler: @escaping () -> Void) { + guard !isFinished else { finishHandler(); return } + + $mutableState.write { state in + state.finishHandlers.append(finishHandler) + } + } + + /// Final cleanup step executed when the instance finishes response serialization. + func cleanup() { + delegate?.cleanup(after: self) + let handlers = $mutableState.finishHandlers + handlers.forEach { $0() } + $mutableState.write { state in + state.finishHandlers.removeAll() + } + } +} + +// MARK: - Protocol Conformances + +extension Request: Equatable { + public static func ==(lhs: Request, rhs: Request) -> Bool { + lhs.id == rhs.id + } +} + +extension Request: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension Request: CustomStringConvertible { + /// A textual representation of this instance, including the `HTTPMethod` and `URL` if the `URLRequest` has been + /// created, as well as the response status code, if a response has been received. + public var description: String { + guard let request = performedRequests.last ?? lastRequest, + let url = request.url, + let method = request.httpMethod else { return "No request created yet." } + + let requestDescription = "\(method) \(url.absoluteString)" + + return response.map { "\(requestDescription) (\($0.statusCode))" } ?? requestDescription + } +} + +extension Request { + /// cURL representation of the instance. + /// + /// - Returns: The cURL equivalent of the instance. + public func cURLDescription() -> String { + guard + let request = lastRequest, + let url = request.url, + let host = url.host, + let method = request.httpMethod else { return "$ curl command could not be created" } + + var components = ["$ curl -v"] + + components.append("-X \(method)") + + if let credentialStorage = delegate?.sessionConfiguration.urlCredentialStorage { + let protectionSpace = URLProtectionSpace(host: host, + port: url.port ?? 0, + protocol: url.scheme, + realm: host, + authenticationMethod: NSURLAuthenticationMethodHTTPBasic) + + if let credentials = credentialStorage.credentials(for: protectionSpace)?.values { + for credential in credentials { + guard let user = credential.user, let password = credential.password else { continue } + components.append("-u \(user):\(password)") + } + } else { + if let credential = credential, let user = credential.user, let password = credential.password { + components.append("-u \(user):\(password)") + } + } + } + + if let configuration = delegate?.sessionConfiguration, configuration.httpShouldSetCookies { + if + let cookieStorage = configuration.httpCookieStorage, + let cookies = cookieStorage.cookies(for: url), !cookies.isEmpty { + let allCookies = cookies.map { "\($0.name)=\($0.value)" }.joined(separator: ";") + + components.append("-b \"\(allCookies)\"") + } + } + + var headers = HTTPHeaders() + + if let sessionHeaders = delegate?.sessionConfiguration.headers { + for header in sessionHeaders where header.name != "Cookie" { + headers[header.name] = header.value + } + } + + for header in request.headers where header.name != "Cookie" { + headers[header.name] = header.value + } + + for header in headers { + let escapedValue = header.value.replacingOccurrences(of: "\"", with: "\\\"") + components.append("-H \"\(header.name): \(escapedValue)\"") + } + + if let httpBodyData = request.httpBody { + let httpBody = String(decoding: httpBodyData, as: UTF8.self) + var escapedBody = httpBody.replacingOccurrences(of: "\\\"", with: "\\\\\"") + escapedBody = escapedBody.replacingOccurrences(of: "\"", with: "\\\"") + + components.append("-d \"\(escapedBody)\"") + } + + components.append("\"\(url.absoluteString)\"") + + return components.joined(separator: " \\\n\t") + } +} + +/// Protocol abstraction for `Request`'s communication back to the `SessionDelegate`. +public protocol RequestDelegate: AnyObject { + /// `URLSessionConfiguration` used to create the underlying `URLSessionTask`s. + var sessionConfiguration: URLSessionConfiguration { get } + + /// Determines whether the `Request` should automatically call `resume()` when adding the first response handler. + var startImmediately: Bool { get } + + /// Notifies the delegate the `Request` has reached a point where it needs cleanup. + /// + /// - Parameter request: The `Request` to cleanup after. + func cleanup(after request: Request) + + /// Asynchronously ask the delegate whether a `Request` will be retried. + /// + /// - Parameters: + /// - request: `Request` which failed. + /// - error: `Error` which produced the failure. + /// - completion: Closure taking the `RetryResult` for evaluation. + func retryResult(for request: Request, dueTo error: AFError, completion: @escaping (RetryResult) -> Void) + + /// Asynchronously retry the `Request`. + /// + /// - Parameters: + /// - request: `Request` which will be retried. + /// - timeDelay: `TimeInterval` after which the retry will be triggered. + func retryRequest(_ request: Request, withDelay timeDelay: TimeInterval?) +} + +// MARK: - Subclasses + +// MARK: - DataRequest + +/// `Request` subclass which handles in-memory `Data` download using `URLSessionDataTask`. +public class DataRequest: Request { + /// `URLRequestConvertible` value used to create `URLRequest`s for this instance. + public let convertible: URLRequestConvertible + /// `Data` read from the server so far. + public var data: Data? { mutableData } + + /// Protected storage for the `Data` read by the instance. + @Protected + private var mutableData: Data? = nil + + /// Creates a `DataRequest` using the provided parameters. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. + /// - convertible: `URLRequestConvertible` value used to create `URLRequest`s for this instance. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. + init(id: UUID = UUID(), + convertible: URLRequestConvertible, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + delegate: RequestDelegate) { + self.convertible = convertible + + super.init(id: id, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: delegate) + } + + override func reset() { + super.reset() + + mutableData = nil + } + + /// Called when `Data` is received by this instance. + /// + /// - Note: Also calls `updateDownloadProgress`. + /// + /// - Parameter data: The `Data` received. + func didReceive(data: Data) { + if self.data == nil { + mutableData = data + } else { + $mutableData.write { $0?.append(data) } + } + + updateDownloadProgress() + } + + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + let copiedRequest = request + return session.dataTask(with: copiedRequest) + } + + /// Called to updated the `downloadProgress` of the instance. + func updateDownloadProgress() { + let totalBytesReceived = Int64(data?.count ?? 0) + let totalBytesExpected = task?.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown + + downloadProgress.totalUnitCount = totalBytesExpected + downloadProgress.completedUnitCount = totalBytesReceived + + downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) } + } + + /// Validates the request, using the specified closure. + /// + /// - Note: If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - Parameter validation: `Validation` closure used to validate the response. + /// + /// - Returns: The instance. + @discardableResult + public func validate(_ validation: @escaping Validation) -> Self { + let validator: () -> Void = { [unowned self] in + guard self.error == nil, let response = self.response else { return } + + let result = validation(self.request, response, self.data) + + if case let .failure(error) = result { self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) } + + self.eventMonitor?.request(self, + didValidateRequest: self.request, + response: response, + data: self.data, + withResult: result) + } + + $validators.write { $0.append(validator) } + + return self + } +} + +// MARK: - DataStreamRequest + +/// `Request` subclass which streams HTTP response `Data` through a `Handler` closure. +public final class DataStreamRequest: Request { + /// Closure type handling `DataStreamRequest.Stream` values. + public typealias Handler = (Stream) throws -> Void + + /// Type encapsulating an `Event` as it flows through the stream, as well as a `CancellationToken` which can be used + /// to stop the stream at any time. + public struct Stream { + /// Latest `Event` from the stream. + public let event: Event + /// Token used to cancel the stream. + public let token: CancellationToken + + /// Cancel the ongoing stream by canceling the underlying `DataStreamRequest`. + public func cancel() { + token.cancel() + } + } + + /// Type representing an event flowing through the stream. Contains either the `Result` of processing streamed + /// `Data` or the completion of the stream. + public enum Event { + /// Output produced every time the instance receives additional `Data`. The associated value contains the + /// `Result` of processing the incoming `Data`. + case stream(Result) + /// Output produced when the instance has completed, whether due to stream end, cancellation, or an error. + /// Associated `Completion` value contains the final state. + case complete(Completion) + } + + /// Value containing the state of a `DataStreamRequest` when the stream was completed. + public struct Completion { + /// Last `URLRequest` issued by the instance. + public let request: URLRequest? + /// Last `HTTPURLResponse` received by the instance. + public let response: HTTPURLResponse? + /// Last `URLSessionTaskMetrics` produced for the instance. + public let metrics: URLSessionTaskMetrics? + /// `AFError` produced for the instance, if any. + public let error: AFError? + } + + /// Type used to cancel an ongoing stream. + public struct CancellationToken { + weak var request: DataStreamRequest? + + init(_ request: DataStreamRequest) { + self.request = request + } + + /// Cancel the ongoing stream by canceling the underlying `DataStreamRequest`. + public func cancel() { + request?.cancel() + } + } + + /// `URLRequestConvertible` value used to create `URLRequest`s for this instance. + public let convertible: URLRequestConvertible + /// Whether or not the instance will be cancelled if stream parsing encounters an error. + public let automaticallyCancelOnStreamError: Bool + + /// Internal mutable state specific to this type. + struct StreamMutableState { + /// `OutputStream` bound to the `InputStream` produced by `asInputStream`, if it has been called. + var outputStream: OutputStream? + /// Stream closures called as `Data` is received. + var streams: [(_ data: Data) -> Void] = [] + /// Number of currently executing streams. Used to ensure completions are only fired after all streams are + /// enqueued. + var numberOfExecutingStreams = 0 + /// Completion calls enqueued while streams are still executing. + var enqueuedCompletionEvents: [() -> Void] = [] + } + + @Protected + var streamMutableState = StreamMutableState() + + /// Creates a `DataStreamRequest` using the provided parameters. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` + /// by default. + /// - convertible: `URLRequestConvertible` value used to create `URLRequest`s for this + /// instance. + /// - automaticallyCancelOnStreamError: `Bool` indicating whether the instance will be cancelled when an `Error` + /// is thrown while serializing stream `Data`. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default + /// targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by + /// the `Request`. + init(id: UUID = UUID(), + convertible: URLRequestConvertible, + automaticallyCancelOnStreamError: Bool, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + delegate: RequestDelegate) { + self.convertible = convertible + self.automaticallyCancelOnStreamError = automaticallyCancelOnStreamError + + super.init(id: id, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: delegate) + } + + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + let copiedRequest = request + return session.dataTask(with: copiedRequest) + } + + override func finish(error: AFError? = nil) { + $streamMutableState.write { state in + state.outputStream?.close() + } + + super.finish(error: error) + } + + func didReceive(data: Data) { + $streamMutableState.write { state in + #if !(os(Linux) || os(Windows)) + if let stream = state.outputStream { + underlyingQueue.async { + var bytes = Array(data) + stream.write(&bytes, maxLength: bytes.count) + } + } + #endif + state.numberOfExecutingStreams += state.streams.count + let localState = state + underlyingQueue.async { localState.streams.forEach { $0(data) } } + } + } + + /// Validates the `URLRequest` and `HTTPURLResponse` received for the instance using the provided `Validation` closure. + /// + /// - Parameter validation: `Validation` closure used to validate the request and response. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func validate(_ validation: @escaping Validation) -> Self { + let validator: () -> Void = { [unowned self] in + guard self.error == nil, let response = self.response else { return } + + let result = validation(self.request, response) + + if case let .failure(error) = result { + self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) + } + + self.eventMonitor?.request(self, + didValidateRequest: self.request, + response: response, + withResult: result) + } + + $validators.write { $0.append(validator) } + + return self + } + + #if !(os(Linux) || os(Windows)) + /// Produces an `InputStream` that receives the `Data` received by the instance. + /// + /// - Note: The `InputStream` produced by this method must have `open()` called before being able to read `Data`. + /// Additionally, this method will automatically call `resume()` on the instance, regardless of whether or + /// not the creating session has `startRequestsImmediately` set to `true`. + /// + /// - Parameter bufferSize: Size, in bytes, of the buffer between the `OutputStream` and `InputStream`. + /// + /// - Returns: The `InputStream` bound to the internal `OutboundStream`. + public func asInputStream(bufferSize: Int = 1024) -> InputStream? { + defer { resume() } + + var inputStream: InputStream? + $streamMutableState.write { state in + Foundation.Stream.getBoundStreams(withBufferSize: bufferSize, + inputStream: &inputStream, + outputStream: &state.outputStream) + state.outputStream?.open() + } + + return inputStream + } + #endif + + func capturingError(from closure: () throws -> Void) { + do { + try closure() + } catch { + self.error = error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error))) + cancel() + } + } + + func appendStreamCompletion(on queue: DispatchQueue, + stream: @escaping Handler) { + appendResponseSerializer { + self.underlyingQueue.async { + self.responseSerializerDidComplete { + self.$streamMutableState.write { state in + guard state.numberOfExecutingStreams == 0 else { + state.enqueuedCompletionEvents.append { + self.enqueueCompletion(on: queue, stream: stream) + } + + return + } + + self.enqueueCompletion(on: queue, stream: stream) + } + } + } + } + } + + func enqueueCompletion(on queue: DispatchQueue, + stream: @escaping Handler) { + queue.async { + do { + let completion = Completion(request: self.request, + response: self.response, + metrics: self.metrics, + error: self.error) + try stream(.init(event: .complete(completion), token: .init(self))) + } catch { + // Ignore error, as errors on Completion can't be handled anyway. + } + } + } +} + +extension DataStreamRequest.Stream { + /// Incoming `Result` values from `Event.stream`. + public var result: Result? { + guard case let .stream(result) = event else { return nil } + + return result + } + + /// `Success` value of the instance, if any. + public var value: Success? { + guard case let .success(value) = result else { return nil } + + return value + } + + /// `Failure` value of the instance, if any. + public var error: Failure? { + guard case let .failure(error) = result else { return nil } + + return error + } + + /// `Completion` value of the instance, if any. + public var completion: DataStreamRequest.Completion? { + guard case let .complete(completion) = event else { return nil } + + return completion + } +} + +// MARK: - DownloadRequest + +/// `Request` subclass which downloads `Data` to a file on disk using `URLSessionDownloadTask`. +public class DownloadRequest: Request { + /// A set of options to be executed prior to moving a downloaded file from the temporary `URL` to the destination + /// `URL`. + public struct Options: OptionSet { + /// Specifies that intermediate directories for the destination URL should be created. + public static let createIntermediateDirectories = Options(rawValue: 1 << 0) + /// Specifies that any previous file at the destination `URL` should be removed. + public static let removePreviousFile = Options(rawValue: 1 << 1) + + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + } + + // MARK: Destination + + /// A closure executed once a `DownloadRequest` has successfully completed in order to determine where to move the + /// temporary file written to during the download process. The closure takes two arguments: the temporary file URL + /// and the `HTTPURLResponse`, and returns two values: the file URL where the temporary file should be moved and + /// the options defining how the file should be moved. + /// + /// - Note: Downloads from a local `file://` `URL`s do not use the `Destination` closure, as those downloads do not + /// return an `HTTPURLResponse`. Instead the file is merely moved within the temporary directory. + public typealias Destination = (_ temporaryURL: URL, + _ response: HTTPURLResponse) -> (destinationURL: URL, options: Options) + + /// Creates a download file destination closure which uses the default file manager to move the temporary file to a + /// file URL in the first available directory with the specified search path directory and search path domain mask. + /// + /// - Parameters: + /// - directory: The search path directory. `.documentDirectory` by default. + /// - domain: The search path domain mask. `.userDomainMask` by default. + /// - options: `DownloadRequest.Options` used when moving the downloaded file to its destination. None by + /// default. + /// - Returns: The `Destination` closure. + public class func suggestedDownloadDestination(for directory: FileManager.SearchPathDirectory = .documentDirectory, + in domain: FileManager.SearchPathDomainMask = .userDomainMask, + options: Options = []) -> Destination { + { temporaryURL, response in + let directoryURLs = FileManager.default.urls(for: directory, in: domain) + let url = directoryURLs.first?.appendingPathComponent(response.suggestedFilename!) ?? temporaryURL + + return (url, options) + } + } + + /// Default `Destination` used by Alamofire to ensure all downloads persist. This `Destination` prepends + /// `Alamofire_` to the automatically generated download name and moves it within the temporary directory. Files + /// with this destination must be additionally moved if they should survive the system reclamation of temporary + /// space. + static let defaultDestination: Destination = { url, _ in + (defaultDestinationURL(url), []) + } + + /// Default `URL` creation closure. Creates a `URL` in the temporary directory with `Alamofire_` prepended to the + /// provided file name. + static let defaultDestinationURL: (URL) -> URL = { url in + let filename = "Alamofire_\(url.lastPathComponent)" + let destination = url.deletingLastPathComponent().appendingPathComponent(filename) + + return destination + } + + // MARK: Downloadable + + /// Type describing the source used to create the underlying `URLSessionDownloadTask`. + public enum Downloadable { + /// Download should be started from the `URLRequest` produced by the associated `URLRequestConvertible` value. + case request(URLRequestConvertible) + /// Download should be started from the associated resume `Data` value. + case resumeData(Data) + } + + // MARK: Mutable State + + /// Type containing all mutable state for `DownloadRequest` instances. + private struct DownloadRequestMutableState { + /// Possible resume `Data` produced when cancelling the instance. + var resumeData: Data? + /// `URL` to which `Data` is being downloaded. + var fileURL: URL? + } + + /// Protected mutable state specific to `DownloadRequest`. + @Protected + private var mutableDownloadState = DownloadRequestMutableState() + + /// If the download is resumable and is eventually cancelled or fails, this value may be used to resume the download + /// using the `download(resumingWith data:)` API. + /// + /// - Note: For more information about `resumeData`, see [Apple's documentation](https://developer.apple.com/documentation/foundation/urlsessiondownloadtask/1411634-cancel). + public var resumeData: Data? { + #if !(os(Linux) || os(Windows)) + return $mutableDownloadState.resumeData ?? error?.downloadResumeData + #else + return $mutableDownloadState.resumeData + #endif + } + + /// If the download is successful, the `URL` where the file was downloaded. + public var fileURL: URL? { $mutableDownloadState.fileURL } + + // MARK: Initial State + + /// `Downloadable` value used for this instance. + public let downloadable: Downloadable + /// The `Destination` to which the downloaded file is moved. + let destination: Destination + + /// Creates a `DownloadRequest` using the provided parameters. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. + /// - downloadable: `Downloadable` value used to create `URLSessionDownloadTasks` for the instance. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request` + /// - destination: `Destination` closure used to move the downloaded file to its final location. + init(id: UUID = UUID(), + downloadable: Downloadable, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + delegate: RequestDelegate, + destination: @escaping Destination) { + self.downloadable = downloadable + self.destination = destination + + super.init(id: id, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: delegate) + } + + override func reset() { + super.reset() + + $mutableDownloadState.write { + $0.resumeData = nil + $0.fileURL = nil + } + } + + /// Called when a download has finished. + /// + /// - Parameters: + /// - task: `URLSessionTask` that finished the download. + /// - result: `Result` of the automatic move to `destination`. + func didFinishDownloading(using task: URLSessionTask, with result: Result) { + eventMonitor?.request(self, didFinishDownloadingUsing: task, with: result) + + switch result { + case let .success(url): $mutableDownloadState.fileURL = url + case let .failure(error): self.error = error + } + } + + /// Updates the `downloadProgress` using the provided values. + /// + /// - Parameters: + /// - bytesWritten: Total bytes written so far. + /// - totalBytesExpectedToWrite: Total bytes expected to write. + func updateDownloadProgress(bytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + downloadProgress.totalUnitCount = totalBytesExpectedToWrite + downloadProgress.completedUnitCount += bytesWritten + + downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) } + } + + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + session.downloadTask(with: request) + } + + /// Creates a `URLSessionTask` from the provided resume data. + /// + /// - Parameters: + /// - data: `Data` used to resume the download. + /// - session: `URLSession` used to create the `URLSessionTask`. + /// + /// - Returns: The `URLSessionTask` created. + public func task(forResumeData data: Data, using session: URLSession) -> URLSessionTask { + session.downloadTask(withResumeData: data) + } + + /// Cancels the instance. Once cancelled, a `DownloadRequest` can no longer be resumed or suspended. + /// + /// - Note: This method will NOT produce resume data. If you wish to cancel and produce resume data, use + /// `cancel(producingResumeData:)` or `cancel(byProducingResumeData:)`. + /// + /// - Returns: The instance. + @discardableResult + override public func cancel() -> Self { + cancel(producingResumeData: false) + } + + /// Cancels the instance, optionally producing resume data. Once cancelled, a `DownloadRequest` can no longer be + /// resumed or suspended. + /// + /// - Note: If `producingResumeData` is `true`, the `resumeData` property will be populated with any resume data, if + /// available. + /// + /// - Returns: The instance. + @discardableResult + public func cancel(producingResumeData shouldProduceResumeData: Bool) -> Self { + cancel(optionallyProducingResumeData: shouldProduceResumeData ? { _ in } : nil) + } + + /// Cancels the instance while producing resume data. Once cancelled, a `DownloadRequest` can no longer be resumed + /// or suspended. + /// + /// - Note: The resume data passed to the completion handler will also be available on the instance's `resumeData` + /// property. + /// + /// - Parameter completionHandler: The completion handler that is called when the download has been successfully + /// cancelled. It is not guaranteed to be called on a particular queue, so you may + /// want use an appropriate queue to perform your work. + /// + /// - Returns: The instance. + @discardableResult + public func cancel(byProducingResumeData completionHandler: @escaping (_ data: Data?) -> Void) -> Self { + cancel(optionallyProducingResumeData: completionHandler) + } + + /// Internal implementation of cancellation that optionally takes a resume data handler. If no handler is passed, + /// cancellation is performed without producing resume data. + /// + /// - Parameter completionHandler: Optional resume data handler. + /// + /// - Returns: The instance. + private func cancel(optionallyProducingResumeData completionHandler: ((_ resumeData: Data?) -> Void)?) -> Self { + $mutableState.write { mutableState in + guard mutableState.state.canTransitionTo(.cancelled) else { return } + + mutableState.state = .cancelled + + underlyingQueue.async { self.didCancel() } + + guard let task = mutableState.tasks.last as? URLSessionDownloadTask, task.state != .completed else { + underlyingQueue.async { self.finish() } + return + } + + if let completionHandler = completionHandler { + // Resume to ensure metrics are gathered. + task.resume() + task.cancel { resumeData in + self.$mutableDownloadState.resumeData = resumeData + self.underlyingQueue.async { self.didCancelTask(task) } + completionHandler(resumeData) + } + } else { + // Resume to ensure metrics are gathered. + task.resume() + task.cancel() + self.underlyingQueue.async { self.didCancelTask(task) } + } + } + + return self + } + + /// Validates the request, using the specified closure. + /// + /// - Note: If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - Parameter validation: `Validation` closure to validate the response. + /// + /// - Returns: The instance. + @discardableResult + public func validate(_ validation: @escaping Validation) -> Self { + let validator: () -> Void = { [unowned self] in + guard self.error == nil, let response = self.response else { return } + + let result = validation(self.request, response, self.fileURL) + + if case let .failure(error) = result { + self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) + } + + self.eventMonitor?.request(self, + didValidateRequest: self.request, + response: response, + fileURL: self.fileURL, + withResult: result) + } + + $validators.write { $0.append(validator) } + + return self + } +} + +// MARK: - UploadRequest + +/// `DataRequest` subclass which handles `Data` upload from memory, file, or stream using `URLSessionUploadTask`. +public class UploadRequest: DataRequest { + /// Type describing the origin of the upload, whether `Data`, file, or stream. + public enum Uploadable { + /// Upload from the provided `Data` value. + case data(Data) + /// Upload from the provided file `URL`, as well as a `Bool` determining whether the source file should be + /// automatically removed once uploaded. + case file(URL, shouldRemove: Bool) + /// Upload from the provided `InputStream`. + case stream(InputStream) + } + + // MARK: Initial State + + /// The `UploadableConvertible` value used to produce the `Uploadable` value for this instance. + public let upload: UploadableConvertible + + /// `FileManager` used to perform cleanup tasks, including the removal of multipart form encoded payloads written + /// to disk. + public let fileManager: FileManager + + // MARK: Mutable State + + /// `Uploadable` value used by the instance. + public var uploadable: Uploadable? + + /// Creates an `UploadRequest` using the provided parameters. + /// + /// - Parameters: + /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. + /// - convertible: `UploadConvertible` value used to determine the type of upload to be performed. + /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. + /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets + /// `underlyingQueue`, but can be passed another queue from a `Session`. + /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. + /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. + /// - fileManager: `FileManager` used to perform cleanup tasks, including the removal of multipart form + /// encoded payloads written to disk. + /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. + init(id: UUID = UUID(), + convertible: UploadConvertible, + underlyingQueue: DispatchQueue, + serializationQueue: DispatchQueue, + eventMonitor: EventMonitor?, + interceptor: RequestInterceptor?, + fileManager: FileManager, + delegate: RequestDelegate) { + upload = convertible + self.fileManager = fileManager + + super.init(id: id, + convertible: convertible, + underlyingQueue: underlyingQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: delegate) + } + + /// Called when the `Uploadable` value has been created from the `UploadConvertible`. + /// + /// - Parameter uploadable: The `Uploadable` that was created. + func didCreateUploadable(_ uploadable: Uploadable) { + self.uploadable = uploadable + + eventMonitor?.request(self, didCreateUploadable: uploadable) + } + + /// Called when the `Uploadable` value could not be created. + /// + /// - Parameter error: `AFError` produced by the failure. + func didFailToCreateUploadable(with error: AFError) { + self.error = error + + eventMonitor?.request(self, didFailToCreateUploadableWithError: error) + + retryOrFinish(error: error) + } + + override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { + guard let uploadable = uploadable else { + fatalError("Attempting to create a URLSessionUploadTask when Uploadable value doesn't exist.") + } + + switch uploadable { + case let .data(data): return session.uploadTask(with: request, from: data) + case let .file(url, _): return session.uploadTask(with: request, fromFile: url) + case .stream: return session.uploadTask(withStreamedRequest: request) + } + } + + override func reset() { + // Uploadable must be recreated on every retry. + uploadable = nil + + super.reset() + } + + /// Produces the `InputStream` from `uploadable`, if it can. + /// + /// - Note: Calling this method with a non-`.stream` `Uploadable` is a logic error and will crash. + /// + /// - Returns: The `InputStream`. + func inputStream() -> InputStream { + guard let uploadable = uploadable else { + fatalError("Attempting to access the input stream but the uploadable doesn't exist.") + } + + guard case let .stream(stream) = uploadable else { + fatalError("Attempted to access the stream of an UploadRequest that wasn't created with one.") + } + + eventMonitor?.request(self, didProvideInputStream: stream) + + return stream + } + + override public func cleanup() { + defer { super.cleanup() } + + guard + let uploadable = uploadable, + case let .file(url, shouldRemove) = uploadable, + shouldRemove + else { return } + + try? fileManager.removeItem(at: url) + } +} + +/// A type that can produce an `UploadRequest.Uploadable` value. +public protocol UploadableConvertible { + /// Produces an `UploadRequest.Uploadable` value from the instance. + /// + /// - Returns: The `UploadRequest.Uploadable`. + /// - Throws: Any `Error` produced during creation. + func createUploadable() throws -> UploadRequest.Uploadable +} + +extension UploadRequest.Uploadable: UploadableConvertible { + public func createUploadable() throws -> UploadRequest.Uploadable { + self + } +} + +/// A type that can be converted to an upload, whether from an `UploadRequest.Uploadable` or `URLRequestConvertible`. +public protocol UploadConvertible: UploadableConvertible & URLRequestConvertible {} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/RequestInterceptor.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/RequestInterceptor.swift new file mode 100644 index 0000000..7ed39a5 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/RequestInterceptor.swift @@ -0,0 +1,357 @@ +// +// RequestInterceptor.swift +// +// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Stores all state associated with a `URLRequest` being adapted. +public struct RequestAdapterState { + /// The `UUID` of the `Request` associated with the `URLRequest` to adapt. + public let requestID: UUID + + /// The `Session` associated with the `URLRequest` to adapt. + public let session: Session +} + +// MARK: - + +/// A type that can inspect and optionally adapt a `URLRequest` in some manner if necessary. +public protocol RequestAdapter { + /// Inspects and adapts the specified `URLRequest` in some manner and calls the completion handler with the Result. + /// + /// - Parameters: + /// - urlRequest: The `URLRequest` to adapt. + /// - session: The `Session` that will execute the `URLRequest`. + /// - completion: The completion handler that must be called when adaptation is complete. + func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) + + /// Inspects and adapts the specified `URLRequest` in some manner and calls the completion handler with the Result. + /// + /// - Parameters: + /// - urlRequest: The `URLRequest` to adapt. + /// - state: The `RequestAdapterState` associated with the `URLRequest`. + /// - completion: The completion handler that must be called when adaptation is complete. + func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result) -> Void) +} + +extension RequestAdapter { + public func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result) -> Void) { + adapt(urlRequest, for: state.session, completion: completion) + } +} + +// MARK: - + +/// Outcome of determination whether retry is necessary. +public enum RetryResult { + /// Retry should be attempted immediately. + case retry + /// Retry should be attempted after the associated `TimeInterval`. + case retryWithDelay(TimeInterval) + /// Do not retry. + case doNotRetry + /// Do not retry due to the associated `Error`. + case doNotRetryWithError(Error) +} + +extension RetryResult { + var retryRequired: Bool { + switch self { + case .retry, .retryWithDelay: return true + default: return false + } + } + + var delay: TimeInterval? { + switch self { + case let .retryWithDelay(delay): return delay + default: return nil + } + } + + var error: Error? { + guard case let .doNotRetryWithError(error) = self else { return nil } + return error + } +} + +/// A type that determines whether a request should be retried after being executed by the specified session manager +/// and encountering an error. +public protocol RequestRetrier { + /// Determines whether the `Request` should be retried by calling the `completion` closure. + /// + /// This operation is fully asynchronous. Any amount of time can be taken to determine whether the request needs + /// to be retried. The one requirement is that the completion closure is called to ensure the request is properly + /// cleaned up after. + /// + /// - Parameters: + /// - request: `Request` that failed due to the provided `Error`. + /// - session: `Session` that produced the `Request`. + /// - error: `Error` encountered while executing the `Request`. + /// - completion: Completion closure to be executed when a retry decision has been determined. + func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) +} + +// MARK: - + +/// Type that provides both `RequestAdapter` and `RequestRetrier` functionality. +public protocol RequestInterceptor: RequestAdapter, RequestRetrier {} + +extension RequestInterceptor { + public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + completion(.success(urlRequest)) + } + + public func retry(_ request: Request, + for session: Session, + dueTo error: Error, + completion: @escaping (RetryResult) -> Void) { + completion(.doNotRetry) + } +} + +/// `RequestAdapter` closure definition. +public typealias AdaptHandler = (URLRequest, Session, _ completion: @escaping (Result) -> Void) -> Void +/// `RequestRetrier` closure definition. +public typealias RetryHandler = (Request, Session, Error, _ completion: @escaping (RetryResult) -> Void) -> Void + +// MARK: - + +/// Closure-based `RequestAdapter`. +open class Adapter: RequestInterceptor { + private let adaptHandler: AdaptHandler + + /// Creates an instance using the provided closure. + /// + /// - Parameter adaptHandler: `AdaptHandler` closure to be executed when handling request adaptation. + public init(_ adaptHandler: @escaping AdaptHandler) { + self.adaptHandler = adaptHandler + } + + open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + adaptHandler(urlRequest, session, completion) + } + + open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result) -> Void) { + adaptHandler(urlRequest, state.session, completion) + } +} + +#if swift(>=5.5) +extension RequestAdapter where Self == Adapter { + /// Creates an `Adapter` using the provided `AdaptHandler` closure. + /// + /// - Parameter closure: `AdaptHandler` to use to adapt the request. + /// - Returns: The `Adapter`. + public static func adapter(using closure: @escaping AdaptHandler) -> Adapter { + Adapter(closure) + } +} +#endif + +// MARK: - + +/// Closure-based `RequestRetrier`. +open class Retrier: RequestInterceptor { + private let retryHandler: RetryHandler + + /// Creates an instance using the provided closure. + /// + /// - Parameter retryHandler: `RetryHandler` closure to be executed when handling request retry. + public init(_ retryHandler: @escaping RetryHandler) { + self.retryHandler = retryHandler + } + + open func retry(_ request: Request, + for session: Session, + dueTo error: Error, + completion: @escaping (RetryResult) -> Void) { + retryHandler(request, session, error, completion) + } +} + +#if swift(>=5.5) +extension RequestRetrier where Self == Retrier { + /// Creates a `Retrier` using the provided `RetryHandler` closure. + /// + /// - Parameter closure: `RetryHandler` to use to retry the request. + /// - Returns: The `Retrier`. + public static func retrier(using closure: @escaping RetryHandler) -> Retrier { + Retrier(closure) + } +} +#endif + +// MARK: - + +/// `RequestInterceptor` which can use multiple `RequestAdapter` and `RequestRetrier` values. +open class Interceptor: RequestInterceptor { + /// All `RequestAdapter`s associated with the instance. These adapters will be run until one fails. + public let adapters: [RequestAdapter] + /// All `RequestRetrier`s associated with the instance. These retriers will be run one at a time until one triggers retry. + public let retriers: [RequestRetrier] + + /// Creates an instance from `AdaptHandler` and `RetryHandler` closures. + /// + /// - Parameters: + /// - adaptHandler: `AdaptHandler` closure to be used. + /// - retryHandler: `RetryHandler` closure to be used. + public init(adaptHandler: @escaping AdaptHandler, retryHandler: @escaping RetryHandler) { + adapters = [Adapter(adaptHandler)] + retriers = [Retrier(retryHandler)] + } + + /// Creates an instance from `RequestAdapter` and `RequestRetrier` values. + /// + /// - Parameters: + /// - adapter: `RequestAdapter` value to be used. + /// - retrier: `RequestRetrier` value to be used. + public init(adapter: RequestAdapter, retrier: RequestRetrier) { + adapters = [adapter] + retriers = [retrier] + } + + /// Creates an instance from the arrays of `RequestAdapter` and `RequestRetrier` values. + /// + /// - Parameters: + /// - adapters: `RequestAdapter` values to be used. + /// - retriers: `RequestRetrier` values to be used. + /// - interceptors: `RequestInterceptor`s to be used. + public init(adapters: [RequestAdapter] = [], retriers: [RequestRetrier] = [], interceptors: [RequestInterceptor] = []) { + self.adapters = adapters + interceptors + self.retriers = retriers + interceptors + } + + open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + adapt(urlRequest, for: session, using: adapters, completion: completion) + } + + private func adapt(_ urlRequest: URLRequest, + for session: Session, + using adapters: [RequestAdapter], + completion: @escaping (Result) -> Void) { + var pendingAdapters = adapters + + guard !pendingAdapters.isEmpty else { completion(.success(urlRequest)); return } + + let adapter = pendingAdapters.removeFirst() + + adapter.adapt(urlRequest, for: session) { result in + switch result { + case let .success(urlRequest): + self.adapt(urlRequest, for: session, using: pendingAdapters, completion: completion) + case .failure: + completion(result) + } + } + } + + open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result) -> Void) { + adapt(urlRequest, using: state, adapters: adapters, completion: completion) + } + + private func adapt(_ urlRequest: URLRequest, + using state: RequestAdapterState, + adapters: [RequestAdapter], + completion: @escaping (Result) -> Void) { + var pendingAdapters = adapters + + guard !pendingAdapters.isEmpty else { completion(.success(urlRequest)); return } + + let adapter = pendingAdapters.removeFirst() + + adapter.adapt(urlRequest, using: state) { result in + switch result { + case let .success(urlRequest): + self.adapt(urlRequest, using: state, adapters: pendingAdapters, completion: completion) + case .failure: + completion(result) + } + } + } + + open func retry(_ request: Request, + for session: Session, + dueTo error: Error, + completion: @escaping (RetryResult) -> Void) { + retry(request, for: session, dueTo: error, using: retriers, completion: completion) + } + + private func retry(_ request: Request, + for session: Session, + dueTo error: Error, + using retriers: [RequestRetrier], + completion: @escaping (RetryResult) -> Void) { + var pendingRetriers = retriers + + guard !pendingRetriers.isEmpty else { completion(.doNotRetry); return } + + let retrier = pendingRetriers.removeFirst() + + retrier.retry(request, for: session, dueTo: error) { result in + switch result { + case .retry, .retryWithDelay, .doNotRetryWithError: + completion(result) + case .doNotRetry: + // Only continue to the next retrier if retry was not triggered and no error was encountered + self.retry(request, for: session, dueTo: error, using: pendingRetriers, completion: completion) + } + } + } +} + +#if swift(>=5.5) +extension RequestInterceptor where Self == Interceptor { + /// Creates an `Interceptor` using the provided `AdaptHandler` and `RetryHandler` closures. + /// + /// - Parameters: + /// - adapter: `AdapterHandler`to use to adapt the request. + /// - retrier: `RetryHandler` to use to retry the request. + /// - Returns: The `Interceptor`. + public static func interceptor(adapter: @escaping AdaptHandler, retrier: @escaping RetryHandler) -> Interceptor { + Interceptor(adaptHandler: adapter, retryHandler: retrier) + } + + /// Creates an `Interceptor` using the provided `RequestAdapter` and `RequestRetrier` instances. + /// - Parameters: + /// - adapter: `RequestAdapter` to use to adapt the request + /// - retrier: `RequestRetrier` to use to retry the request. + /// - Returns: The `Interceptor`. + public static func interceptor(adapter: RequestAdapter, retrier: RequestRetrier) -> Interceptor { + Interceptor(adapter: adapter, retrier: retrier) + } + + /// Creates an `Interceptor` using the provided `RequestAdapter`s, `RequestRetrier`s, and `RequestInterceptor`s. + /// - Parameters: + /// - adapters: `RequestAdapter`s to use to adapt the request. These adapters will be run until one fails. + /// - retriers: `RequestRetrier`s to use to retry the request. These retriers will be run one at a time until + /// a retry is triggered. + /// - interceptors: `RequestInterceptor`s to use to intercept the request. + /// - Returns: The `Interceptor`. + public static func interceptor(adapters: [RequestAdapter] = [], + retriers: [RequestRetrier] = [], + interceptors: [RequestInterceptor] = []) -> Interceptor { + Interceptor(adapters: adapters, retriers: retriers, interceptors: interceptors) + } +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/RequestTaskMap.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/RequestTaskMap.swift new file mode 100644 index 0000000..85b58f3 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/RequestTaskMap.swift @@ -0,0 +1,149 @@ +// +// RequestTaskMap.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A type that maintains a two way, one to one map of `URLSessionTask`s to `Request`s. +struct RequestTaskMap { + private typealias Events = (completed: Bool, metricsGathered: Bool) + + private var tasksToRequests: [URLSessionTask: Request] + private var requestsToTasks: [Request: URLSessionTask] + private var taskEvents: [URLSessionTask: Events] + + var requests: [Request] { + Array(tasksToRequests.values) + } + + init(tasksToRequests: [URLSessionTask: Request] = [:], + requestsToTasks: [Request: URLSessionTask] = [:], + taskEvents: [URLSessionTask: (completed: Bool, metricsGathered: Bool)] = [:]) { + self.tasksToRequests = tasksToRequests + self.requestsToTasks = requestsToTasks + self.taskEvents = taskEvents + } + + subscript(_ request: Request) -> URLSessionTask? { + get { requestsToTasks[request] } + set { + guard let newValue = newValue else { + guard let task = requestsToTasks[request] else { + fatalError("RequestTaskMap consistency error: no task corresponding to request found.") + } + + requestsToTasks.removeValue(forKey: request) + tasksToRequests.removeValue(forKey: task) + taskEvents.removeValue(forKey: task) + + return + } + + requestsToTasks[request] = newValue + tasksToRequests[newValue] = request + taskEvents[newValue] = (completed: false, metricsGathered: false) + } + } + + subscript(_ task: URLSessionTask) -> Request? { + get { tasksToRequests[task] } + set { + guard let newValue = newValue else { + guard let request = tasksToRequests[task] else { + fatalError("RequestTaskMap consistency error: no request corresponding to task found.") + } + + tasksToRequests.removeValue(forKey: task) + requestsToTasks.removeValue(forKey: request) + taskEvents.removeValue(forKey: task) + + return + } + + tasksToRequests[task] = newValue + requestsToTasks[newValue] = task + taskEvents[task] = (completed: false, metricsGathered: false) + } + } + + var count: Int { + precondition(tasksToRequests.count == requestsToTasks.count, + "RequestTaskMap.count invalid, requests.count: \(tasksToRequests.count) != tasks.count: \(requestsToTasks.count)") + + return tasksToRequests.count + } + + var eventCount: Int { + precondition(taskEvents.count == count, "RequestTaskMap.eventCount invalid, count: \(count) != taskEvents.count: \(taskEvents.count)") + + return taskEvents.count + } + + var isEmpty: Bool { + precondition(tasksToRequests.isEmpty == requestsToTasks.isEmpty, + "RequestTaskMap.isEmpty invalid, requests.isEmpty: \(tasksToRequests.isEmpty) != tasks.isEmpty: \(requestsToTasks.isEmpty)") + + return tasksToRequests.isEmpty + } + + var isEventsEmpty: Bool { + precondition(taskEvents.isEmpty == isEmpty, "RequestTaskMap.isEventsEmpty invalid, isEmpty: \(isEmpty) != taskEvents.isEmpty: \(taskEvents.isEmpty)") + + return taskEvents.isEmpty + } + + mutating func disassociateIfNecessaryAfterGatheringMetricsForTask(_ task: URLSessionTask) -> Bool { + guard let events = taskEvents[task] else { + fatalError("RequestTaskMap consistency error: no events corresponding to task found.") + } + + switch (events.completed, events.metricsGathered) { + case (_, true): fatalError("RequestTaskMap consistency error: duplicate metricsGatheredForTask call.") + case (false, false): taskEvents[task] = (completed: false, metricsGathered: true); return false + case (true, false): self[task] = nil; return true + } + } + + mutating func disassociateIfNecessaryAfterCompletingTask(_ task: URLSessionTask) -> Bool { + guard let events = taskEvents[task] else { + fatalError("RequestTaskMap consistency error: no events corresponding to task found.") + } + + switch (events.completed, events.metricsGathered) { + case (true, _): fatalError("RequestTaskMap consistency error: duplicate completionReceivedForTask call.") + #if os(Linux) // Linux doesn't gather metrics, so unconditionally remove the reference and return true. + default: self[task] = nil; return true + #else + case (false, false): + if #available(macOS 10.12, iOS 10, watchOS 7, tvOS 10, *) { + taskEvents[task] = (completed: true, metricsGathered: false); return false + } else { + // watchOS < 7 doesn't gather metrics, so unconditionally remove the reference and return true. + self[task] = nil; return true + } + case (false, true): + self[task] = nil; return true + #endif + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/Response.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Response.swift new file mode 100644 index 0000000..d9ae9d8 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Response.swift @@ -0,0 +1,453 @@ +// +// Response.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Default type of `DataResponse` returned by Alamofire, with an `AFError` `Failure` type. +public typealias AFDataResponse = DataResponse +/// Default type of `DownloadResponse` returned by Alamofire, with an `AFError` `Failure` type. +public typealias AFDownloadResponse = DownloadResponse + +/// Type used to store all values associated with a serialized response of a `DataRequest` or `UploadRequest`. +public struct DataResponse { + /// The URL request sent to the server. + public let request: URLRequest? + + /// The server's response to the URL request. + public let response: HTTPURLResponse? + + /// The data returned by the server. + public let data: Data? + + /// The final metrics of the response. + /// + /// - Note: Due to `FB7624529`, collection of `URLSessionTaskMetrics` on watchOS is currently disabled.` + /// + public let metrics: URLSessionTaskMetrics? + + /// The time taken to serialize the response. + public let serializationDuration: TimeInterval + + /// The result of response serialization. + public let result: Result + + /// Returns the associated value of the result if it is a success, `nil` otherwise. + public var value: Success? { result.success } + + /// Returns the associated error value if the result if it is a failure, `nil` otherwise. + public var error: Failure? { result.failure } + + /// Creates a `DataResponse` instance with the specified parameters derived from the response serialization. + /// + /// - Parameters: + /// - request: The `URLRequest` sent to the server. + /// - response: The `HTTPURLResponse` from the server. + /// - data: The `Data` returned by the server. + /// - metrics: The `URLSessionTaskMetrics` of the `DataRequest` or `UploadRequest`. + /// - serializationDuration: The duration taken by serialization. + /// - result: The `Result` of response serialization. + public init(request: URLRequest?, + response: HTTPURLResponse?, + data: Data?, + metrics: URLSessionTaskMetrics?, + serializationDuration: TimeInterval, + result: Result) { + self.request = request + self.response = response + self.data = data + self.metrics = metrics + self.serializationDuration = serializationDuration + self.result = result + } +} + +// MARK: - + +extension DataResponse: CustomStringConvertible, CustomDebugStringConvertible { + /// The textual representation used when written to an output stream, which includes whether the result was a + /// success or failure. + public var description: String { + "\(result)" + } + + /// The debug textual representation used when written to an output stream, which includes (if available) a summary + /// of the `URLRequest`, the request's headers and body (if decodable as a `String` below 100KB); the + /// `HTTPURLResponse`'s status code, headers, and body; the duration of the network and serialization actions; and + /// the `Result` of serialization. + public var debugDescription: String { + guard let urlRequest = request else { return "[Request]: None\n[Result]: \(result)" } + + let requestDescription = DebugDescription.description(of: urlRequest) + + let responseDescription = response.map { response in + let responseBodyDescription = DebugDescription.description(for: data, headers: response.headers) + + return """ + \(DebugDescription.description(of: response)) + \(responseBodyDescription.indentingNewlines()) + """ + } ?? "[Response]: None" + + let networkDuration = metrics.map { "\($0.taskInterval.duration)s" } ?? "None" + + return """ + \(requestDescription) + \(responseDescription) + [Network Duration]: \(networkDuration) + [Serialization Duration]: \(serializationDuration)s + [Result]: \(result) + """ + } +} + +// MARK: - + +extension DataResponse { + /// Evaluates the specified closure when the result of this `DataResponse` is a success, passing the unwrapped + /// result value as a parameter. + /// + /// Use the `map` method with a closure that does not throw. For example: + /// + /// let possibleData: DataResponse = ... + /// let possibleInt = possibleData.map { $0.count } + /// + /// - parameter transform: A closure that takes the success value of the instance's result. + /// + /// - returns: A `DataResponse` whose result wraps the value returned by the given closure. If this instance's + /// result is a failure, returns a response wrapping the same failure. + public func map(_ transform: (Success) -> NewSuccess) -> DataResponse { + DataResponse(request: request, + response: response, + data: data, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.map(transform)) + } + + /// Evaluates the given closure when the result of this `DataResponse` is a success, passing the unwrapped result + /// value as a parameter. + /// + /// Use the `tryMap` method with a closure that may throw an error. For example: + /// + /// let possibleData: DataResponse = ... + /// let possibleObject = possibleData.tryMap { + /// try JSONSerialization.jsonObject(with: $0) + /// } + /// + /// - parameter transform: A closure that takes the success value of the instance's result. + /// + /// - returns: A success or failure `DataResponse` depending on the result of the given closure. If this instance's + /// result is a failure, returns the same failure. + public func tryMap(_ transform: (Success) throws -> NewSuccess) -> DataResponse { + DataResponse(request: request, + response: response, + data: data, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.tryMap(transform)) + } + + /// Evaluates the specified closure when the `DataResponse` is a failure, passing the unwrapped error as a parameter. + /// + /// Use the `mapError` function with a closure that does not throw. For example: + /// + /// let possibleData: DataResponse = ... + /// let withMyError = possibleData.mapError { MyError.error($0) } + /// + /// - Parameter transform: A closure that takes the error of the instance. + /// + /// - Returns: A `DataResponse` instance containing the result of the transform. + public func mapError(_ transform: (Failure) -> NewFailure) -> DataResponse { + DataResponse(request: request, + response: response, + data: data, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.mapError(transform)) + } + + /// Evaluates the specified closure when the `DataResponse` is a failure, passing the unwrapped error as a parameter. + /// + /// Use the `tryMapError` function with a closure that may throw an error. For example: + /// + /// let possibleData: DataResponse = ... + /// let possibleObject = possibleData.tryMapError { + /// try someFailableFunction(taking: $0) + /// } + /// + /// - Parameter transform: A throwing closure that takes the error of the instance. + /// + /// - Returns: A `DataResponse` instance containing the result of the transform. + public func tryMapError(_ transform: (Failure) throws -> NewFailure) -> DataResponse { + DataResponse(request: request, + response: response, + data: data, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.tryMapError(transform)) + } +} + +// MARK: - + +/// Used to store all data associated with a serialized response of a download request. +public struct DownloadResponse { + /// The URL request sent to the server. + public let request: URLRequest? + + /// The server's response to the URL request. + public let response: HTTPURLResponse? + + /// The final destination URL of the data returned from the server after it is moved. + public let fileURL: URL? + + /// The resume data generated if the request was cancelled. + public let resumeData: Data? + + /// The final metrics of the response. + /// + /// - Note: Due to `FB7624529`, collection of `URLSessionTaskMetrics` on watchOS is currently disabled.` + /// + public let metrics: URLSessionTaskMetrics? + + /// The time taken to serialize the response. + public let serializationDuration: TimeInterval + + /// The result of response serialization. + public let result: Result + + /// Returns the associated value of the result if it is a success, `nil` otherwise. + public var value: Success? { result.success } + + /// Returns the associated error value if the result if it is a failure, `nil` otherwise. + public var error: Failure? { result.failure } + + /// Creates a `DownloadResponse` instance with the specified parameters derived from response serialization. + /// + /// - Parameters: + /// - request: The `URLRequest` sent to the server. + /// - response: The `HTTPURLResponse` from the server. + /// - fileURL: The final destination URL of the data returned from the server after it is moved. + /// - resumeData: The resume `Data` generated if the request was cancelled. + /// - metrics: The `URLSessionTaskMetrics` of the `DownloadRequest`. + /// - serializationDuration: The duration taken by serialization. + /// - result: The `Result` of response serialization. + public init(request: URLRequest?, + response: HTTPURLResponse?, + fileURL: URL?, + resumeData: Data?, + metrics: URLSessionTaskMetrics?, + serializationDuration: TimeInterval, + result: Result) { + self.request = request + self.response = response + self.fileURL = fileURL + self.resumeData = resumeData + self.metrics = metrics + self.serializationDuration = serializationDuration + self.result = result + } +} + +// MARK: - + +extension DownloadResponse: CustomStringConvertible, CustomDebugStringConvertible { + /// The textual representation used when written to an output stream, which includes whether the result was a + /// success or failure. + public var description: String { + "\(result)" + } + + /// The debug textual representation used when written to an output stream, which includes the URL request, the URL + /// response, the temporary and destination URLs, the resume data, the durations of the network and serialization + /// actions, and the response serialization result. + public var debugDescription: String { + guard let urlRequest = request else { return "[Request]: None\n[Result]: \(result)" } + + let requestDescription = DebugDescription.description(of: urlRequest) + let responseDescription = response.map(DebugDescription.description(of:)) ?? "[Response]: None" + let networkDuration = metrics.map { "\($0.taskInterval.duration)s" } ?? "None" + let resumeDataDescription = resumeData.map { "\($0)" } ?? "None" + + return """ + \(requestDescription) + \(responseDescription) + [File URL]: \(fileURL?.path ?? "None") + [Resume Data]: \(resumeDataDescription) + [Network Duration]: \(networkDuration) + [Serialization Duration]: \(serializationDuration)s + [Result]: \(result) + """ + } +} + +// MARK: - + +extension DownloadResponse { + /// Evaluates the given closure when the result of this `DownloadResponse` is a success, passing the unwrapped + /// result value as a parameter. + /// + /// Use the `map` method with a closure that does not throw. For example: + /// + /// let possibleData: DownloadResponse = ... + /// let possibleInt = possibleData.map { $0.count } + /// + /// - parameter transform: A closure that takes the success value of the instance's result. + /// + /// - returns: A `DownloadResponse` whose result wraps the value returned by the given closure. If this instance's + /// result is a failure, returns a response wrapping the same failure. + public func map(_ transform: (Success) -> NewSuccess) -> DownloadResponse { + DownloadResponse(request: request, + response: response, + fileURL: fileURL, + resumeData: resumeData, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.map(transform)) + } + + /// Evaluates the given closure when the result of this `DownloadResponse` is a success, passing the unwrapped + /// result value as a parameter. + /// + /// Use the `tryMap` method with a closure that may throw an error. For example: + /// + /// let possibleData: DownloadResponse = ... + /// let possibleObject = possibleData.tryMap { + /// try JSONSerialization.jsonObject(with: $0) + /// } + /// + /// - parameter transform: A closure that takes the success value of the instance's result. + /// + /// - returns: A success or failure `DownloadResponse` depending on the result of the given closure. If this + /// instance's result is a failure, returns the same failure. + public func tryMap(_ transform: (Success) throws -> NewSuccess) -> DownloadResponse { + DownloadResponse(request: request, + response: response, + fileURL: fileURL, + resumeData: resumeData, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.tryMap(transform)) + } + + /// Evaluates the specified closure when the `DownloadResponse` is a failure, passing the unwrapped error as a parameter. + /// + /// Use the `mapError` function with a closure that does not throw. For example: + /// + /// let possibleData: DownloadResponse = ... + /// let withMyError = possibleData.mapError { MyError.error($0) } + /// + /// - Parameter transform: A closure that takes the error of the instance. + /// + /// - Returns: A `DownloadResponse` instance containing the result of the transform. + public func mapError(_ transform: (Failure) -> NewFailure) -> DownloadResponse { + DownloadResponse(request: request, + response: response, + fileURL: fileURL, + resumeData: resumeData, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.mapError(transform)) + } + + /// Evaluates the specified closure when the `DownloadResponse` is a failure, passing the unwrapped error as a parameter. + /// + /// Use the `tryMapError` function with a closure that may throw an error. For example: + /// + /// let possibleData: DownloadResponse = ... + /// let possibleObject = possibleData.tryMapError { + /// try someFailableFunction(taking: $0) + /// } + /// + /// - Parameter transform: A throwing closure that takes the error of the instance. + /// + /// - Returns: A `DownloadResponse` instance containing the result of the transform. + public func tryMapError(_ transform: (Failure) throws -> NewFailure) -> DownloadResponse { + DownloadResponse(request: request, + response: response, + fileURL: fileURL, + resumeData: resumeData, + metrics: metrics, + serializationDuration: serializationDuration, + result: result.tryMapError(transform)) + } +} + +private enum DebugDescription { + static func description(of request: URLRequest) -> String { + let requestSummary = "\(request.httpMethod!) \(request)" + let requestHeadersDescription = DebugDescription.description(for: request.headers) + let requestBodyDescription = DebugDescription.description(for: request.httpBody, headers: request.headers) + + return """ + [Request]: \(requestSummary) + \(requestHeadersDescription.indentingNewlines()) + \(requestBodyDescription.indentingNewlines()) + """ + } + + static func description(of response: HTTPURLResponse) -> String { + """ + [Response]: + [Status Code]: \(response.statusCode) + \(DebugDescription.description(for: response.headers).indentingNewlines()) + """ + } + + static func description(for headers: HTTPHeaders) -> String { + guard !headers.isEmpty else { return "[Headers]: None" } + + let headerDescription = "\(headers.sorted())".indentingNewlines() + return """ + [Headers]: + \(headerDescription) + """ + } + + static func description(for data: Data?, + headers: HTTPHeaders, + allowingPrintableTypes printableTypes: [String] = ["json", "xml", "text"], + maximumLength: Int = 100_000) -> String { + guard let data = data, !data.isEmpty else { return "[Body]: None" } + + guard + data.count <= maximumLength, + printableTypes.compactMap({ headers["Content-Type"]?.contains($0) }).contains(true) + else { return "[Body]: \(data.count) bytes" } + + return """ + [Body]: + \(String(decoding: data, as: UTF8.self) + .trimmingCharacters(in: .whitespacesAndNewlines) + .indentingNewlines()) + """ + } +} + +extension String { + fileprivate func indentingNewlines(by spaceCount: Int = 4) -> String { + let spaces = String(repeating: " ", count: spaceCount) + return replacingOccurrences(of: "\n", with: "\n\(spaces)") + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/ResponseSerialization.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/ResponseSerialization.swift new file mode 100644 index 0000000..3097364 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/ResponseSerialization.swift @@ -0,0 +1,1290 @@ +// +// ResponseSerialization.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +// MARK: Protocols + +/// The type to which all data response serializers must conform in order to serialize a response. +public protocol DataResponseSerializerProtocol { + /// The type of serialized object to be created. + associatedtype SerializedObject + + /// Serialize the response `Data` into the provided type.. + /// + /// - Parameters: + /// - request: `URLRequest` which was used to perform the request, if any. + /// - response: `HTTPURLResponse` received from the server, if any. + /// - data: `Data` returned from the server, if any. + /// - error: `Error` produced by Alamofire or the underlying `URLSession` during the request. + /// + /// - Returns: The `SerializedObject`. + /// - Throws: Any `Error` produced during serialization. + func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> SerializedObject +} + +/// The type to which all download response serializers must conform in order to serialize a response. +public protocol DownloadResponseSerializerProtocol { + /// The type of serialized object to be created. + associatedtype SerializedObject + + /// Serialize the downloaded response `Data` from disk into the provided type.. + /// + /// - Parameters: + /// - request: `URLRequest` which was used to perform the request, if any. + /// - response: `HTTPURLResponse` received from the server, if any. + /// - fileURL: File `URL` to which the response data was downloaded. + /// - error: `Error` produced by Alamofire or the underlying `URLSession` during the request. + /// + /// - Returns: The `SerializedObject`. + /// - Throws: Any `Error` produced during serialization. + func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> SerializedObject +} + +/// A serializer that can handle both data and download responses. +public protocol ResponseSerializer: DataResponseSerializerProtocol & DownloadResponseSerializerProtocol { + /// `DataPreprocessor` used to prepare incoming `Data` for serialization. + var dataPreprocessor: DataPreprocessor { get } + /// `HTTPMethod`s for which empty response bodies are considered appropriate. + var emptyRequestMethods: Set { get } + /// HTTP response codes for which empty response bodies are considered appropriate. + var emptyResponseCodes: Set { get } +} + +/// Type used to preprocess `Data` before it handled by a serializer. +public protocol DataPreprocessor { + /// Process `Data` before it's handled by a serializer. + /// - Parameter data: The raw `Data` to process. + func preprocess(_ data: Data) throws -> Data +} + +/// `DataPreprocessor` that returns passed `Data` without any transform. +public struct PassthroughPreprocessor: DataPreprocessor { + public init() {} + + public func preprocess(_ data: Data) throws -> Data { data } +} + +/// `DataPreprocessor` that trims Google's typical `)]}',\n` XSSI JSON header. +public struct GoogleXSSIPreprocessor: DataPreprocessor { + public init() {} + + public func preprocess(_ data: Data) throws -> Data { + (data.prefix(6) == Data(")]}',\n".utf8)) ? data.dropFirst(6) : data + } +} + +#if swift(>=5.5) +extension DataPreprocessor where Self == PassthroughPreprocessor { + /// Provides a `PassthroughPreprocessor` instance. + public static var passthrough: PassthroughPreprocessor { PassthroughPreprocessor() } +} + +extension DataPreprocessor where Self == GoogleXSSIPreprocessor { + /// Provides a `GoogleXSSIPreprocessor` instance. + public static var googleXSSI: GoogleXSSIPreprocessor { GoogleXSSIPreprocessor() } +} +#endif + +extension ResponseSerializer { + /// Default `DataPreprocessor`. `PassthroughPreprocessor` by default. + public static var defaultDataPreprocessor: DataPreprocessor { PassthroughPreprocessor() } + /// Default `HTTPMethod`s for which empty response bodies are considered appropriate. `[.head]` by default. + public static var defaultEmptyRequestMethods: Set { [.head] } + /// HTTP response codes for which empty response bodies are considered appropriate. `[204, 205]` by default. + public static var defaultEmptyResponseCodes: Set { [204, 205] } + + public var dataPreprocessor: DataPreprocessor { Self.defaultDataPreprocessor } + public var emptyRequestMethods: Set { Self.defaultEmptyRequestMethods } + public var emptyResponseCodes: Set { Self.defaultEmptyResponseCodes } + + /// Determines whether the `request` allows empty response bodies, if `request` exists. + /// + /// - Parameter request: `URLRequest` to evaluate. + /// + /// - Returns: `Bool` representing the outcome of the evaluation, or `nil` if `request` was `nil`. + public func requestAllowsEmptyResponseData(_ request: URLRequest?) -> Bool? { + request.flatMap(\.httpMethod) + .flatMap(HTTPMethod.init) + .map { emptyRequestMethods.contains($0) } + } + + /// Determines whether the `response` allows empty response bodies, if `response` exists`. + /// + /// - Parameter response: `HTTPURLResponse` to evaluate. + /// + /// - Returns: `Bool` representing the outcome of the evaluation, or `nil` if `response` was `nil`. + public func responseAllowsEmptyResponseData(_ response: HTTPURLResponse?) -> Bool? { + response.map(\.statusCode) + .map { emptyResponseCodes.contains($0) } + } + + /// Determines whether `request` and `response` allow empty response bodies. + /// + /// - Parameters: + /// - request: `URLRequest` to evaluate. + /// - response: `HTTPURLResponse` to evaluate. + /// + /// - Returns: `true` if `request` or `response` allow empty bodies, `false` otherwise. + public func emptyResponseAllowed(forRequest request: URLRequest?, response: HTTPURLResponse?) -> Bool { + (requestAllowsEmptyResponseData(request) == true) || (responseAllowsEmptyResponseData(response) == true) + } +} + +/// By default, any serializer declared to conform to both types will get file serialization for free, as it just feeds +/// the data read from disk into the data response serializer. +extension DownloadResponseSerializerProtocol where Self: DataResponseSerializerProtocol { + public func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> Self.SerializedObject { + guard error == nil else { throw error! } + + guard let fileURL = fileURL else { + throw AFError.responseSerializationFailed(reason: .inputFileNil) + } + + let data: Data + do { + data = try Data(contentsOf: fileURL) + } catch { + throw AFError.responseSerializationFailed(reason: .inputFileReadFailed(at: fileURL)) + } + + do { + return try serialize(request: request, response: response, data: data, error: error) + } catch { + throw error + } + } +} + +// MARK: - Default + +extension DataRequest { + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + appendResponseSerializer { + // Start work that should be on the serialization queue. + let result = AFResult(value: self.data, error: self.error) + // End work that should be on the serialization queue. + + self.underlyingQueue.async { + let response = DataResponse(request: self.request, + response: self.response, + data: self.data, + metrics: self.metrics, + serializationDuration: 0, + result: result) + + self.eventMonitor?.request(self, didParseResponse: response) + + self.responseSerializerDidComplete { queue.async { completionHandler(response) } } + } + } + + return self + } + + private func _response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDataResponse) -> Void) + -> Self { + appendResponseSerializer { + // Start work that should be on the serialization queue. + let start = ProcessInfo.processInfo.systemUptime + let result: AFResult = Result { + try responseSerializer.serialize(request: self.request, + response: self.response, + data: self.data, + error: self.error) + }.mapError { error in + error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error))) + } + + let end = ProcessInfo.processInfo.systemUptime + // End work that should be on the serialization queue. + + self.underlyingQueue.async { + let response = DataResponse(request: self.request, + response: self.response, + data: self.data, + metrics: self.metrics, + serializationDuration: end - start, + result: result) + + self.eventMonitor?.request(self, didParseResponse: response) + + guard let serializerError = result.failure, let delegate = self.delegate else { + self.responseSerializerDidComplete { queue.async { completionHandler(response) } } + return + } + + delegate.retryResult(for: self, dueTo: serializerError) { retryResult in + var didComplete: (() -> Void)? + + defer { + if let didComplete = didComplete { + self.responseSerializerDidComplete { queue.async { didComplete() } } + } + } + + switch retryResult { + case .doNotRetry: + didComplete = { completionHandler(response) } + + case let .doNotRetryWithError(retryError): + let result: AFResult = .failure(retryError.asAFError(orFailWith: "Received retryError was not already AFError")) + + let response = DataResponse(request: self.request, + response: self.response, + data: self.data, + metrics: self.metrics, + serializationDuration: end - start, + result: result) + + didComplete = { completionHandler(response) } + + case .retry, .retryWithDelay: + delegate.retryRequest(self, withDelay: retryResult.delay) + } + } + } + } + + return self + } + + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default + /// - responseSerializer: The response serializer responsible for serializing the request, response, and data. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDataResponse) -> Void) + -> Self { + _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) + } + + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default + /// - responseSerializer: The response serializer responsible for serializing the request, response, and data. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDataResponse) -> Void) + -> Self { + _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) + } +} + +extension DownloadRequest { + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + completionHandler: @escaping (AFDownloadResponse) -> Void) + -> Self { + appendResponseSerializer { + // Start work that should be on the serialization queue. + let result = AFResult(value: self.fileURL, error: self.error) + // End work that should be on the serialization queue. + + self.underlyingQueue.async { + let response = DownloadResponse(request: self.request, + response: self.response, + fileURL: self.fileURL, + resumeData: self.resumeData, + metrics: self.metrics, + serializationDuration: 0, + result: result) + + self.eventMonitor?.request(self, didParseResponse: response) + + self.responseSerializerDidComplete { queue.async { completionHandler(response) } } + } + } + + return self + } + + private func _response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDownloadResponse) -> Void) + -> Self { + appendResponseSerializer { + // Start work that should be on the serialization queue. + let start = ProcessInfo.processInfo.systemUptime + let result: AFResult = Result { + try responseSerializer.serializeDownload(request: self.request, + response: self.response, + fileURL: self.fileURL, + error: self.error) + }.mapError { error in + error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error))) + } + let end = ProcessInfo.processInfo.systemUptime + // End work that should be on the serialization queue. + + self.underlyingQueue.async { + let response = DownloadResponse(request: self.request, + response: self.response, + fileURL: self.fileURL, + resumeData: self.resumeData, + metrics: self.metrics, + serializationDuration: end - start, + result: result) + + self.eventMonitor?.request(self, didParseResponse: response) + + guard let serializerError = result.failure, let delegate = self.delegate else { + self.responseSerializerDidComplete { queue.async { completionHandler(response) } } + return + } + + delegate.retryResult(for: self, dueTo: serializerError) { retryResult in + var didComplete: (() -> Void)? + + defer { + if let didComplete = didComplete { + self.responseSerializerDidComplete { queue.async { didComplete() } } + } + } + + switch retryResult { + case .doNotRetry: + didComplete = { completionHandler(response) } + + case let .doNotRetryWithError(retryError): + let result: AFResult = .failure(retryError.asAFError(orFailWith: "Received retryError was not already AFError")) + + let response = DownloadResponse(request: self.request, + response: self.response, + fileURL: self.fileURL, + resumeData: self.resumeData, + metrics: self.metrics, + serializationDuration: end - start, + result: result) + + didComplete = { completionHandler(response) } + + case .retry, .retryWithDelay: + delegate.retryRequest(self, withDelay: retryResult.delay) + } + } + } + } + + return self + } + + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - responseSerializer: The response serializer responsible for serializing the request, response, and data + /// contained in the destination `URL`. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDownloadResponse) -> Void) + -> Self { + _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) + } + + /// Adds a handler to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - responseSerializer: The response serializer responsible for serializing the request, response, and data + /// contained in the destination `URL`. + /// - completionHandler: The code to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func response(queue: DispatchQueue = .main, + responseSerializer: Serializer, + completionHandler: @escaping (AFDownloadResponse) -> Void) + -> Self { + _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) + } +} + +// MARK: - URL + +/// A `DownloadResponseSerializerProtocol` that performs only `Error` checking and ensures that a downloaded `fileURL` +/// is present. +public struct URLResponseSerializer: DownloadResponseSerializerProtocol { + /// Creates an instance. + public init() {} + + public func serializeDownload(request: URLRequest?, + response: HTTPURLResponse?, + fileURL: URL?, + error: Error?) throws -> URL { + guard error == nil else { throw error! } + + guard let url = fileURL else { + throw AFError.responseSerializationFailed(reason: .inputFileNil) + } + + return url + } +} + +#if swift(>=5.5) +extension DownloadResponseSerializerProtocol where Self == URLResponseSerializer { + /// Provides a `URLResponseSerializer` instance. + public static var url: URLResponseSerializer { URLResponseSerializer() } +} +#endif + +extension DownloadRequest { + /// Adds a handler using a `URLResponseSerializer` to be called once the request is finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is called. `.main` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseURL(queue: DispatchQueue = .main, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, responseSerializer: URLResponseSerializer(), completionHandler: completionHandler) + } +} + +// MARK: - Data + +/// A `ResponseSerializer` that performs minimal response checking and returns any response `Data` as-is. By default, a +/// request returning `nil` or no data is considered an error. However, if the request has an `HTTPMethod` or the +/// response has an HTTP status code valid for empty responses, then an empty `Data` value is returned. +public final class DataResponseSerializer: ResponseSerializer { + public let dataPreprocessor: DataPreprocessor + public let emptyResponseCodes: Set + public let emptyRequestMethods: Set + + /// Creates a `DataResponseSerializer` using the provided parameters. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + public init(dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) { + self.dataPreprocessor = dataPreprocessor + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + } + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Data { + guard error == nil else { throw error! } + + guard var data = data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) + } + + return Data() + } + + data = try dataPreprocessor.preprocess(data) + + return data + } +} + +#if swift(>=5.5) +extension ResponseSerializer where Self == DataResponseSerializer { + /// Provides a default `DataResponseSerializer` instance. + public static var data: DataResponseSerializer { DataResponseSerializer() } + + /// Creates a `DataResponseSerializer` using the provided parameters. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + /// + /// - Returns: The `DataResponseSerializer`. + public static func data(dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) -> DataResponseSerializer { + DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods) + } +} +#endif + +extension DataRequest { + /// Adds a handler using a `DataResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is called. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseData(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } +} + +extension DownloadRequest { + /// Adds a handler using a `DataResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is called. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseData(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } +} + +// MARK: - String + +/// A `ResponseSerializer` that decodes the response data as a `String`. By default, a request returning `nil` or no +/// data is considered an error. However, if the request has an `HTTPMethod` or the response has an HTTP status code +/// valid for empty responses, then an empty `String` is returned. +public final class StringResponseSerializer: ResponseSerializer { + public let dataPreprocessor: DataPreprocessor + /// Optional string encoding used to validate the response. + public let encoding: String.Encoding? + public let emptyResponseCodes: Set + public let emptyRequestMethods: Set + + /// Creates an instance with the provided values. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - encoding: A string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + public init(dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) { + self.dataPreprocessor = dataPreprocessor + self.encoding = encoding + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + } + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> String { + guard error == nil else { throw error! } + + guard var data = data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) + } + + return "" + } + + data = try dataPreprocessor.preprocess(data) + + var convertedEncoding = encoding + + if let encodingName = response?.textEncodingName, convertedEncoding == nil { + convertedEncoding = String.Encoding(ianaCharsetName: encodingName) + } + + let actualEncoding = convertedEncoding ?? .isoLatin1 + + guard let string = String(data: data, encoding: actualEncoding) else { + throw AFError.responseSerializationFailed(reason: .stringSerializationFailed(encoding: actualEncoding)) + } + + return string + } +} + +#if swift(>=5.5) +extension ResponseSerializer where Self == StringResponseSerializer { + /// Provides a default `StringResponseSerializer` instance. + public static var string: StringResponseSerializer { StringResponseSerializer() } + + /// Creates a `StringResponseSerializer` with the provided values. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - encoding: A string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + /// + /// - Returns: The `StringResponseSerializer`. + public static func string(dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) -> StringResponseSerializer { + StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods) + } +} +#endif + +extension DataRequest { + /// Adds a handler using a `StringResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseString(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } +} + +extension DownloadRequest { + /// Adds a handler using a `StringResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseString(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } +} + +// MARK: - JSON + +/// A `ResponseSerializer` that decodes the response data using `JSONSerialization`. By default, a request returning +/// `nil` or no data is considered an error. However, if the request has an `HTTPMethod` or the response has an +/// HTTP status code valid for empty responses, then an `NSNull` value is returned. +@available(*, deprecated, message: "JSONResponseSerializer deprecated and will be removed in Alamofire 6. Use DecodableResponseSerializer instead.") +public final class JSONResponseSerializer: ResponseSerializer { + public let dataPreprocessor: DataPreprocessor + public let emptyResponseCodes: Set + public let emptyRequestMethods: Set + /// `JSONSerialization.ReadingOptions` used when serializing a response. + public let options: JSONSerialization.ReadingOptions + + /// Creates an instance with the provided values. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + /// - options: The options to use. `.allowFragments` by default. + public init(dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = JSONResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = JSONResponseSerializer.defaultEmptyRequestMethods, + options: JSONSerialization.ReadingOptions = .allowFragments) { + self.dataPreprocessor = dataPreprocessor + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + self.options = options + } + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Any { + guard error == nil else { throw error! } + + guard var data = data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) + } + + return NSNull() + } + + data = try dataPreprocessor.preprocess(data) + + do { + return try JSONSerialization.jsonObject(with: data, options: options) + } catch { + throw AFError.responseSerializationFailed(reason: .jsonSerializationFailed(error: error)) + } + } +} + +extension DataRequest { + /// Adds a handler using a `JSONResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - options: `JSONSerialization.ReadingOptions` used when parsing the response. `.allowFragments` + /// by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @available(*, deprecated, message: "responseJSON deprecated and will be removed in Alamofire 6. Use responseDecodable instead.") + @discardableResult + public func responseJSON(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = JSONResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = JSONResponseSerializer.defaultEmptyRequestMethods, + options: JSONSerialization.ReadingOptions = .allowFragments, + completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods, + options: options), + completionHandler: completionHandler) + } +} + +extension DownloadRequest { + /// Adds a handler using a `JSONResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - options: `JSONSerialization.ReadingOptions` used when parsing the response. `.allowFragments` + /// by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @available(*, deprecated, message: "responseJSON deprecated and will be removed in Alamofire 6. Use responseDecodable instead.") + @discardableResult + public func responseJSON(queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = JSONResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = JSONResponseSerializer.defaultEmptyRequestMethods, + options: JSONSerialization.ReadingOptions = .allowFragments, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods, + options: options), + completionHandler: completionHandler) + } +} + +// MARK: - Empty + +/// Protocol representing an empty response. Use `T.emptyValue()` to get an instance. +public protocol EmptyResponse { + /// Empty value for the conforming type. + /// + /// - Returns: Value of `Self` to use for empty values. + static func emptyValue() -> Self +} + +/// Type representing an empty value. Use `Empty.value` to get the static instance. +public struct Empty: Codable { + /// Static `Empty` instance used for all `Empty` responses. + public static let value = Empty() +} + +extension Empty: EmptyResponse { + public static func emptyValue() -> Empty { + value + } +} + +// MARK: - DataDecoder Protocol + +/// Any type which can decode `Data` into a `Decodable` type. +public protocol DataDecoder { + /// Decode `Data` into the provided type. + /// + /// - Parameters: + /// - type: The `Type` to be decoded. + /// - data: The `Data` to be decoded. + /// + /// - Returns: The decoded value of type `D`. + /// - Throws: Any error that occurs during decode. + func decode(_ type: D.Type, from data: Data) throws -> D +} + +/// `JSONDecoder` automatically conforms to `DataDecoder`. +extension JSONDecoder: DataDecoder {} +/// `PropertyListDecoder` automatically conforms to `DataDecoder`. +extension PropertyListDecoder: DataDecoder {} + +// MARK: - Decodable + +/// A `ResponseSerializer` that decodes the response data as a generic value using any type that conforms to +/// `DataDecoder`. By default, this is an instance of `JSONDecoder`. Additionally, a request returning `nil` or no data +/// is considered an error. However, if the request has an `HTTPMethod` or the response has an HTTP status code valid +/// for empty responses then an empty value will be returned. If the decoded type conforms to `EmptyResponse`, the +/// type's `emptyValue()` will be returned. If the decoded type is `Empty`, the `.value` instance is returned. If the +/// decoded type *does not* conform to `EmptyResponse` and isn't `Empty`, an error will be produced. +public final class DecodableResponseSerializer: ResponseSerializer { + public let dataPreprocessor: DataPreprocessor + /// The `DataDecoder` instance used to decode responses. + public let decoder: DataDecoder + public let emptyResponseCodes: Set + public let emptyRequestMethods: Set + + /// Creates an instance using the values provided. + /// + /// - Parameters: + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - decoder: The `DataDecoder`. `JSONDecoder()` by default. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + public init(dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) { + self.dataPreprocessor = dataPreprocessor + self.decoder = decoder + self.emptyResponseCodes = emptyResponseCodes + self.emptyRequestMethods = emptyRequestMethods + } + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> T { + guard error == nil else { throw error! } + + guard var data = data, !data.isEmpty else { + guard emptyResponseAllowed(forRequest: request, response: response) else { + throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) + } + + guard let emptyResponseType = T.self as? EmptyResponse.Type, let emptyValue = emptyResponseType.emptyValue() as? T else { + throw AFError.responseSerializationFailed(reason: .invalidEmptyResponse(type: "\(T.self)")) + } + + return emptyValue + } + + data = try dataPreprocessor.preprocess(data) + + do { + return try decoder.decode(T.self, from: data) + } catch { + throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error)) + } + } +} + +#if swift(>=5.5) +extension ResponseSerializer { + /// Creates a `DecodableResponseSerializer` using the values provided. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. + /// - decoder: The `DataDecoder`. `JSONDecoder()` by default. + /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. + /// + /// - Returns: The `DecodableResponseSerializer`. + public static func decodable(of type: T.Type, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DecodableResponseSerializer where Self == DecodableResponseSerializer { + DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods) + } +} +#endif + +extension DataRequest { + /// Adds a handler using a `DecodableResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseDecodable(of type: T.Type = T.self, + queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDataResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } +} + +extension DownloadRequest { + /// Adds a handler using a `DecodableResponseSerializer` to be called once the request has finished. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - queue: The queue on which the completion handler is dispatched. `.main` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// `completionHandler`. `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. + /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined + /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// - completionHandler: A closure to be executed once the request has finished. + /// + /// - Returns: The request. + @discardableResult + public func responseDecodable(of type: T.Type = T.self, + queue: DispatchQueue = .main, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, + completionHandler: @escaping (AFDownloadResponse) -> Void) -> Self { + response(queue: queue, + responseSerializer: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + completionHandler: completionHandler) + } +} + +// MARK: - DataStreamRequest + +/// A type which can serialize incoming `Data`. +public protocol DataStreamSerializer { + /// Type produced from the serialized `Data`. + associatedtype SerializedObject + + /// Serializes incoming `Data` into a `SerializedObject` value. + /// + /// - Parameter data: `Data` to be serialized. + /// + /// - Throws: Any error produced during serialization. + func serialize(_ data: Data) throws -> SerializedObject +} + +/// `DataStreamSerializer` which uses the provided `DataPreprocessor` and `DataDecoder` to serialize the incoming `Data`. +public struct DecodableStreamSerializer: DataStreamSerializer { + /// `DataDecoder` used to decode incoming `Data`. + public let decoder: DataDecoder + /// `DataPreprocessor` incoming `Data` is passed through before being passed to the `DataDecoder`. + public let dataPreprocessor: DataPreprocessor + + /// Creates an instance with the provided `DataDecoder` and `DataPreprocessor`. + /// - Parameters: + /// - decoder: ` DataDecoder` used to decode incoming `Data`. `JSONDecoder()` by default. + /// - dataPreprocessor: `DataPreprocessor` used to process incoming `Data` before it's passed through the + /// `decoder`. `PassthroughPreprocessor()` by default. + public init(decoder: DataDecoder = JSONDecoder(), dataPreprocessor: DataPreprocessor = PassthroughPreprocessor()) { + self.decoder = decoder + self.dataPreprocessor = dataPreprocessor + } + + public func serialize(_ data: Data) throws -> T { + let processedData = try dataPreprocessor.preprocess(data) + do { + return try decoder.decode(T.self, from: processedData) + } catch { + throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error)) + } + } +} + +/// `DataStreamSerializer` which performs no serialization on incoming `Data`. +public struct PassthroughStreamSerializer: DataStreamSerializer { + /// Creates an instance. + public init() {} + + public func serialize(_ data: Data) throws -> Data { data } +} + +/// `DataStreamSerializer` which serializes incoming stream `Data` into `UTF8`-decoded `String` values. +public struct StringStreamSerializer: DataStreamSerializer { + /// Creates an instance. + public init() {} + + public func serialize(_ data: Data) throws -> String { + String(decoding: data, as: UTF8.self) + } +} + +#if swift(>=5.5) +extension DataStreamSerializer { + /// Creates a `DecodableStreamSerializer` instance with the provided `DataDecoder` and `DataPreprocessor`. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from stream data. + /// - decoder: ` DataDecoder` used to decode incoming `Data`. `JSONDecoder()` by default. + /// - dataPreprocessor: `DataPreprocessor` used to process incoming `Data` before it's passed through the + /// `decoder`. `PassthroughPreprocessor()` by default. + public static func decodable(of type: T.Type, + decoder: DataDecoder = JSONDecoder(), + dataPreprocessor: DataPreprocessor = PassthroughPreprocessor()) -> Self where Self == DecodableStreamSerializer { + DecodableStreamSerializer(decoder: decoder, dataPreprocessor: dataPreprocessor) + } +} + +extension DataStreamSerializer where Self == PassthroughStreamSerializer { + /// Provides a `PassthroughStreamSerializer` instance. + public static var passthrough: PassthroughStreamSerializer { PassthroughStreamSerializer() } +} + +extension DataStreamSerializer where Self == StringStreamSerializer { + /// Provides a `StringStreamSerializer` instance. + public static var string: StringStreamSerializer { StringStreamSerializer() } +} +#endif + +extension DataStreamRequest { + /// Adds a `StreamHandler` which performs no parsing on incoming `Data`. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. + /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func responseStream(on queue: DispatchQueue = .main, stream: @escaping Handler) -> Self { + let parser = { [unowned self] (data: Data) in + queue.async { + self.capturingError { + try stream(.init(event: .stream(.success(data)), token: .init(self))) + } + + self.updateAndCompleteIfPossible() + } + } + + $streamMutableState.write { $0.streams.append(parser) } + appendStreamCompletion(on: queue, stream: stream) + + return self + } + + /// Adds a `StreamHandler` which uses the provided `DataStreamSerializer` to process incoming `Data`. + /// + /// - Parameters: + /// - serializer: `DataStreamSerializer` used to process incoming `Data`. Its work is done on the `serializationQueue`. + /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. + /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func responseStream(using serializer: Serializer, + on queue: DispatchQueue = .main, + stream: @escaping Handler) -> Self { + let parser = { [unowned self] (data: Data) in + self.serializationQueue.async { + // Start work on serialization queue. + let result = Result { try serializer.serialize(data) } + .mapError { $0.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: $0))) } + // End work on serialization queue. + self.underlyingQueue.async { + self.eventMonitor?.request(self, didParseStream: result) + + if result.isFailure, self.automaticallyCancelOnStreamError { + self.cancel() + } + + queue.async { + self.capturingError { + try stream(.init(event: .stream(result), token: .init(self))) + } + + self.updateAndCompleteIfPossible() + } + } + } + } + + $streamMutableState.write { $0.streams.append(parser) } + appendStreamCompletion(on: queue, stream: stream) + + return self + } + + /// Adds a `StreamHandler` which parses incoming `Data` as a UTF8 `String`. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. + /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func responseStreamString(on queue: DispatchQueue = .main, + stream: @escaping Handler) -> Self { + let parser = { [unowned self] (data: Data) in + self.serializationQueue.async { + // Start work on serialization queue. + let string = String(decoding: data, as: UTF8.self) + // End work on serialization queue. + self.underlyingQueue.async { + self.eventMonitor?.request(self, didParseStream: .success(string)) + + queue.async { + self.capturingError { + try stream(.init(event: .stream(.success(string)), token: .init(self))) + } + + self.updateAndCompleteIfPossible() + } + } + } + } + + $streamMutableState.write { $0.streams.append(parser) } + appendStreamCompletion(on: queue, stream: stream) + + return self + } + + private func updateAndCompleteIfPossible() { + $streamMutableState.write { state in + state.numberOfExecutingStreams -= 1 + + guard state.numberOfExecutingStreams == 0, !state.enqueuedCompletionEvents.isEmpty else { return } + + let completionEvents = state.enqueuedCompletionEvents + self.underlyingQueue.async { completionEvents.forEach { $0() } } + state.enqueuedCompletionEvents.removeAll() + } + } + + /// Adds a `StreamHandler` which parses incoming `Data` using the provided `DataDecoder`. + /// + /// - Parameters: + /// - type: `Decodable` type to parse incoming `Data` into. + /// - queue: `DispatchQueue` on which to perform `StreamHandler` closure. + /// - decoder: `DataDecoder` used to decode the incoming `Data`. + /// - preprocessor: `DataPreprocessor` used to process the incoming `Data` before it's passed to the `decoder`. + /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. + /// + /// - Returns: The `DataStreamRequest`. + @discardableResult + public func responseStreamDecodable(of type: T.Type = T.self, + on queue: DispatchQueue = .main, + using decoder: DataDecoder = JSONDecoder(), + preprocessor: DataPreprocessor = PassthroughPreprocessor(), + stream: @escaping Handler) -> Self { + responseStream(using: DecodableStreamSerializer(decoder: decoder, dataPreprocessor: preprocessor), + stream: stream) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/Result+Alamofire.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Result+Alamofire.swift new file mode 100644 index 0000000..39ac286 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Result+Alamofire.swift @@ -0,0 +1,120 @@ +// +// Result+Alamofire.swift +// +// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Default type of `Result` returned by Alamofire, with an `AFError` `Failure` type. +public typealias AFResult = Result + +// MARK: - Internal APIs + +extension Result { + /// Returns whether the instance is `.success`. + var isSuccess: Bool { + guard case .success = self else { return false } + return true + } + + /// Returns whether the instance is `.failure`. + var isFailure: Bool { + !isSuccess + } + + /// Returns the associated value if the result is a success, `nil` otherwise. + var success: Success? { + guard case let .success(value) = self else { return nil } + return value + } + + /// Returns the associated error value if the result is a failure, `nil` otherwise. + var failure: Failure? { + guard case let .failure(error) = self else { return nil } + return error + } + + /// Initializes a `Result` from value or error. Returns `.failure` if the error is non-nil, `.success` otherwise. + /// + /// - Parameters: + /// - value: A value. + /// - error: An `Error`. + init(value: Success, error: Failure?) { + if let error = error { + self = .failure(error) + } else { + self = .success(value) + } + } + + /// Evaluates the specified closure when the `Result` is a success, passing the unwrapped value as a parameter. + /// + /// Use the `tryMap` method with a closure that may throw an error. For example: + /// + /// let possibleData: Result = .success(Data(...)) + /// let possibleObject = possibleData.tryMap { + /// try JSONSerialization.jsonObject(with: $0) + /// } + /// + /// - parameter transform: A closure that takes the success value of the instance. + /// + /// - returns: A `Result` containing the result of the given closure. If this instance is a failure, returns the + /// same failure. + func tryMap(_ transform: (Success) throws -> NewSuccess) -> Result { + switch self { + case let .success(value): + do { + return try .success(transform(value)) + } catch { + return .failure(error) + } + case let .failure(error): + return .failure(error) + } + } + + /// Evaluates the specified closure when the `Result` is a failure, passing the unwrapped error as a parameter. + /// + /// Use the `tryMapError` function with a closure that may throw an error. For example: + /// + /// let possibleData: Result = .success(Data(...)) + /// let possibleObject = possibleData.tryMapError { + /// try someFailableFunction(taking: $0) + /// } + /// + /// - Parameter transform: A throwing closure that takes the error of the instance. + /// + /// - Returns: A `Result` instance containing the result of the transform. If this instance is a success, returns + /// the same success. + func tryMapError(_ transform: (Failure) throws -> NewFailure) -> Result { + switch self { + case let .failure(error): + do { + return try .failure(transform(error)) + } catch { + return .failure(error) + } + case let .success(value): + return .success(value) + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/RetryPolicy.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/RetryPolicy.swift new file mode 100644 index 0000000..f6fd8d3 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/RetryPolicy.swift @@ -0,0 +1,434 @@ +// +// RetryPolicy.swift +// +// Copyright (c) 2019-2020 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// A retry policy that retries requests using an exponential backoff for allowed HTTP methods and HTTP status codes +/// as well as certain types of networking errors. +open class RetryPolicy: RequestInterceptor { + /// The default retry limit for retry policies. + public static let defaultRetryLimit: UInt = 2 + + /// The default exponential backoff base for retry policies (must be a minimum of 2). + public static let defaultExponentialBackoffBase: UInt = 2 + + /// The default exponential backoff scale for retry policies. + public static let defaultExponentialBackoffScale: Double = 0.5 + + /// The default HTTP methods to retry. + /// See [RFC 2616 - Section 9.1.2](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html) for more information. + public static let defaultRetryableHTTPMethods: Set = [.delete, // [Delete](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7) - not always idempotent + .get, // [GET](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3) - generally idempotent + .head, // [HEAD](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4) - generally idempotent + .options, // [OPTIONS](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2) - inherently idempotent + .put, // [PUT](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6) - not always idempotent + .trace // [TRACE](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8) - inherently idempotent + ] + + /// The default HTTP status codes to retry. + /// See [RFC 2616 - Section 10](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10) for more information. + public static let defaultRetryableHTTPStatusCodes: Set = [408, // [Request Timeout](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.9) + 500, // [Internal Server Error](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.1) + 502, // [Bad Gateway](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.3) + 503, // [Service Unavailable](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4) + 504 // [Gateway Timeout](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.5) + ] + + /// The default URL error codes to retry. + public static let defaultRetryableURLErrorCodes: Set = [// [Security] App Transport Security disallowed a connection because there is no secure network connection. + // - [Disabled] ATS settings do not change at runtime. + // .appTransportSecurityRequiresSecureConnection, + + // [System] An app or app extension attempted to connect to a background session that is already connected to a + // process. + // - [Enabled] The other process could release the background session. + .backgroundSessionInUseByAnotherProcess, + + // [System] The shared container identifier of the URL session configuration is needed but has not been set. + // - [Disabled] Cannot change at runtime. + // .backgroundSessionRequiresSharedContainer, + + // [System] The app is suspended or exits while a background data task is processing. + // - [Enabled] App can be foregrounded or launched to recover. + .backgroundSessionWasDisconnected, + + // [Network] The URL Loading system received bad data from the server. + // - [Enabled] Server could return valid data when retrying. + .badServerResponse, + + // [Resource] A malformed URL prevented a URL request from being initiated. + // - [Disabled] URL was most likely constructed incorrectly. + // .badURL, + + // [System] A connection was attempted while a phone call is active on a network that does not support + // simultaneous phone and data communication (EDGE or GPRS). + // - [Enabled] Phone call could be ended to allow request to recover. + .callIsActive, + + // [Client] An asynchronous load has been canceled. + // - [Disabled] Request was cancelled by the client. + // .cancelled, + + // [File System] A download task couldn’t close the downloaded file on disk. + // - [Disabled] File system error is unlikely to recover with retry. + // .cannotCloseFile, + + // [Network] An attempt to connect to a host failed. + // - [Enabled] Server or DNS lookup could recover during retry. + .cannotConnectToHost, + + // [File System] A download task couldn’t create the downloaded file on disk because of an I/O failure. + // - [Disabled] File system error is unlikely to recover with retry. + // .cannotCreateFile, + + // [Data] Content data received during a connection request had an unknown content encoding. + // - [Disabled] Server is unlikely to modify the content encoding during a retry. + // .cannotDecodeContentData, + + // [Data] Content data received during a connection request could not be decoded for a known content encoding. + // - [Disabled] Server is unlikely to modify the content encoding during a retry. + // .cannotDecodeRawData, + + // [Network] The host name for a URL could not be resolved. + // - [Enabled] Server or DNS lookup could recover during retry. + .cannotFindHost, + + // [Network] A request to load an item only from the cache could not be satisfied. + // - [Enabled] Cache could be populated during a retry. + .cannotLoadFromNetwork, + + // [File System] A download task was unable to move a downloaded file on disk. + // - [Disabled] File system error is unlikely to recover with retry. + // .cannotMoveFile, + + // [File System] A download task was unable to open the downloaded file on disk. + // - [Disabled] File system error is unlikely to recover with retry. + // .cannotOpenFile, + + // [Data] A task could not parse a response. + // - [Disabled] Invalid response is unlikely to recover with retry. + // .cannotParseResponse, + + // [File System] A download task was unable to remove a downloaded file from disk. + // - [Disabled] File system error is unlikely to recover with retry. + // .cannotRemoveFile, + + // [File System] A download task was unable to write to the downloaded file on disk. + // - [Disabled] File system error is unlikely to recover with retry. + // .cannotWriteToFile, + + // [Security] A client certificate was rejected. + // - [Disabled] Client certificate is unlikely to change with retry. + // .clientCertificateRejected, + + // [Security] A client certificate was required to authenticate an SSL connection during a request. + // - [Disabled] Client certificate is unlikely to be provided with retry. + // .clientCertificateRequired, + + // [Data] The length of the resource data exceeds the maximum allowed. + // - [Disabled] Resource will likely still exceed the length maximum on retry. + // .dataLengthExceedsMaximum, + + // [System] The cellular network disallowed a connection. + // - [Enabled] WiFi connection could be established during retry. + .dataNotAllowed, + + // [Network] The host address could not be found via DNS lookup. + // - [Enabled] DNS lookup could succeed during retry. + .dnsLookupFailed, + + // [Data] A download task failed to decode an encoded file during the download. + // - [Enabled] Server could correct the decoding issue with retry. + .downloadDecodingFailedMidStream, + + // [Data] A download task failed to decode an encoded file after downloading. + // - [Enabled] Server could correct the decoding issue with retry. + .downloadDecodingFailedToComplete, + + // [File System] A file does not exist. + // - [Disabled] File system error is unlikely to recover with retry. + // .fileDoesNotExist, + + // [File System] A request for an FTP file resulted in the server responding that the file is not a plain file, + // but a directory. + // - [Disabled] FTP directory is not likely to change to a file during a retry. + // .fileIsDirectory, + + // [Network] A redirect loop has been detected or the threshold for number of allowable redirects has been + // exceeded (currently 16). + // - [Disabled] The redirect loop is unlikely to be resolved within the retry window. + // .httpTooManyRedirects, + + // [System] The attempted connection required activating a data context while roaming, but international roaming + // is disabled. + // - [Enabled] WiFi connection could be established during retry. + .internationalRoamingOff, + + // [Connectivity] A client or server connection was severed in the middle of an in-progress load. + // - [Enabled] A network connection could be established during retry. + .networkConnectionLost, + + // [File System] A resource couldn’t be read because of insufficient permissions. + // - [Disabled] Permissions are unlikely to be granted during retry. + // .noPermissionsToReadFile, + + // [Connectivity] A network resource was requested, but an internet connection has not been established and + // cannot be established automatically. + // - [Enabled] A network connection could be established during retry. + .notConnectedToInternet, + + // [Resource] A redirect was specified by way of server response code, but the server did not accompany this + // code with a redirect URL. + // - [Disabled] The redirect URL is unlikely to be supplied during a retry. + // .redirectToNonExistentLocation, + + // [Client] A body stream is needed but the client did not provide one. + // - [Disabled] The client will be unlikely to supply a body stream during retry. + // .requestBodyStreamExhausted, + + // [Resource] A requested resource couldn’t be retrieved. + // - [Disabled] The resource is unlikely to become available during the retry window. + // .resourceUnavailable, + + // [Security] An attempt to establish a secure connection failed for reasons that can’t be expressed more + // specifically. + // - [Enabled] The secure connection could be established during a retry given the lack of specificity + // provided by the error. + .secureConnectionFailed, + + // [Security] A server certificate had a date which indicates it has expired, or is not yet valid. + // - [Enabled] The server certificate could become valid within the retry window. + .serverCertificateHasBadDate, + + // [Security] A server certificate was not signed by any root server. + // - [Disabled] The server certificate is unlikely to change during the retry window. + // .serverCertificateHasUnknownRoot, + + // [Security] A server certificate is not yet valid. + // - [Enabled] The server certificate could become valid within the retry window. + .serverCertificateNotYetValid, + + // [Security] A server certificate was signed by a root server that isn’t trusted. + // - [Disabled] The server certificate is unlikely to become trusted within the retry window. + // .serverCertificateUntrusted, + + // [Network] An asynchronous operation timed out. + // - [Enabled] The request timed out for an unknown reason and should be retried. + .timedOut + + // [System] The URL Loading System encountered an error that it can’t interpret. + // - [Disabled] The error could not be interpreted and is unlikely to be recovered from during a retry. + // .unknown, + + // [Resource] A properly formed URL couldn’t be handled by the framework. + // - [Disabled] The URL is unlikely to change during a retry. + // .unsupportedURL, + + // [Client] Authentication is required to access a resource. + // - [Disabled] The user authentication is unlikely to be provided by retrying. + // .userAuthenticationRequired, + + // [Client] An asynchronous request for authentication has been canceled by the user. + // - [Disabled] The user cancelled authentication and explicitly took action to not retry. + // .userCancelledAuthentication, + + // [Resource] A server reported that a URL has a non-zero content length, but terminated the network connection + // gracefully without sending any data. + // - [Disabled] The server is unlikely to provide data during the retry window. + // .zeroByteResource, + ] + + /// The total number of times the request is allowed to be retried. + public let retryLimit: UInt + + /// The base of the exponential backoff policy (should always be greater than or equal to 2). + public let exponentialBackoffBase: UInt + + /// The scale of the exponential backoff. + public let exponentialBackoffScale: Double + + /// The HTTP methods that are allowed to be retried. + public let retryableHTTPMethods: Set + + /// The HTTP status codes that are automatically retried by the policy. + public let retryableHTTPStatusCodes: Set + + /// The URL error codes that are automatically retried by the policy. + public let retryableURLErrorCodes: Set + + /// Creates a `RetryPolicy` from the specified parameters. + /// + /// - Parameters: + /// - retryLimit: The total number of times the request is allowed to be retried. `2` by default. + /// - exponentialBackoffBase: The base of the exponential backoff policy. `2` by default. + /// - exponentialBackoffScale: The scale of the exponential backoff. `0.5` by default. + /// - retryableHTTPMethods: The HTTP methods that are allowed to be retried. + /// `RetryPolicy.defaultRetryableHTTPMethods` by default. + /// - retryableHTTPStatusCodes: The HTTP status codes that are automatically retried by the policy. + /// `RetryPolicy.defaultRetryableHTTPStatusCodes` by default. + /// - retryableURLErrorCodes: The URL error codes that are automatically retried by the policy. + /// `RetryPolicy.defaultRetryableURLErrorCodes` by default. + public init(retryLimit: UInt = RetryPolicy.defaultRetryLimit, + exponentialBackoffBase: UInt = RetryPolicy.defaultExponentialBackoffBase, + exponentialBackoffScale: Double = RetryPolicy.defaultExponentialBackoffScale, + retryableHTTPMethods: Set = RetryPolicy.defaultRetryableHTTPMethods, + retryableHTTPStatusCodes: Set = RetryPolicy.defaultRetryableHTTPStatusCodes, + retryableURLErrorCodes: Set = RetryPolicy.defaultRetryableURLErrorCodes) { + precondition(exponentialBackoffBase >= 2, "The `exponentialBackoffBase` must be a minimum of 2.") + + self.retryLimit = retryLimit + self.exponentialBackoffBase = exponentialBackoffBase + self.exponentialBackoffScale = exponentialBackoffScale + self.retryableHTTPMethods = retryableHTTPMethods + self.retryableHTTPStatusCodes = retryableHTTPStatusCodes + self.retryableURLErrorCodes = retryableURLErrorCodes + } + + open func retry(_ request: Request, + for session: Session, + dueTo error: Error, + completion: @escaping (RetryResult) -> Void) { + if request.retryCount < retryLimit, shouldRetry(request: request, dueTo: error) { + completion(.retryWithDelay(pow(Double(exponentialBackoffBase), Double(request.retryCount)) * exponentialBackoffScale)) + } else { + completion(.doNotRetry) + } + } + + /// Determines whether or not to retry the provided `Request`. + /// + /// - Parameters: + /// - request: `Request` that failed due to the provided `Error`. + /// - error: `Error` encountered while executing the `Request`. + /// + /// - Returns: `Bool` determining whether or not to retry the `Request`. + open func shouldRetry(request: Request, dueTo error: Error) -> Bool { + guard let httpMethod = request.request?.method, retryableHTTPMethods.contains(httpMethod) else { return false } + + if let statusCode = request.response?.statusCode, retryableHTTPStatusCodes.contains(statusCode) { + return true + } else { + let errorCode = (error as? URLError)?.code + let afErrorCode = (error.asAFError?.underlyingError as? URLError)?.code + + guard let code = errorCode ?? afErrorCode else { return false } + + return retryableURLErrorCodes.contains(code) + } + } +} + +#if swift(>=5.5) +extension RequestInterceptor where Self == RetryPolicy { + /// Provides a default `RetryPolicy` instance. + public static var retryPolicy: RetryPolicy { RetryPolicy() } + + /// Creates an `RetryPolicy` from the specified parameters. + /// + /// - Parameters: + /// - retryLimit: The total number of times the request is allowed to be retried. `2` by default. + /// - exponentialBackoffBase: The base of the exponential backoff policy. `2` by default. + /// - exponentialBackoffScale: The scale of the exponential backoff. `0.5` by default. + /// - retryableHTTPMethods: The HTTP methods that are allowed to be retried. + /// `RetryPolicy.defaultRetryableHTTPMethods` by default. + /// - retryableHTTPStatusCodes: The HTTP status codes that are automatically retried by the policy. + /// `RetryPolicy.defaultRetryableHTTPStatusCodes` by default. + /// - retryableURLErrorCodes: The URL error codes that are automatically retried by the policy. + /// `RetryPolicy.defaultRetryableURLErrorCodes` by default. + /// + /// - Returns: The `RetryPolicy` + public static func retryPolicy(retryLimit: UInt = RetryPolicy.defaultRetryLimit, + exponentialBackoffBase: UInt = RetryPolicy.defaultExponentialBackoffBase, + exponentialBackoffScale: Double = RetryPolicy.defaultExponentialBackoffScale, + retryableHTTPMethods: Set = RetryPolicy.defaultRetryableHTTPMethods, + retryableHTTPStatusCodes: Set = RetryPolicy.defaultRetryableHTTPStatusCodes, + retryableURLErrorCodes: Set = RetryPolicy.defaultRetryableURLErrorCodes) -> RetryPolicy { + RetryPolicy(retryLimit: retryLimit, + exponentialBackoffBase: exponentialBackoffBase, + exponentialBackoffScale: exponentialBackoffScale, + retryableHTTPMethods: retryableHTTPMethods, + retryableHTTPStatusCodes: retryableHTTPStatusCodes, + retryableURLErrorCodes: retryableURLErrorCodes) + } +} +#endif + +// MARK: - + +/// A retry policy that automatically retries idempotent requests for network connection lost errors. For more +/// information about retrying network connection lost errors, please refer to Apple's +/// [technical document](https://developer.apple.com/library/content/qa/qa1941/_index.html). +open class ConnectionLostRetryPolicy: RetryPolicy { + /// Creates a `ConnectionLostRetryPolicy` instance from the specified parameters. + /// + /// - Parameters: + /// - retryLimit: The total number of times the request is allowed to be retried. + /// `RetryPolicy.defaultRetryLimit` by default. + /// - exponentialBackoffBase: The base of the exponential backoff policy. + /// `RetryPolicy.defaultExponentialBackoffBase` by default. + /// - exponentialBackoffScale: The scale of the exponential backoff. + /// `RetryPolicy.defaultExponentialBackoffScale` by default. + /// - retryableHTTPMethods: The idempotent http methods to retry. + /// `RetryPolicy.defaultRetryableHTTPMethods` by default. + public init(retryLimit: UInt = RetryPolicy.defaultRetryLimit, + exponentialBackoffBase: UInt = RetryPolicy.defaultExponentialBackoffBase, + exponentialBackoffScale: Double = RetryPolicy.defaultExponentialBackoffScale, + retryableHTTPMethods: Set = RetryPolicy.defaultRetryableHTTPMethods) { + super.init(retryLimit: retryLimit, + exponentialBackoffBase: exponentialBackoffBase, + exponentialBackoffScale: exponentialBackoffScale, + retryableHTTPMethods: retryableHTTPMethods, + retryableHTTPStatusCodes: [], + retryableURLErrorCodes: [.networkConnectionLost]) + } +} + +#if swift(>=5.5) +extension RequestInterceptor where Self == ConnectionLostRetryPolicy { + /// Provides a default `ConnectionLostRetryPolicy` instance. + public static var connectionLostRetryPolicy: ConnectionLostRetryPolicy { ConnectionLostRetryPolicy() } + + /// Creates a `ConnectionLostRetryPolicy` instance from the specified parameters. + /// + /// - Parameters: + /// - retryLimit: The total number of times the request is allowed to be retried. + /// `RetryPolicy.defaultRetryLimit` by default. + /// - exponentialBackoffBase: The base of the exponential backoff policy. + /// `RetryPolicy.defaultExponentialBackoffBase` by default. + /// - exponentialBackoffScale: The scale of the exponential backoff. + /// `RetryPolicy.defaultExponentialBackoffScale` by default. + /// - retryableHTTPMethods: The idempotent http methods to retry. + /// + /// - Returns: The `ConnectionLostRetryPolicy`. + public static func connectionLostRetryPolicy(retryLimit: UInt = RetryPolicy.defaultRetryLimit, + exponentialBackoffBase: UInt = RetryPolicy.defaultExponentialBackoffBase, + exponentialBackoffScale: Double = RetryPolicy.defaultExponentialBackoffScale, + retryableHTTPMethods: Set = RetryPolicy.defaultRetryableHTTPMethods) -> ConnectionLostRetryPolicy { + ConnectionLostRetryPolicy(retryLimit: retryLimit, + exponentialBackoffBase: exponentialBackoffBase, + exponentialBackoffScale: exponentialBackoffScale, + retryableHTTPMethods: retryableHTTPMethods) + } +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/ServerTrustEvaluation.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/ServerTrustEvaluation.swift new file mode 100644 index 0000000..06abf19 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/ServerTrustEvaluation.swift @@ -0,0 +1,739 @@ +// +// ServerTrustPolicy.swift +// +// Copyright (c) 2014-2016 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Responsible for managing the mapping of `ServerTrustEvaluating` values to given hosts. +open class ServerTrustManager { + /// Determines whether all hosts for this `ServerTrustManager` must be evaluated. `true` by default. + public let allHostsMustBeEvaluated: Bool + + /// The dictionary of policies mapped to a particular host. + public let evaluators: [String: ServerTrustEvaluating] + + /// Initializes the `ServerTrustManager` instance with the given evaluators. + /// + /// Since different servers and web services can have different leaf certificates, intermediate and even root + /// certificates, it is important to have the flexibility to specify evaluation policies on a per host basis. This + /// allows for scenarios such as using default evaluation for host1, certificate pinning for host2, public key + /// pinning for host3 and disabling evaluation for host4. + /// + /// - Parameters: + /// - allHostsMustBeEvaluated: The value determining whether all hosts for this instance must be evaluated. `true` + /// by default. + /// - evaluators: A dictionary of evaluators mapped to hosts. + public init(allHostsMustBeEvaluated: Bool = true, evaluators: [String: ServerTrustEvaluating]) { + self.allHostsMustBeEvaluated = allHostsMustBeEvaluated + self.evaluators = evaluators + } + + #if !(os(Linux) || os(Windows)) + /// Returns the `ServerTrustEvaluating` value for the given host, if one is set. + /// + /// By default, this method will return the policy that perfectly matches the given host. Subclasses could override + /// this method and implement more complex mapping implementations such as wildcards. + /// + /// - Parameter host: The host to use when searching for a matching policy. + /// + /// - Returns: The `ServerTrustEvaluating` value for the given host if found, `nil` otherwise. + /// - Throws: `AFError.serverTrustEvaluationFailed` if `allHostsMustBeEvaluated` is `true` and no matching + /// evaluators are found. + open func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? { + guard let evaluator = evaluators[host] else { + if allHostsMustBeEvaluated { + throw AFError.serverTrustEvaluationFailed(reason: .noRequiredEvaluator(host: host)) + } + + return nil + } + + return evaluator + } + #endif +} + +/// A protocol describing the API used to evaluate server trusts. +public protocol ServerTrustEvaluating { + #if os(Linux) || os(Windows) + // Implement this once Linux/Windows has API for evaluating server trusts. + #else + /// Evaluates the given `SecTrust` value for the given `host`. + /// + /// - Parameters: + /// - trust: The `SecTrust` value to evaluate. + /// - host: The host for which to evaluate the `SecTrust` value. + /// + /// - Returns: A `Bool` indicating whether the evaluator considers the `SecTrust` value valid for `host`. + func evaluate(_ trust: SecTrust, forHost host: String) throws + #endif +} + +// MARK: - Server Trust Evaluators + +#if !(os(Linux) || os(Windows)) +/// An evaluator which uses the default server trust evaluation while allowing you to control whether to validate the +/// host provided by the challenge. Applications are encouraged to always validate the host in production environments +/// to guarantee the validity of the server's certificate chain. +public final class DefaultTrustEvaluator: ServerTrustEvaluating { + private let validateHost: Bool + + /// Creates a `DefaultTrustEvaluator`. + /// + /// - Parameter validateHost: Determines whether or not the evaluator should validate the host. `true` by default. + public init(validateHost: Bool = true) { + self.validateHost = validateHost + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + if validateHost { + try trust.af.performValidation(forHost: host) + } + + try trust.af.performDefaultValidation(forHost: host) + } +} + +/// An evaluator which Uses the default and revoked server trust evaluations allowing you to control whether to validate +/// the host provided by the challenge as well as specify the revocation flags for testing for revoked certificates. +/// Apple platforms did not start testing for revoked certificates automatically until iOS 10.1, macOS 10.12 and tvOS +/// 10.1 which is demonstrated in our TLS tests. Applications are encouraged to always validate the host in production +/// environments to guarantee the validity of the server's certificate chain. +public final class RevocationTrustEvaluator: ServerTrustEvaluating { + /// Represents the options to be use when evaluating the status of a certificate. + /// Only Revocation Policy Constants are valid, and can be found in [Apple's documentation](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/policies/1563600-revocation_policy_constants). + public struct Options: OptionSet { + /// Perform revocation checking using the CRL (Certification Revocation List) method. + public static let crl = Options(rawValue: kSecRevocationCRLMethod) + /// Consult only locally cached replies; do not use network access. + public static let networkAccessDisabled = Options(rawValue: kSecRevocationNetworkAccessDisabled) + /// Perform revocation checking using OCSP (Online Certificate Status Protocol). + public static let ocsp = Options(rawValue: kSecRevocationOCSPMethod) + /// Prefer CRL revocation checking over OCSP; by default, OCSP is preferred. + public static let preferCRL = Options(rawValue: kSecRevocationPreferCRL) + /// Require a positive response to pass the policy. If the flag is not set, revocation checking is done on a + /// "best attempt" basis, where failure to reach the server is not considered fatal. + public static let requirePositiveResponse = Options(rawValue: kSecRevocationRequirePositiveResponse) + /// Perform either OCSP or CRL checking. The checking is performed according to the method(s) specified in the + /// certificate and the value of `preferCRL`. + public static let any = Options(rawValue: kSecRevocationUseAnyAvailableMethod) + + /// The raw value of the option. + public let rawValue: CFOptionFlags + + /// Creates an `Options` value with the given `CFOptionFlags`. + /// + /// - Parameter rawValue: The `CFOptionFlags` value to initialize with. + public init(rawValue: CFOptionFlags) { + self.rawValue = rawValue + } + } + + private let performDefaultValidation: Bool + private let validateHost: Bool + private let options: Options + + /// Creates a `RevocationTrustEvaluator` using the provided parameters. + /// + /// - Note: Default and host validation will fail when using this evaluator with self-signed certificates. Use + /// `PinnedCertificatesTrustEvaluator` if you need to use self-signed certificates. + /// + /// - Parameters: + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. `true` by default. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition to + /// performing the default evaluation, even if `performDefaultValidation` is `false`. + /// `true` by default. + /// - options: The `Options` to use to check the revocation status of the certificate. `.any` by + /// default. + public init(performDefaultValidation: Bool = true, validateHost: Bool = true, options: Options = .any) { + self.performDefaultValidation = performDefaultValidation + self.validateHost = validateHost + self.options = options + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + if performDefaultValidation { + try trust.af.performDefaultValidation(forHost: host) + } + + if validateHost { + try trust.af.performValidation(forHost: host) + } + + if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) { + try trust.af.evaluate(afterApplying: SecPolicy.af.revocation(options: options)) + } else { + try trust.af.validate(policy: SecPolicy.af.revocation(options: options)) { status, result in + AFError.serverTrustEvaluationFailed(reason: .revocationCheckFailed(output: .init(host, trust, status, result), options: options)) + } + } + } +} + +#if swift(>=5.5) +extension ServerTrustEvaluating where Self == RevocationTrustEvaluator { + /// Provides a default `RevocationTrustEvaluator` instance. + public static var revocationChecking: RevocationTrustEvaluator { RevocationTrustEvaluator() } + + /// Creates a `RevocationTrustEvaluator` using the provided parameters. + /// + /// - Note: Default and host validation will fail when using this evaluator with self-signed certificates. Use + /// `PinnedCertificatesTrustEvaluator` if you need to use self-signed certificates. + /// + /// - Parameters: + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. `true` by default. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition + /// to performing the default evaluation, even if `performDefaultValidation` is + /// `false`. `true` by default. + /// - options: The `Options` to use to check the revocation status of the certificate. `.any` + /// by default. + /// - Returns: The `RevocationTrustEvaluator`. + public static func revocationChecking(performDefaultValidation: Bool = true, + validateHost: Bool = true, + options: RevocationTrustEvaluator.Options = .any) -> RevocationTrustEvaluator { + RevocationTrustEvaluator(performDefaultValidation: performDefaultValidation, + validateHost: validateHost, + options: options) + } +} +#endif + +/// Uses the pinned certificates to validate the server trust. The server trust is considered valid if one of the pinned +/// certificates match one of the server certificates. By validating both the certificate chain and host, certificate +/// pinning provides a very secure form of server trust validation mitigating most, if not all, MITM attacks. +/// Applications are encouraged to always validate the host and require a valid certificate chain in production +/// environments. +public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating { + private let certificates: [SecCertificate] + private let acceptSelfSignedCertificates: Bool + private let performDefaultValidation: Bool + private let validateHost: Bool + + /// Creates a `PinnedCertificatesTrustEvaluator` from the provided parameters. + /// + /// - Parameters: + /// - certificates: The certificates to use to evaluate the trust. All `cer`, `crt`, and `der` + /// certificates in `Bundle.main` by default. + /// - acceptSelfSignedCertificates: Adds the provided certificates as anchors for the trust evaluation, allowing + /// self-signed certificates to pass. `false` by default. THIS SETTING SHOULD BE + /// FALSE IN PRODUCTION! + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. `true` by default. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition + /// to performing the default evaluation, even if `performDefaultValidation` is + /// `false`. `true` by default. + public init(certificates: [SecCertificate] = Bundle.main.af.certificates, + acceptSelfSignedCertificates: Bool = false, + performDefaultValidation: Bool = true, + validateHost: Bool = true) { + self.certificates = certificates + self.acceptSelfSignedCertificates = acceptSelfSignedCertificates + self.performDefaultValidation = performDefaultValidation + self.validateHost = validateHost + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + guard !certificates.isEmpty else { + throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound) + } + + if acceptSelfSignedCertificates { + try trust.af.setAnchorCertificates(certificates) + } + + if performDefaultValidation { + try trust.af.performDefaultValidation(forHost: host) + } + + if validateHost { + try trust.af.performValidation(forHost: host) + } + + let serverCertificatesData = Set(trust.af.certificateData) + let pinnedCertificatesData = Set(certificates.af.data) + let pinnedCertificatesInServerData = !serverCertificatesData.isDisjoint(with: pinnedCertificatesData) + if !pinnedCertificatesInServerData { + throw AFError.serverTrustEvaluationFailed(reason: .certificatePinningFailed(host: host, + trust: trust, + pinnedCertificates: certificates, + serverCertificates: trust.af.certificates)) + } + } +} + +#if swift(>=5.5) +extension ServerTrustEvaluating where Self == PinnedCertificatesTrustEvaluator { + /// Provides a default `PinnedCertificatesTrustEvaluator` instance. + public static var pinnedCertificates: PinnedCertificatesTrustEvaluator { PinnedCertificatesTrustEvaluator() } + + /// Creates a `PinnedCertificatesTrustEvaluator` using the provided parameters. + /// + /// - Parameters: + /// - certificates: The certificates to use to evaluate the trust. All `cer`, `crt`, and `der` + /// certificates in `Bundle.main` by default. + /// - acceptSelfSignedCertificates: Adds the provided certificates as anchors for the trust evaluation, allowing + /// self-signed certificates to pass. `false` by default. THIS SETTING SHOULD BE + /// FALSE IN PRODUCTION! + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. `true` by default. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition + /// to performing the default evaluation, even if `performDefaultValidation` is + /// `false`. `true` by default. + public static func pinnedCertificates(certificates: [SecCertificate] = Bundle.main.af.certificates, + acceptSelfSignedCertificates: Bool = false, + performDefaultValidation: Bool = true, + validateHost: Bool = true) -> PinnedCertificatesTrustEvaluator { + PinnedCertificatesTrustEvaluator(certificates: certificates, + acceptSelfSignedCertificates: acceptSelfSignedCertificates, + performDefaultValidation: performDefaultValidation, + validateHost: validateHost) + } +} +#endif + +/// Uses the pinned public keys to validate the server trust. The server trust is considered valid if one of the pinned +/// public keys match one of the server certificate public keys. By validating both the certificate chain and host, +/// public key pinning provides a very secure form of server trust validation mitigating most, if not all, MITM attacks. +/// Applications are encouraged to always validate the host and require a valid certificate chain in production +/// environments. +public final class PublicKeysTrustEvaluator: ServerTrustEvaluating { + private let keys: [SecKey] + private let performDefaultValidation: Bool + private let validateHost: Bool + + /// Creates a `PublicKeysTrustEvaluator` from the provided parameters. + /// + /// - Note: Default and host validation will fail when using this evaluator with self-signed certificates. Use + /// `PinnedCertificatesTrustEvaluator` if you need to use self-signed certificates. + /// + /// - Parameters: + /// - keys: The `SecKey`s to use to validate public keys. Defaults to the public keys of all + /// certificates included in the main bundle. + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. `true` by default. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition to + /// performing the default evaluation, even if `performDefaultValidation` is `false`. + /// `true` by default. + public init(keys: [SecKey] = Bundle.main.af.publicKeys, + performDefaultValidation: Bool = true, + validateHost: Bool = true) { + self.keys = keys + self.performDefaultValidation = performDefaultValidation + self.validateHost = validateHost + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + guard !keys.isEmpty else { + throw AFError.serverTrustEvaluationFailed(reason: .noPublicKeysFound) + } + + if performDefaultValidation { + try trust.af.performDefaultValidation(forHost: host) + } + + if validateHost { + try trust.af.performValidation(forHost: host) + } + + let pinnedKeysInServerKeys: Bool = { + for serverPublicKey in trust.af.publicKeys { + for pinnedPublicKey in keys { + if serverPublicKey == pinnedPublicKey { + return true + } + } + } + return false + }() + + if !pinnedKeysInServerKeys { + throw AFError.serverTrustEvaluationFailed(reason: .publicKeyPinningFailed(host: host, + trust: trust, + pinnedKeys: keys, + serverKeys: trust.af.publicKeys)) + } + } +} + +#if swift(>=5.5) +extension ServerTrustEvaluating where Self == PublicKeysTrustEvaluator { + /// Provides a default `PublicKeysTrustEvaluator` instance. + public static var publicKeys: PublicKeysTrustEvaluator { PublicKeysTrustEvaluator() } + + /// Creates a `PublicKeysTrustEvaluator` from the provided parameters. + /// + /// - Note: Default and host validation will fail when using this evaluator with self-signed certificates. Use + /// `PinnedCertificatesTrustEvaluator` if you need to use self-signed certificates. + /// + /// - Parameters: + /// - keys: The `SecKey`s to use to validate public keys. Defaults to the public keys of all + /// certificates included in the main bundle. + /// - performDefaultValidation: Determines whether default validation should be performed in addition to + /// evaluating the pinned certificates. `true` by default. + /// - validateHost: Determines whether or not the evaluator should validate the host, in addition to + /// performing the default evaluation, even if `performDefaultValidation` is `false`. + /// `true` by default. + public static func publicKeys(keys: [SecKey] = Bundle.main.af.publicKeys, + performDefaultValidation: Bool = true, + validateHost: Bool = true) -> PublicKeysTrustEvaluator { + PublicKeysTrustEvaluator(keys: keys, performDefaultValidation: performDefaultValidation, validateHost: validateHost) + } +} +#endif + +/// Uses the provided evaluators to validate the server trust. The trust is only considered valid if all of the +/// evaluators consider it valid. +public final class CompositeTrustEvaluator: ServerTrustEvaluating { + private let evaluators: [ServerTrustEvaluating] + + /// Creates a `CompositeTrustEvaluator` from the provided evaluators. + /// + /// - Parameter evaluators: The `ServerTrustEvaluating` values used to evaluate the server trust. + public init(evaluators: [ServerTrustEvaluating]) { + self.evaluators = evaluators + } + + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + try evaluators.evaluate(trust, forHost: host) + } +} + +#if swift(>=5.5) +extension ServerTrustEvaluating where Self == CompositeTrustEvaluator { + /// Creates a `CompositeTrustEvaluator` from the provided evaluators. + /// + /// - Parameter evaluators: The `ServerTrustEvaluating` values used to evaluate the server trust. + public static func composite(evaluators: [ServerTrustEvaluating]) -> CompositeTrustEvaluator { + CompositeTrustEvaluator(evaluators: evaluators) + } +} +#endif + +/// Disables all evaluation which in turn will always consider any server trust as valid. +/// +/// - Note: Instead of disabling server trust evaluation, it's a better idea to configure systems to properly trust test +/// certificates, as outlined in [this Apple tech note](https://developer.apple.com/library/archive/qa/qa1948/_index.html). +/// +/// **THIS EVALUATOR SHOULD NEVER BE USED IN PRODUCTION!** +@available(*, deprecated, renamed: "DisabledTrustEvaluator", message: "DisabledEvaluator has been renamed DisabledTrustEvaluator.") +public typealias DisabledEvaluator = DisabledTrustEvaluator + +/// Disables all evaluation which in turn will always consider any server trust as valid. +/// +/// +/// - Note: Instead of disabling server trust evaluation, it's a better idea to configure systems to properly trust test +/// certificates, as outlined in [this Apple tech note](https://developer.apple.com/library/archive/qa/qa1948/_index.html). +/// +/// **THIS EVALUATOR SHOULD NEVER BE USED IN PRODUCTION!** +public final class DisabledTrustEvaluator: ServerTrustEvaluating { + /// Creates an instance. + public init() {} + + public func evaluate(_ trust: SecTrust, forHost host: String) throws {} +} + +// MARK: - Extensions + +extension Array where Element == ServerTrustEvaluating { + #if os(Linux) || os(Windows) + // Add this same convenience method for Linux/Windows. + #else + /// Evaluates the given `SecTrust` value for the given `host`. + /// + /// - Parameters: + /// - trust: The `SecTrust` value to evaluate. + /// - host: The host for which to evaluate the `SecTrust` value. + /// + /// - Returns: Whether or not the evaluator considers the `SecTrust` value valid for `host`. + public func evaluate(_ trust: SecTrust, forHost host: String) throws { + for evaluator in self { + try evaluator.evaluate(trust, forHost: host) + } + } + #endif +} + +extension Bundle: AlamofireExtended {} +extension AlamofireExtension where ExtendedType: Bundle { + /// Returns all valid `cer`, `crt`, and `der` certificates in the bundle. + public var certificates: [SecCertificate] { + paths(forResourcesOfTypes: [".cer", ".CER", ".crt", ".CRT", ".der", ".DER"]).compactMap { path in + guard + let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData, + let certificate = SecCertificateCreateWithData(nil, certificateData) else { return nil } + + return certificate + } + } + + /// Returns all public keys for the valid certificates in the bundle. + public var publicKeys: [SecKey] { + certificates.af.publicKeys + } + + /// Returns all pathnames for the resources identified by the provided file extensions. + /// + /// - Parameter types: The filename extensions locate. + /// + /// - Returns: All pathnames for the given filename extensions. + public func paths(forResourcesOfTypes types: [String]) -> [String] { + Array(Set(types.flatMap { type.paths(forResourcesOfType: $0, inDirectory: nil) })) + } +} + +extension SecTrust: AlamofireExtended {} +extension AlamofireExtension where ExtendedType == SecTrust { + /// Evaluates `self` after applying the `SecPolicy` value provided. + /// + /// - Parameter policy: The `SecPolicy` to apply to `self` before evaluation. + /// + /// - Throws: Any `Error` from applying the `SecPolicy` or from evaluation. + @available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) + public func evaluate(afterApplying policy: SecPolicy) throws { + try apply(policy: policy).af.evaluate() + } + + /// Attempts to validate `self` using the `SecPolicy` provided and transforming any error produced using the closure passed. + /// + /// - Parameters: + /// - policy: The `SecPolicy` used to evaluate `self`. + /// - errorProducer: The closure used transform the failed `OSStatus` and `SecTrustResultType`. + /// - Throws: Any `Error` from applying the `policy`, or the result of `errorProducer` if validation fails. + @available(iOS, introduced: 10, deprecated: 12, renamed: "evaluate(afterApplying:)") + @available(macOS, introduced: 10.12, deprecated: 10.14, renamed: "evaluate(afterApplying:)") + @available(tvOS, introduced: 10, deprecated: 12, renamed: "evaluate(afterApplying:)") + @available(watchOS, introduced: 3, deprecated: 5, renamed: "evaluate(afterApplying:)") + public func validate(policy: SecPolicy, errorProducer: (_ status: OSStatus, _ result: SecTrustResultType) -> Error) throws { + try apply(policy: policy).af.validate(errorProducer: errorProducer) + } + + /// Applies a `SecPolicy` to `self`, throwing if it fails. + /// + /// - Parameter policy: The `SecPolicy`. + /// + /// - Returns: `self`, with the policy applied. + /// - Throws: An `AFError.serverTrustEvaluationFailed` instance with a `.policyApplicationFailed` reason. + public func apply(policy: SecPolicy) throws -> SecTrust { + let status = SecTrustSetPolicies(type, policy) + + guard status.af.isSuccess else { + throw AFError.serverTrustEvaluationFailed(reason: .policyApplicationFailed(trust: type, + policy: policy, + status: status)) + } + + return type + } + + /// Evaluate `self`, throwing an `Error` if evaluation fails. + /// + /// - Throws: `AFError.serverTrustEvaluationFailed` with reason `.trustValidationFailed` and associated error from + /// the underlying evaluation. + @available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) + public func evaluate() throws { + var error: CFError? + let evaluationSucceeded = SecTrustEvaluateWithError(type, &error) + + if !evaluationSucceeded { + throw AFError.serverTrustEvaluationFailed(reason: .trustEvaluationFailed(error: error)) + } + } + + /// Validate `self`, passing any failure values through `errorProducer`. + /// + /// - Parameter errorProducer: The closure used to transform the failed `OSStatus` and `SecTrustResultType` into an + /// `Error`. + /// - Throws: The `Error` produced by the `errorProducer` closure. + @available(iOS, introduced: 10, deprecated: 12, renamed: "evaluate()") + @available(macOS, introduced: 10.12, deprecated: 10.14, renamed: "evaluate()") + @available(tvOS, introduced: 10, deprecated: 12, renamed: "evaluate()") + @available(watchOS, introduced: 3, deprecated: 5, renamed: "evaluate()") + public func validate(errorProducer: (_ status: OSStatus, _ result: SecTrustResultType) -> Error) throws { + var result = SecTrustResultType.invalid + let status = SecTrustEvaluate(type, &result) + + guard status.af.isSuccess && result.af.isSuccess else { + throw errorProducer(status, result) + } + } + + /// Sets a custom certificate chain on `self`, allowing full validation of a self-signed certificate and its chain. + /// + /// - Parameter certificates: The `SecCertificate`s to add to the chain. + /// - Throws: Any error produced when applying the new certificate chain. + public func setAnchorCertificates(_ certificates: [SecCertificate]) throws { + // Add additional anchor certificates. + let status = SecTrustSetAnchorCertificates(type, certificates as CFArray) + guard status.af.isSuccess else { + throw AFError.serverTrustEvaluationFailed(reason: .settingAnchorCertificatesFailed(status: status, + certificates: certificates)) + } + + // Trust only the set anchor certs. + let onlyStatus = SecTrustSetAnchorCertificatesOnly(type, true) + guard onlyStatus.af.isSuccess else { + throw AFError.serverTrustEvaluationFailed(reason: .settingAnchorCertificatesFailed(status: onlyStatus, + certificates: certificates)) + } + } + + /// The public keys contained in `self`. + public var publicKeys: [SecKey] { + certificates.af.publicKeys + } + + #if swift(>=5.5) // Xcode 13 / 2021 SDKs. + /// The `SecCertificate`s contained in `self`. + public var certificates: [SecCertificate] { + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + return (SecTrustCopyCertificateChain(type) as? [SecCertificate]) ?? [] + } else { + return (0.. SecPolicy { + SecPolicyCreateSSL(true, hostname as CFString) + } + + /// Creates a `SecPolicy` which checks the revocation of certificates. + /// + /// - Parameter options: The `RevocationTrustEvaluator.Options` for evaluation. + /// + /// - Returns: The `SecPolicy`. + /// - Throws: An `AFError.serverTrustEvaluationFailed` error with reason `.revocationPolicyCreationFailed` + /// if the policy cannot be created. + public static func revocation(options: RevocationTrustEvaluator.Options) throws -> SecPolicy { + guard let policy = SecPolicyCreateRevocation(options.rawValue) else { + throw AFError.serverTrustEvaluationFailed(reason: .revocationPolicyCreationFailed) + } + + return policy + } +} + +extension Array: AlamofireExtended {} +extension AlamofireExtension where ExtendedType == [SecCertificate] { + /// All `Data` values for the contained `SecCertificate`s. + public var data: [Data] { + type.map { SecCertificateCopyData($0) as Data } + } + + /// All public `SecKey` values for the contained `SecCertificate`s. + public var publicKeys: [SecKey] { + type.compactMap(\.af.publicKey) + } +} + +extension SecCertificate: AlamofireExtended {} +extension AlamofireExtension where ExtendedType == SecCertificate { + /// The public key for `self`, if it can be extracted. + /// + /// - Note: On 2020 OSes and newer, only RSA and ECDSA keys are supported. + /// + public var publicKey: SecKey? { + let policy = SecPolicyCreateBasicX509() + var trust: SecTrust? + let trustCreationStatus = SecTrustCreateWithCertificates(type, policy, &trust) + + guard let createdTrust = trust, trustCreationStatus == errSecSuccess else { return nil } + + if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) { + return SecTrustCopyKey(createdTrust) + } else { + return SecTrustCopyPublicKey(createdTrust) + } + } +} + +extension OSStatus: AlamofireExtended {} +extension AlamofireExtension where ExtendedType == OSStatus { + /// Returns whether `self` is `errSecSuccess`. + public var isSuccess: Bool { type == errSecSuccess } +} + +extension SecTrustResultType: AlamofireExtended {} +extension AlamofireExtension where ExtendedType == SecTrustResultType { + /// Returns whether `self is `.unspecified` or `.proceed`. + public var isSuccess: Bool { + type == .unspecified || type == .proceed + } +} +#endif diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/Session.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Session.swift new file mode 100644 index 0000000..4232f85 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Session.swift @@ -0,0 +1,1264 @@ +// +// Session.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// `Session` creates and manages Alamofire's `Request` types during their lifetimes. It also provides common +/// functionality for all `Request`s, including queuing, interception, trust management, redirect handling, and response +/// cache handling. +open class Session { + /// Shared singleton instance used by all `AF.request` APIs. Cannot be modified. + public static let `default` = Session() + + /// Underlying `URLSession` used to create `URLSessionTasks` for this instance, and for which this instance's + /// `delegate` handles `URLSessionDelegate` callbacks. + /// + /// - Note: This instance should **NOT** be used to interact with the underlying `URLSessionTask`s. Doing so will + /// break internal Alamofire logic that tracks those tasks. + /// + public let session: URLSession + /// Instance's `SessionDelegate`, which handles the `URLSessionDelegate` methods and `Request` interaction. + public let delegate: SessionDelegate + /// Root `DispatchQueue` for all internal callbacks and state update. **MUST** be a serial queue. + public let rootQueue: DispatchQueue + /// Value determining whether this instance automatically calls `resume()` on all created `Request`s. + public let startRequestsImmediately: Bool + /// `DispatchQueue` on which `URLRequest`s are created asynchronously. By default this queue uses `rootQueue` as its + /// `target`, but a separate queue can be used if request creation is determined to be a bottleneck. Always profile + /// and test before introducing an additional queue. + public let requestQueue: DispatchQueue + /// `DispatchQueue` passed to all `Request`s on which they perform their response serialization. By default this + /// queue uses `rootQueue` as its `target` but a separate queue can be used if response serialization is determined + /// to be a bottleneck. Always profile and test before introducing an additional queue. + public let serializationQueue: DispatchQueue + /// `RequestInterceptor` used for all `Request` created by the instance. `RequestInterceptor`s can also be set on a + /// per-`Request` basis, in which case the `Request`'s interceptor takes precedence over this value. + public let interceptor: RequestInterceptor? + /// `ServerTrustManager` instance used to evaluate all trust challenges and provide certificate and key pinning. + public let serverTrustManager: ServerTrustManager? + /// `RedirectHandler` instance used to provide customization for request redirection. + public let redirectHandler: RedirectHandler? + /// `CachedResponseHandler` instance used to provide customization of cached response handling. + public let cachedResponseHandler: CachedResponseHandler? + /// `CompositeEventMonitor` used to compose Alamofire's `defaultEventMonitors` and any passed `EventMonitor`s. + public let eventMonitor: CompositeEventMonitor + /// `EventMonitor`s included in all instances. `[AlamofireNotifications()]` by default. + public let defaultEventMonitors: [EventMonitor] = [AlamofireNotifications()] + + /// Internal map between `Request`s and any `URLSessionTasks` that may be in flight for them. + var requestTaskMap = RequestTaskMap() + /// `Set` of currently active `Request`s. + var activeRequests: Set = [] + /// Completion events awaiting `URLSessionTaskMetrics`. + var waitingCompletions: [URLSessionTask: () -> Void] = [:] + + /// Creates a `Session` from a `URLSession` and other parameters. + /// + /// - Note: When passing a `URLSession`, you must create the `URLSession` with a specific `delegateQueue` value and + /// pass the `delegateQueue`'s `underlyingQueue` as the `rootQueue` parameter of this initializer. + /// + /// - Parameters: + /// - session: Underlying `URLSession` for this instance. + /// - delegate: `SessionDelegate` that handles `session`'s delegate callbacks as well as `Request` + /// interaction. + /// - rootQueue: Root `DispatchQueue` for all internal callbacks and state updates. **MUST** be a + /// serial queue. + /// - startRequestsImmediately: Determines whether this instance will automatically start all `Request`s. `true` + /// by default. If set to `false`, all `Request`s created must have `.resume()` called. + /// on them for them to start. + /// - requestQueue: `DispatchQueue` on which to perform `URLRequest` creation. By default this queue + /// will use the `rootQueue` as its `target`. A separate queue can be used if it's + /// determined request creation is a bottleneck, but that should only be done after + /// careful testing and profiling. `nil` by default. + /// - serializationQueue: `DispatchQueue` on which to perform all response serialization. By default this + /// queue will use the `rootQueue` as its `target`. A separate queue can be used if + /// it's determined response serialization is a bottleneck, but that should only be + /// done after careful testing and profiling. `nil` by default. + /// - interceptor: `RequestInterceptor` to be used for all `Request`s created by this instance. `nil` + /// by default. + /// - serverTrustManager: `ServerTrustManager` to be used for all trust evaluations by this instance. `nil` + /// by default. + /// - redirectHandler: `RedirectHandler` to be used by all `Request`s created by this instance. `nil` by + /// default. + /// - cachedResponseHandler: `CachedResponseHandler` to be used by all `Request`s created by this instance. + /// `nil` by default. + /// - eventMonitors: Additional `EventMonitor`s used by the instance. Alamofire always adds a + /// `AlamofireNotifications` `EventMonitor` to the array passed here. `[]` by default. + public init(session: URLSession, + delegate: SessionDelegate, + rootQueue: DispatchQueue, + startRequestsImmediately: Bool = true, + requestQueue: DispatchQueue? = nil, + serializationQueue: DispatchQueue? = nil, + interceptor: RequestInterceptor? = nil, + serverTrustManager: ServerTrustManager? = nil, + redirectHandler: RedirectHandler? = nil, + cachedResponseHandler: CachedResponseHandler? = nil, + eventMonitors: [EventMonitor] = []) { + precondition(session.configuration.identifier == nil, + "Alamofire does not support background URLSessionConfigurations.") + precondition(session.delegateQueue.underlyingQueue === rootQueue, + "Session(session:) initializer must be passed the DispatchQueue used as the delegateQueue's underlyingQueue as rootQueue.") + + self.session = session + self.delegate = delegate + self.rootQueue = rootQueue + self.startRequestsImmediately = startRequestsImmediately + self.requestQueue = requestQueue ?? DispatchQueue(label: "\(rootQueue.label).requestQueue", target: rootQueue) + self.serializationQueue = serializationQueue ?? DispatchQueue(label: "\(rootQueue.label).serializationQueue", target: rootQueue) + self.interceptor = interceptor + self.serverTrustManager = serverTrustManager + self.redirectHandler = redirectHandler + self.cachedResponseHandler = cachedResponseHandler + eventMonitor = CompositeEventMonitor(monitors: defaultEventMonitors + eventMonitors) + delegate.eventMonitor = eventMonitor + delegate.stateProvider = self + } + + /// Creates a `Session` from a `URLSessionConfiguration`. + /// + /// - Note: This initializer lets Alamofire handle the creation of the underlying `URLSession` and its + /// `delegateQueue`, and is the recommended initializer for most uses. + /// + /// - Parameters: + /// - configuration: `URLSessionConfiguration` to be used to create the underlying `URLSession`. Changes + /// to this value after being passed to this initializer will have no effect. + /// `URLSessionConfiguration.af.default` by default. + /// - delegate: `SessionDelegate` that handles `session`'s delegate callbacks as well as `Request` + /// interaction. `SessionDelegate()` by default. + /// - rootQueue: Root `DispatchQueue` for all internal callbacks and state updates. **MUST** be a + /// serial queue. `DispatchQueue(label: "org.alamofire.session.rootQueue")` by default. + /// - startRequestsImmediately: Determines whether this instance will automatically start all `Request`s. `true` + /// by default. If set to `false`, all `Request`s created must have `.resume()` called. + /// on them for them to start. + /// - requestQueue: `DispatchQueue` on which to perform `URLRequest` creation. By default this queue + /// will use the `rootQueue` as its `target`. A separate queue can be used if it's + /// determined request creation is a bottleneck, but that should only be done after + /// careful testing and profiling. `nil` by default. + /// - serializationQueue: `DispatchQueue` on which to perform all response serialization. By default this + /// queue will use the `rootQueue` as its `target`. A separate queue can be used if + /// it's determined response serialization is a bottleneck, but that should only be + /// done after careful testing and profiling. `nil` by default. + /// - interceptor: `RequestInterceptor` to be used for all `Request`s created by this instance. `nil` + /// by default. + /// - serverTrustManager: `ServerTrustManager` to be used for all trust evaluations by this instance. `nil` + /// by default. + /// - redirectHandler: `RedirectHandler` to be used by all `Request`s created by this instance. `nil` by + /// default. + /// - cachedResponseHandler: `CachedResponseHandler` to be used by all `Request`s created by this instance. + /// `nil` by default. + /// - eventMonitors: Additional `EventMonitor`s used by the instance. Alamofire always adds a + /// `AlamofireNotifications` `EventMonitor` to the array passed here. `[]` by default. + public convenience init(configuration: URLSessionConfiguration = URLSessionConfiguration.af.default, + delegate: SessionDelegate = SessionDelegate(), + rootQueue: DispatchQueue = DispatchQueue(label: "org.alamofire.session.rootQueue"), + startRequestsImmediately: Bool = true, + requestQueue: DispatchQueue? = nil, + serializationQueue: DispatchQueue? = nil, + interceptor: RequestInterceptor? = nil, + serverTrustManager: ServerTrustManager? = nil, + redirectHandler: RedirectHandler? = nil, + cachedResponseHandler: CachedResponseHandler? = nil, + eventMonitors: [EventMonitor] = []) { + precondition(configuration.identifier == nil, "Alamofire does not support background URLSessionConfigurations.") + + // Retarget the incoming rootQueue for safety, unless it's the main queue, which we know is safe. + let serialRootQueue = (rootQueue === DispatchQueue.main) ? rootQueue : DispatchQueue(label: rootQueue.label, + target: rootQueue) + let delegateQueue = OperationQueue(maxConcurrentOperationCount: 1, underlyingQueue: serialRootQueue, name: "\(serialRootQueue.label).sessionDelegate") + let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue) + + self.init(session: session, + delegate: delegate, + rootQueue: serialRootQueue, + startRequestsImmediately: startRequestsImmediately, + requestQueue: requestQueue, + serializationQueue: serializationQueue, + interceptor: interceptor, + serverTrustManager: serverTrustManager, + redirectHandler: redirectHandler, + cachedResponseHandler: cachedResponseHandler, + eventMonitors: eventMonitors) + } + + deinit { + finishRequestsForDeinit() + session.invalidateAndCancel() + } + + // MARK: - All Requests API + + /// Perform an action on all active `Request`s. + /// + /// - Note: The provided `action` closure is performed asynchronously, meaning that some `Request`s may complete and + /// be unavailable by time it runs. Additionally, this action is performed on the instances's `rootQueue`, + /// so care should be taken that actions are fast. Once the work on the `Request`s is complete, any + /// additional work should be performed on another queue. + /// + /// - Parameters: + /// - action: Closure to perform with all `Request`s. + public func withAllRequests(perform action: @escaping (Set) -> Void) { + rootQueue.async { + action(self.activeRequests) + } + } + + /// Cancel all active `Request`s, optionally calling a completion handler when complete. + /// + /// - Note: This is an asynchronous operation and does not block the creation of future `Request`s. Cancelled + /// `Request`s may not cancel immediately due internal work, and may not cancel at all if they are close to + /// completion when cancelled. + /// + /// - Parameters: + /// - queue: `DispatchQueue` on which the completion handler is run. `.main` by default. + /// - completion: Closure to be called when all `Request`s have been cancelled. + public func cancelAllRequests(completingOnQueue queue: DispatchQueue = .main, completion: (() -> Void)? = nil) { + withAllRequests { requests in + requests.forEach { $0.cancel() } + queue.async { + completion?() + } + } + } + + // MARK: - DataRequest + + /// Closure which provides a `URLRequest` for mutation. + public typealias RequestModifier = (inout URLRequest) throws -> Void + + struct RequestConvertible: URLRequestConvertible { + let url: URLConvertible + let method: HTTPMethod + let parameters: Parameters? + let encoding: ParameterEncoding + let headers: HTTPHeaders? + let requestModifier: RequestModifier? + + func asURLRequest() throws -> URLRequest { + var request = try URLRequest(url: url, method: method, headers: headers) + try requestModifier?(&request) + + return try encoding.encode(request, with: parameters) + } + } + + /// Creates a `DataRequest` from a `URLRequest` created using the passed components and a `RequestInterceptor`. + /// + /// - Parameters: + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.get` by default. + /// - parameters: `Parameters` (a.k.a. `[String: Any]`) value to be encoded into the `URLRequest`. `nil` by + /// default. + /// - encoding: `ParameterEncoding` to be used to encode the `parameters` value into the `URLRequest`. + /// `URLEncoding.default` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided + /// parameters. `nil` by default. + /// + /// - Returns: The created `DataRequest`. + open func request(_ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoding: ParameterEncoding = URLEncoding.default, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil) -> DataRequest { + let convertible = RequestConvertible(url: convertible, + method: method, + parameters: parameters, + encoding: encoding, + headers: headers, + requestModifier: requestModifier) + + return request(convertible, interceptor: interceptor) + } + + struct RequestEncodableConvertible: URLRequestConvertible { + let url: URLConvertible + let method: HTTPMethod + let parameters: Parameters? + let encoder: ParameterEncoder + let headers: HTTPHeaders? + let requestModifier: RequestModifier? + + func asURLRequest() throws -> URLRequest { + var request = try URLRequest(url: url, method: method, headers: headers) + try requestModifier?(&request) + + return try parameters.map { try encoder.encode($0, into: request) } ?? request + } + } + + /// Creates a `DataRequest` from a `URLRequest` created using the passed components, `Encodable` parameters, and a + /// `RequestInterceptor`. + /// + /// - Parameters: + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.get` by default. + /// - parameters: `Encodable` value to be encoded into the `URLRequest`. `nil` by default. + /// - encoder: `ParameterEncoder` to be used to encode the `parameters` value into the `URLRequest`. + /// `URLEncodedFormParameterEncoder.default` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from + /// the provided parameters. `nil` by default. + /// + /// - Returns: The created `DataRequest`. + open func request(_ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil) -> DataRequest { + let convertible = RequestEncodableConvertible(url: convertible, + method: method, + parameters: parameters, + encoder: encoder, + headers: headers, + requestModifier: requestModifier) + + return request(convertible, interceptor: interceptor) + } + + /// Creates a `DataRequest` from a `URLRequestConvertible` value and a `RequestInterceptor`. + /// + /// - Parameters: + /// - convertible: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// + /// - Returns: The created `DataRequest`. + open func request(_ convertible: URLRequestConvertible, interceptor: RequestInterceptor? = nil) -> DataRequest { + let request = DataRequest(convertible: convertible, + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: self) + + perform(request) + + return request + } + + // MARK: - DataStreamRequest + + /// Creates a `DataStreamRequest` from the passed components, `Encodable` parameters, and `RequestInterceptor`. + /// + /// - Parameters: + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.get` by default. + /// - parameters: `Encodable` value to be encoded into the `URLRequest`. `nil` by default. + /// - encoder: `ParameterEncoder` to be used to encode the `parameters` value into the + /// `URLRequest`. + /// `URLEncodedFormParameterEncoder.default` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - automaticallyCancelOnStreamError: `Bool` indicating whether the instance should be canceled when an `Error` + /// is thrown while serializing stream `Data`. `false` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` + /// by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from + /// the provided parameters. `nil` by default. + /// + /// - Returns: The created `DataStream` request. + open func streamRequest(_ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default, + headers: HTTPHeaders? = nil, + automaticallyCancelOnStreamError: Bool = false, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil) -> DataStreamRequest { + let convertible = RequestEncodableConvertible(url: convertible, + method: method, + parameters: parameters, + encoder: encoder, + headers: headers, + requestModifier: requestModifier) + + return streamRequest(convertible, + automaticallyCancelOnStreamError: automaticallyCancelOnStreamError, + interceptor: interceptor) + } + + /// Creates a `DataStreamRequest` from the passed components and `RequestInterceptor`. + /// + /// - Parameters: + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.get` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - automaticallyCancelOnStreamError: `Bool` indicating whether the instance should be canceled when an `Error` + /// is thrown while serializing stream `Data`. `false` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` + /// by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from + /// the provided parameters. `nil` by default. + /// + /// - Returns: The created `DataStream` request. + open func streamRequest(_ convertible: URLConvertible, + method: HTTPMethod = .get, + headers: HTTPHeaders? = nil, + automaticallyCancelOnStreamError: Bool = false, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil) -> DataStreamRequest { + let convertible = RequestEncodableConvertible(url: convertible, + method: method, + parameters: Empty?.none, + encoder: URLEncodedFormParameterEncoder.default, + headers: headers, + requestModifier: requestModifier) + + return streamRequest(convertible, + automaticallyCancelOnStreamError: automaticallyCancelOnStreamError, + interceptor: interceptor) + } + + /// Creates a `DataStreamRequest` from the passed `URLRequestConvertible` value and `RequestInterceptor`. + /// + /// - Parameters: + /// - convertible: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - automaticallyCancelOnStreamError: `Bool` indicating whether the instance should be canceled when an `Error` + /// is thrown while serializing stream `Data`. `false` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` + /// by default. + /// + /// - Returns: The created `DataStreamRequest`. + open func streamRequest(_ convertible: URLRequestConvertible, + automaticallyCancelOnStreamError: Bool = false, + interceptor: RequestInterceptor? = nil) -> DataStreamRequest { + let request = DataStreamRequest(convertible: convertible, + automaticallyCancelOnStreamError: automaticallyCancelOnStreamError, + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: self) + + perform(request) + + return request + } + + // MARK: - DownloadRequest + + /// Creates a `DownloadRequest` using a `URLRequest` created using the passed components, `RequestInterceptor`, and + /// `Destination`. + /// + /// - Parameters: + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.get` by default. + /// - parameters: `Parameters` (a.k.a. `[String: Any]`) value to be encoded into the `URLRequest`. `nil` by + /// default. + /// - encoding: `ParameterEncoding` to be used to encode the `parameters` value into the `URLRequest`. + /// Defaults to `URLEncoding.default`. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided + /// parameters. `nil` by default. + /// - destination: `DownloadRequest.Destination` closure used to determine how and where the downloaded file + /// should be moved. `nil` by default. + /// + /// - Returns: The created `DownloadRequest`. + open func download(_ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoding: ParameterEncoding = URLEncoding.default, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + let convertible = RequestConvertible(url: convertible, + method: method, + parameters: parameters, + encoding: encoding, + headers: headers, + requestModifier: requestModifier) + + return download(convertible, interceptor: interceptor, to: destination) + } + + /// Creates a `DownloadRequest` from a `URLRequest` created using the passed components, `Encodable` parameters, and + /// a `RequestInterceptor`. + /// + /// - Parameters: + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.get` by default. + /// - parameters: Value conforming to `Encodable` to be encoded into the `URLRequest`. `nil` by default. + /// - encoder: `ParameterEncoder` to be used to encode the `parameters` value into the `URLRequest`. + /// Defaults to `URLEncodedFormParameterEncoder.default`. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided + /// parameters. `nil` by default. + /// - destination: `DownloadRequest.Destination` closure used to determine how and where the downloaded file + /// should be moved. `nil` by default. + /// + /// - Returns: The created `DownloadRequest`. + open func download(_ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + let convertible = RequestEncodableConvertible(url: convertible, + method: method, + parameters: parameters, + encoder: encoder, + headers: headers, + requestModifier: requestModifier) + + return download(convertible, interceptor: interceptor, to: destination) + } + + /// Creates a `DownloadRequest` from a `URLRequestConvertible` value, a `RequestInterceptor`, and a `Destination`. + /// + /// - Parameters: + /// - convertible: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - destination: `DownloadRequest.Destination` closure used to determine how and where the downloaded file + /// should be moved. `nil` by default. + /// + /// - Returns: The created `DownloadRequest`. + open func download(_ convertible: URLRequestConvertible, + interceptor: RequestInterceptor? = nil, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + let request = DownloadRequest(downloadable: .request(convertible), + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: self, + destination: destination ?? DownloadRequest.defaultDestination) + + perform(request) + + return request + } + + /// Creates a `DownloadRequest` from the `resumeData` produced from a previously cancelled `DownloadRequest`, as + /// well as a `RequestInterceptor`, and a `Destination`. + /// + /// - Note: If `destination` is not specified, the download will be moved to a temporary location determined by + /// Alamofire. The file will not be deleted until the system purges the temporary files. + /// + /// - Note: On some versions of all Apple platforms (iOS 10 - 10.2, macOS 10.12 - 10.12.2, tvOS 10 - 10.1, watchOS 3 - 3.1.1), + /// `resumeData` is broken on background URL session configurations. There's an underlying bug in the `resumeData` + /// generation logic where the data is written incorrectly and will always fail to resume the download. For more + /// information about the bug and possible workarounds, please refer to the [this Stack Overflow post](http://stackoverflow.com/a/39347461/1342462). + /// + /// - Parameters: + /// - data: The resume data from a previously cancelled `DownloadRequest` or `URLSessionDownloadTask`. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - destination: `DownloadRequest.Destination` closure used to determine how and where the downloaded file + /// should be moved. `nil` by default. + /// + /// - Returns: The created `DownloadRequest`. + open func download(resumingWith data: Data, + interceptor: RequestInterceptor? = nil, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + let request = DownloadRequest(downloadable: .resumeData(data), + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + delegate: self, + destination: destination ?? DownloadRequest.defaultDestination) + + perform(request) + + return request + } + + // MARK: - UploadRequest + + struct ParameterlessRequestConvertible: URLRequestConvertible { + let url: URLConvertible + let method: HTTPMethod + let headers: HTTPHeaders? + let requestModifier: RequestModifier? + + func asURLRequest() throws -> URLRequest { + var request = try URLRequest(url: url, method: method, headers: headers) + try requestModifier?(&request) + + return request + } + } + + struct Upload: UploadConvertible { + let request: URLRequestConvertible + let uploadable: UploadableConvertible + + func createUploadable() throws -> UploadRequest.Uploadable { + try uploadable.createUploadable() + } + + func asURLRequest() throws -> URLRequest { + try request.asURLRequest() + } + } + + // MARK: Data + + /// Creates an `UploadRequest` for the given `Data`, `URLRequest` components, and `RequestInterceptor`. + /// + /// - Parameters: + /// - data: The `Data` to upload. + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.post` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by + /// default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided + /// parameters. `nil` by default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(_ data: Data, + to convertible: URLConvertible, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default, + requestModifier: RequestModifier? = nil) -> UploadRequest { + let convertible = ParameterlessRequestConvertible(url: convertible, + method: method, + headers: headers, + requestModifier: requestModifier) + + return upload(data, with: convertible, interceptor: interceptor, fileManager: fileManager) + } + + /// Creates an `UploadRequest` for the given `Data` using the `URLRequestConvertible` value and `RequestInterceptor`. + /// + /// - Parameters: + /// - data: The `Data` to upload. + /// - convertible: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by + /// default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(_ data: Data, + with convertible: URLRequestConvertible, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default) -> UploadRequest { + upload(.data(data), with: convertible, interceptor: interceptor, fileManager: fileManager) + } + + // MARK: File + + /// Creates an `UploadRequest` for the file at the given file `URL`, using a `URLRequest` from the provided + /// components and `RequestInterceptor`. + /// + /// - Parameters: + /// - fileURL: The `URL` of the file to upload. + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.post` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `UploadRequest`. `nil` by default. + /// - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by + /// default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided + /// parameters. `nil` by default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(_ fileURL: URL, + to convertible: URLConvertible, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default, + requestModifier: RequestModifier? = nil) -> UploadRequest { + let convertible = ParameterlessRequestConvertible(url: convertible, + method: method, + headers: headers, + requestModifier: requestModifier) + + return upload(fileURL, with: convertible, interceptor: interceptor, fileManager: fileManager) + } + + /// Creates an `UploadRequest` for the file at the given file `URL` using the `URLRequestConvertible` value and + /// `RequestInterceptor`. + /// + /// - Parameters: + /// - fileURL: The `URL` of the file to upload. + /// - convertible: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by + /// default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(_ fileURL: URL, + with convertible: URLRequestConvertible, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default) -> UploadRequest { + upload(.file(fileURL, shouldRemove: false), with: convertible, interceptor: interceptor, fileManager: fileManager) + } + + // MARK: InputStream + + /// Creates an `UploadRequest` from the `InputStream` provided using a `URLRequest` from the provided components and + /// `RequestInterceptor`. + /// + /// - Parameters: + /// - stream: The `InputStream` that provides the data to upload. + /// - convertible: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - method: `HTTPMethod` for the `URLRequest`. `.post` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by + /// default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the provided + /// parameters. `nil` by default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(_ stream: InputStream, + to convertible: URLConvertible, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default, + requestModifier: RequestModifier? = nil) -> UploadRequest { + let convertible = ParameterlessRequestConvertible(url: convertible, + method: method, + headers: headers, + requestModifier: requestModifier) + + return upload(stream, with: convertible, interceptor: interceptor, fileManager: fileManager) + } + + /// Creates an `UploadRequest` from the provided `InputStream` using the `URLRequestConvertible` value and + /// `RequestInterceptor`. + /// + /// - Parameters: + /// - stream: The `InputStream` that provides the data to upload. + /// - convertible: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by + /// default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(_ stream: InputStream, + with convertible: URLRequestConvertible, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default) -> UploadRequest { + upload(.stream(stream), with: convertible, interceptor: interceptor, fileManager: fileManager) + } + + // MARK: MultipartFormData + + /// Creates an `UploadRequest` for the multipart form data built using a closure and sent using the provided + /// `URLRequest` components and `RequestInterceptor`. + /// + /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cumulative + /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most + /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to + /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory + /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be + /// used for larger payloads such as video content. + /// + /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory + /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, + /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk + /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding + /// technique was used. + /// + /// - Parameters: + /// - multipartFormData: `MultipartFormData` building closure. + /// - url: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - encodingMemoryThreshold: Byte threshold used to determine whether the form data is encoded into memory or + /// onto disk before being uploaded. `MultipartFormData.encodingMemoryThreshold` by + /// default. + /// - method: `HTTPMethod` for the `URLRequest`. `.post` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` to be used if the form data exceeds the memory threshold and is + /// written to disk before being uploaded. `.default` instance by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the + /// provided parameters. `nil` by default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(multipartFormData: @escaping (MultipartFormData) -> Void, + to url: URLConvertible, + usingThreshold encodingMemoryThreshold: UInt64 = MultipartFormData.encodingMemoryThreshold, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default, + requestModifier: RequestModifier? = nil) -> UploadRequest { + let convertible = ParameterlessRequestConvertible(url: url, + method: method, + headers: headers, + requestModifier: requestModifier) + + let formData = MultipartFormData(fileManager: fileManager) + multipartFormData(formData) + + return upload(multipartFormData: formData, + with: convertible, + usingThreshold: encodingMemoryThreshold, + interceptor: interceptor, + fileManager: fileManager) + } + + /// Creates an `UploadRequest` using a `MultipartFormData` building closure, the provided `URLRequestConvertible` + /// value, and a `RequestInterceptor`. + /// + /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cumulative + /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most + /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to + /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory + /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be + /// used for larger payloads such as video content. + /// + /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory + /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, + /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk + /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding + /// technique was used. + /// + /// - Parameters: + /// - multipartFormData: `MultipartFormData` building closure. + /// - request: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - encodingMemoryThreshold: Byte threshold used to determine whether the form data is encoded into memory or + /// onto disk before being uploaded. `MultipartFormData.encodingMemoryThreshold` by + /// default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` to be used if the form data exceeds the memory threshold and is + /// written to disk before being uploaded. `.default` instance by default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(multipartFormData: @escaping (MultipartFormData) -> Void, + with request: URLRequestConvertible, + usingThreshold encodingMemoryThreshold: UInt64 = MultipartFormData.encodingMemoryThreshold, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default) -> UploadRequest { + let formData = MultipartFormData(fileManager: fileManager) + multipartFormData(formData) + + return upload(multipartFormData: formData, + with: request, + usingThreshold: encodingMemoryThreshold, + interceptor: interceptor, + fileManager: fileManager) + } + + /// Creates an `UploadRequest` for the prebuilt `MultipartFormData` value using the provided `URLRequest` components + /// and `RequestInterceptor`. + /// + /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cumulative + /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most + /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to + /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory + /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be + /// used for larger payloads such as video content. + /// + /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory + /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, + /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk + /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding + /// technique was used. + /// + /// - Parameters: + /// - multipartFormData: `MultipartFormData` instance to upload. + /// - url: `URLConvertible` value to be used as the `URLRequest`'s `URL`. + /// - encodingMemoryThreshold: Byte threshold used to determine whether the form data is encoded into memory or + /// onto disk before being uploaded. `MultipartFormData.encodingMemoryThreshold` by + /// default. + /// - method: `HTTPMethod` for the `URLRequest`. `.post` by default. + /// - headers: `HTTPHeaders` value to be added to the `URLRequest`. `nil` by default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` to be used if the form data exceeds the memory threshold and is + /// written to disk before being uploaded. `.default` instance by default. + /// - requestModifier: `RequestModifier` which will be applied to the `URLRequest` created from the + /// provided parameters. `nil` by default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(multipartFormData: MultipartFormData, + to url: URLConvertible, + usingThreshold encodingMemoryThreshold: UInt64 = MultipartFormData.encodingMemoryThreshold, + method: HTTPMethod = .post, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default, + requestModifier: RequestModifier? = nil) -> UploadRequest { + let convertible = ParameterlessRequestConvertible(url: url, + method: method, + headers: headers, + requestModifier: requestModifier) + + let multipartUpload = MultipartUpload(encodingMemoryThreshold: encodingMemoryThreshold, + request: convertible, + multipartFormData: multipartFormData) + + return upload(multipartUpload, interceptor: interceptor, fileManager: fileManager) + } + + /// Creates an `UploadRequest` for the prebuilt `MultipartFormData` value using the providing `URLRequestConvertible` + /// value and `RequestInterceptor`. + /// + /// It is important to understand the memory implications of uploading `MultipartFormData`. If the cumulative + /// payload is small, encoding the data in-memory and directly uploading to a server is the by far the most + /// efficient approach. However, if the payload is too large, encoding the data in-memory could cause your app to + /// be terminated. Larger payloads must first be written to disk using input and output streams to keep the memory + /// footprint low, then the data can be uploaded as a stream from the resulting file. Streaming from disk MUST be + /// used for larger payloads such as video content. + /// + /// The `encodingMemoryThreshold` parameter allows Alamofire to automatically determine whether to encode in-memory + /// or stream from disk. If the content length of the `MultipartFormData` is below the `encodingMemoryThreshold`, + /// encoding takes place in-memory. If the content length exceeds the threshold, the data is streamed to disk + /// during the encoding process. Then the result is uploaded as data or as a stream depending on which encoding + /// technique was used. + /// + /// - Parameters: + /// - multipartFormData: `MultipartFormData` instance to upload. + /// - request: `URLRequestConvertible` value to be used to create the `URLRequest`. + /// - encodingMemoryThreshold: Byte threshold used to determine whether the form data is encoded into memory or + /// onto disk before being uploaded. `MultipartFormData.encodingMemoryThreshold` by + /// default. + /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. + /// - fileManager: `FileManager` instance to be used by the returned `UploadRequest`. `.default` instance by + /// default. + /// + /// - Returns: The created `UploadRequest`. + open func upload(multipartFormData: MultipartFormData, + with request: URLRequestConvertible, + usingThreshold encodingMemoryThreshold: UInt64 = MultipartFormData.encodingMemoryThreshold, + interceptor: RequestInterceptor? = nil, + fileManager: FileManager = .default) -> UploadRequest { + let multipartUpload = MultipartUpload(encodingMemoryThreshold: encodingMemoryThreshold, + request: request, + multipartFormData: multipartFormData) + + return upload(multipartUpload, interceptor: interceptor, fileManager: fileManager) + } + + // MARK: - Internal API + + // MARK: Uploadable + + func upload(_ uploadable: UploadRequest.Uploadable, + with convertible: URLRequestConvertible, + interceptor: RequestInterceptor?, + fileManager: FileManager) -> UploadRequest { + let uploadable = Upload(request: convertible, uploadable: uploadable) + + return upload(uploadable, interceptor: interceptor, fileManager: fileManager) + } + + func upload(_ upload: UploadConvertible, interceptor: RequestInterceptor?, fileManager: FileManager) -> UploadRequest { + let request = UploadRequest(convertible: upload, + underlyingQueue: rootQueue, + serializationQueue: serializationQueue, + eventMonitor: eventMonitor, + interceptor: interceptor, + fileManager: fileManager, + delegate: self) + + perform(request) + + return request + } + + // MARK: Perform + + /// Starts performing the provided `Request`. + /// + /// - Parameter request: The `Request` to perform. + func perform(_ request: Request) { + rootQueue.async { + guard !request.isCancelled else { return } + + self.activeRequests.insert(request) + + self.requestQueue.async { + // Leaf types must come first, otherwise they will cast as their superclass. + switch request { + case let r as UploadRequest: self.performUploadRequest(r) // UploadRequest must come before DataRequest due to subtype relationship. + case let r as DataRequest: self.performDataRequest(r) + case let r as DownloadRequest: self.performDownloadRequest(r) + case let r as DataStreamRequest: self.performDataStreamRequest(r) + default: fatalError("Attempted to perform unsupported Request subclass: \(type(of: request))") + } + } + } + } + + func performDataRequest(_ request: DataRequest) { + dispatchPrecondition(condition: .onQueue(requestQueue)) + + performSetupOperations(for: request, convertible: request.convertible) + } + + func performDataStreamRequest(_ request: DataStreamRequest) { + dispatchPrecondition(condition: .onQueue(requestQueue)) + + performSetupOperations(for: request, convertible: request.convertible) + } + + func performUploadRequest(_ request: UploadRequest) { + dispatchPrecondition(condition: .onQueue(requestQueue)) + + performSetupOperations(for: request, convertible: request.convertible) { + do { + let uploadable = try request.upload.createUploadable() + self.rootQueue.async { request.didCreateUploadable(uploadable) } + return true + } catch { + self.rootQueue.async { request.didFailToCreateUploadable(with: error.asAFError(or: .createUploadableFailed(error: error))) } + return false + } + } + } + + func performDownloadRequest(_ request: DownloadRequest) { + dispatchPrecondition(condition: .onQueue(requestQueue)) + + switch request.downloadable { + case let .request(convertible): + performSetupOperations(for: request, convertible: convertible) + case let .resumeData(resumeData): + rootQueue.async { self.didReceiveResumeData(resumeData, for: request) } + } + } + + func performSetupOperations(for request: Request, + convertible: URLRequestConvertible, + shouldCreateTask: @escaping () -> Bool = { true }) { + dispatchPrecondition(condition: .onQueue(requestQueue)) + + let initialRequest: URLRequest + + do { + initialRequest = try convertible.asURLRequest() + try initialRequest.validate() + } catch { + rootQueue.async { request.didFailToCreateURLRequest(with: error.asAFError(or: .createURLRequestFailed(error: error))) } + return + } + + rootQueue.async { request.didCreateInitialURLRequest(initialRequest) } + + guard !request.isCancelled else { return } + + guard let adapter = adapter(for: request) else { + guard shouldCreateTask() else { return } + rootQueue.async { self.didCreateURLRequest(initialRequest, for: request) } + return + } + + let adapterState = RequestAdapterState(requestID: request.id, session: self) + + adapter.adapt(initialRequest, using: adapterState) { result in + do { + let adaptedRequest = try result.get() + try adaptedRequest.validate() + + self.rootQueue.async { request.didAdaptInitialRequest(initialRequest, to: adaptedRequest) } + + guard shouldCreateTask() else { return } + + self.rootQueue.async { self.didCreateURLRequest(adaptedRequest, for: request) } + } catch { + self.rootQueue.async { request.didFailToAdaptURLRequest(initialRequest, withError: .requestAdaptationFailed(error: error)) } + } + } + } + + // MARK: - Task Handling + + func didCreateURLRequest(_ urlRequest: URLRequest, for request: Request) { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + request.didCreateURLRequest(urlRequest) + + guard !request.isCancelled else { return } + + let task = request.task(for: urlRequest, using: session) + requestTaskMap[request] = task + request.didCreateTask(task) + + updateStatesForTask(task, request: request) + } + + func didReceiveResumeData(_ data: Data, for request: DownloadRequest) { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + guard !request.isCancelled else { return } + + let task = request.task(forResumeData: data, using: session) + requestTaskMap[request] = task + request.didCreateTask(task) + + updateStatesForTask(task, request: request) + } + + func updateStatesForTask(_ task: URLSessionTask, request: Request) { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + request.withState { state in + switch state { + case .initialized, .finished: + // Do nothing. + break + case .resumed: + task.resume() + rootQueue.async { request.didResumeTask(task) } + case .suspended: + task.suspend() + rootQueue.async { request.didSuspendTask(task) } + case .cancelled: + // Resume to ensure metrics are gathered. + task.resume() + task.cancel() + rootQueue.async { request.didCancelTask(task) } + } + } + } + + // MARK: - Adapters and Retriers + + func adapter(for request: Request) -> RequestAdapter? { + if let requestInterceptor = request.interceptor, let sessionInterceptor = interceptor { + return Interceptor(adapters: [requestInterceptor, sessionInterceptor]) + } else { + return request.interceptor ?? interceptor + } + } + + func retrier(for request: Request) -> RequestRetrier? { + if let requestInterceptor = request.interceptor, let sessionInterceptor = interceptor { + return Interceptor(retriers: [requestInterceptor, sessionInterceptor]) + } else { + return request.interceptor ?? interceptor + } + } + + // MARK: - Invalidation + + func finishRequestsForDeinit() { + requestTaskMap.requests.forEach { request in + rootQueue.async { + request.finish(error: AFError.sessionDeinitialized) + } + } + } +} + +// MARK: - RequestDelegate + +extension Session: RequestDelegate { + public var sessionConfiguration: URLSessionConfiguration { + session.configuration + } + + public var startImmediately: Bool { startRequestsImmediately } + + public func cleanup(after request: Request) { + activeRequests.remove(request) + } + + public func retryResult(for request: Request, dueTo error: AFError, completion: @escaping (RetryResult) -> Void) { + guard let retrier = retrier(for: request) else { + rootQueue.async { completion(.doNotRetry) } + return + } + + retrier.retry(request, for: self, dueTo: error) { retryResult in + self.rootQueue.async { + guard let retryResultError = retryResult.error else { completion(retryResult); return } + + let retryError = AFError.requestRetryFailed(retryError: retryResultError, originalError: error) + completion(.doNotRetryWithError(retryError)) + } + } + } + + public func retryRequest(_ request: Request, withDelay timeDelay: TimeInterval?) { + rootQueue.async { + let retry: () -> Void = { + guard !request.isCancelled else { return } + + request.prepareForRetry() + self.perform(request) + } + + if let retryDelay = timeDelay { + self.rootQueue.after(retryDelay) { retry() } + } else { + retry() + } + } + } +} + +// MARK: - SessionStateProvider + +extension Session: SessionStateProvider { + func request(for task: URLSessionTask) -> Request? { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + return requestTaskMap[task] + } + + func didGatherMetricsForTask(_ task: URLSessionTask) { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + let didDisassociate = requestTaskMap.disassociateIfNecessaryAfterGatheringMetricsForTask(task) + + if didDisassociate { + waitingCompletions[task]?() + waitingCompletions[task] = nil + } + } + + func didCompleteTask(_ task: URLSessionTask, completion: @escaping () -> Void) { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + let didDisassociate = requestTaskMap.disassociateIfNecessaryAfterCompletingTask(task) + + if didDisassociate { + completion() + } else { + waitingCompletions[task] = completion + } + } + + func credential(for task: URLSessionTask, in protectionSpace: URLProtectionSpace) -> URLCredential? { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + return requestTaskMap[task]?.credential ?? + session.configuration.urlCredentialStorage?.defaultCredential(for: protectionSpace) + } + + func cancelRequestsForSessionInvalidation(with error: Error?) { + dispatchPrecondition(condition: .onQueue(rootQueue)) + + requestTaskMap.requests.forEach { $0.finish(error: AFError.sessionInvalidated(error: error)) } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/SessionDelegate.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/SessionDelegate.swift new file mode 100644 index 0000000..a794d83 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/SessionDelegate.swift @@ -0,0 +1,336 @@ +// +// SessionDelegate.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Class which implements the various `URLSessionDelegate` methods to connect various Alamofire features. +open class SessionDelegate: NSObject { + private let fileManager: FileManager + + weak var stateProvider: SessionStateProvider? + var eventMonitor: EventMonitor? + + /// Creates an instance from the given `FileManager`. + /// + /// - Parameter fileManager: `FileManager` to use for underlying file management, such as moving downloaded files. + /// `.default` by default. + public init(fileManager: FileManager = .default) { + self.fileManager = fileManager + } + + /// Internal method to find and cast requests while maintaining some integrity checking. + /// + /// - Parameters: + /// - task: The `URLSessionTask` for which to find the associated `Request`. + /// - type: The `Request` subclass type to cast any `Request` associate with `task`. + func request(for task: URLSessionTask, as type: R.Type) -> R? { + guard let provider = stateProvider else { + assertionFailure("StateProvider is nil.") + return nil + } + + return provider.request(for: task) as? R + } +} + +/// Type which provides various `Session` state values. +protocol SessionStateProvider: AnyObject { + var serverTrustManager: ServerTrustManager? { get } + var redirectHandler: RedirectHandler? { get } + var cachedResponseHandler: CachedResponseHandler? { get } + + func request(for task: URLSessionTask) -> Request? + func didGatherMetricsForTask(_ task: URLSessionTask) + func didCompleteTask(_ task: URLSessionTask, completion: @escaping () -> Void) + func credential(for task: URLSessionTask, in protectionSpace: URLProtectionSpace) -> URLCredential? + func cancelRequestsForSessionInvalidation(with error: Error?) +} + +// MARK: URLSessionDelegate + +extension SessionDelegate: URLSessionDelegate { + open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + eventMonitor?.urlSession(session, didBecomeInvalidWithError: error) + + stateProvider?.cancelRequestsForSessionInvalidation(with: error) + } +} + +// MARK: URLSessionTaskDelegate + +extension SessionDelegate: URLSessionTaskDelegate { + /// Result of a `URLAuthenticationChallenge` evaluation. + typealias ChallengeEvaluation = (disposition: URLSession.AuthChallengeDisposition, credential: URLCredential?, error: AFError?) + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + eventMonitor?.urlSession(session, task: task, didReceive: challenge) + + let evaluation: ChallengeEvaluation + switch challenge.protectionSpace.authenticationMethod { + case NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest, NSURLAuthenticationMethodNTLM, + NSURLAuthenticationMethodNegotiate: + evaluation = attemptCredentialAuthentication(for: challenge, belongingTo: task) + #if !(os(Linux) || os(Windows)) + case NSURLAuthenticationMethodServerTrust: + evaluation = attemptServerTrustAuthentication(with: challenge) + case NSURLAuthenticationMethodClientCertificate: + evaluation = attemptCredentialAuthentication(for: challenge, belongingTo: task) + #endif + default: + evaluation = (.performDefaultHandling, nil, nil) + } + + if let error = evaluation.error { + stateProvider?.request(for: task)?.didFailTask(task, earlyWithError: error) + } + + completionHandler(evaluation.disposition, evaluation.credential) + } + + #if !(os(Linux) || os(Windows)) + /// Evaluates the server trust `URLAuthenticationChallenge` received. + /// + /// - Parameter challenge: The `URLAuthenticationChallenge`. + /// + /// - Returns: The `ChallengeEvaluation`. + func attemptServerTrustAuthentication(with challenge: URLAuthenticationChallenge) -> ChallengeEvaluation { + let host = challenge.protectionSpace.host + + guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + let trust = challenge.protectionSpace.serverTrust + else { + return (.performDefaultHandling, nil, nil) + } + + do { + guard let evaluator = try stateProvider?.serverTrustManager?.serverTrustEvaluator(forHost: host) else { + return (.performDefaultHandling, nil, nil) + } + + try evaluator.evaluate(trust, forHost: host) + + return (.useCredential, URLCredential(trust: trust), nil) + } catch { + return (.cancelAuthenticationChallenge, nil, error.asAFError(or: .serverTrustEvaluationFailed(reason: .customEvaluationFailed(error: error)))) + } + } + #endif + + /// Evaluates the credential-based authentication `URLAuthenticationChallenge` received for `task`. + /// + /// - Parameters: + /// - challenge: The `URLAuthenticationChallenge`. + /// - task: The `URLSessionTask` which received the challenge. + /// + /// - Returns: The `ChallengeEvaluation`. + func attemptCredentialAuthentication(for challenge: URLAuthenticationChallenge, + belongingTo task: URLSessionTask) -> ChallengeEvaluation { + guard challenge.previousFailureCount == 0 else { + return (.rejectProtectionSpace, nil, nil) + } + + guard let credential = stateProvider?.credential(for: task, in: challenge.protectionSpace) else { + return (.performDefaultHandling, nil, nil) + } + + return (.useCredential, credential, nil) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64) { + eventMonitor?.urlSession(session, + task: task, + didSendBodyData: bytesSent, + totalBytesSent: totalBytesSent, + totalBytesExpectedToSend: totalBytesExpectedToSend) + + stateProvider?.request(for: task)?.updateUploadProgress(totalBytesSent: totalBytesSent, + totalBytesExpectedToSend: totalBytesExpectedToSend) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) { + eventMonitor?.urlSession(session, taskNeedsNewBodyStream: task) + + guard let request = request(for: task, as: UploadRequest.self) else { + assertionFailure("needNewBodyStream did not find UploadRequest.") + completionHandler(nil) + return + } + + completionHandler(request.inputStream()) + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) { + eventMonitor?.urlSession(session, task: task, willPerformHTTPRedirection: response, newRequest: request) + + if let redirectHandler = stateProvider?.request(for: task)?.redirectHandler ?? stateProvider?.redirectHandler { + redirectHandler.task(task, willBeRedirectedTo: request, for: response, completion: completionHandler) + } else { + completionHandler(request) + } + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + eventMonitor?.urlSession(session, task: task, didFinishCollecting: metrics) + + stateProvider?.request(for: task)?.didGatherMetrics(metrics) + + stateProvider?.didGatherMetricsForTask(task) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + eventMonitor?.urlSession(session, task: task, didCompleteWithError: error) + + let request = stateProvider?.request(for: task) + + stateProvider?.didCompleteTask(task) { + request?.didCompleteTask(task, with: error.map { $0.asAFError(or: .sessionTaskFailed(error: $0)) }) + } + } + + @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) + open func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { + eventMonitor?.urlSession(session, taskIsWaitingForConnectivity: task) + } +} + +// MARK: URLSessionDataDelegate + +extension SessionDelegate: URLSessionDataDelegate { + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + eventMonitor?.urlSession(session, dataTask: dataTask, didReceive: data) + + if let request = request(for: dataTask, as: DataRequest.self) { + request.didReceive(data: data) + } else if let request = request(for: dataTask, as: DataStreamRequest.self) { + request.didReceive(data: data) + } else { + assertionFailure("dataTask did not find DataRequest or DataStreamRequest in didReceive") + return + } + } + + open func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + willCacheResponse proposedResponse: CachedURLResponse, + completionHandler: @escaping (CachedURLResponse?) -> Void) { + eventMonitor?.urlSession(session, dataTask: dataTask, willCacheResponse: proposedResponse) + + if let handler = stateProvider?.request(for: dataTask)?.cachedResponseHandler ?? stateProvider?.cachedResponseHandler { + handler.dataTask(dataTask, willCacheResponse: proposedResponse, completion: completionHandler) + } else { + completionHandler(proposedResponse) + } + } +} + +// MARK: URLSessionDownloadDelegate + +extension SessionDelegate: URLSessionDownloadDelegate { + open func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didResumeAtOffset fileOffset: Int64, + expectedTotalBytes: Int64) { + eventMonitor?.urlSession(session, + downloadTask: downloadTask, + didResumeAtOffset: fileOffset, + expectedTotalBytes: expectedTotalBytes) + guard let downloadRequest = request(for: downloadTask, as: DownloadRequest.self) else { + assertionFailure("downloadTask did not find DownloadRequest.") + return + } + + downloadRequest.updateDownloadProgress(bytesWritten: fileOffset, + totalBytesExpectedToWrite: expectedTotalBytes) + } + + open func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) { + eventMonitor?.urlSession(session, + downloadTask: downloadTask, + didWriteData: bytesWritten, + totalBytesWritten: totalBytesWritten, + totalBytesExpectedToWrite: totalBytesExpectedToWrite) + guard let downloadRequest = request(for: downloadTask, as: DownloadRequest.self) else { + assertionFailure("downloadTask did not find DownloadRequest.") + return + } + + downloadRequest.updateDownloadProgress(bytesWritten: bytesWritten, + totalBytesExpectedToWrite: totalBytesExpectedToWrite) + } + + open func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + eventMonitor?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location) + + guard let request = request(for: downloadTask, as: DownloadRequest.self) else { + assertionFailure("downloadTask did not find DownloadRequest.") + return + } + + let (destination, options): (URL, DownloadRequest.Options) + if let response = request.response { + (destination, options) = request.destination(location, response) + } else { + // If there's no response this is likely a local file download, so generate the temporary URL directly. + (destination, options) = (DownloadRequest.defaultDestinationURL(location), []) + } + + eventMonitor?.request(request, didCreateDestinationURL: destination) + + do { + if options.contains(.removePreviousFile), fileManager.fileExists(atPath: destination.path) { + try fileManager.removeItem(at: destination) + } + + if options.contains(.createIntermediateDirectories) { + let directory = destination.deletingLastPathComponent() + try fileManager.createDirectory(at: directory, withIntermediateDirectories: true) + } + + try fileManager.moveItem(at: location, to: destination) + + request.didFinishDownloading(using: downloadTask, with: .success(destination)) + } catch { + request.didFinishDownloading(using: downloadTask, with: .failure(.downloadedFileMoveFailed(error: error, + source: location, + destination: destination))) + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/StringEncoding+Alamofire.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/StringEncoding+Alamofire.swift new file mode 100644 index 0000000..8fa6133 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/StringEncoding+Alamofire.swift @@ -0,0 +1,55 @@ +// +// StringEncoding+Alamofire.swift +// +// Copyright (c) 2020 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension String.Encoding { + /// Creates an encoding from the IANA charset name. + /// + /// - Notes: These mappings match those [provided by CoreFoundation](https://opensource.apple.com/source/CF/CF-476.18/CFStringUtilities.c.auto.html) + /// + /// - Parameter name: IANA charset name. + init?(ianaCharsetName name: String) { + switch name.lowercased() { + case "utf-8": + self = .utf8 + case "iso-8859-1": + self = .isoLatin1 + case "unicode-1-1", "iso-10646-ucs-2", "utf-16": + self = .utf16 + case "utf-16be": + self = .utf16BigEndian + case "utf-16le": + self = .utf16LittleEndian + case "utf-32": + self = .utf32 + case "utf-32be": + self = .utf32BigEndian + case "utf-32le": + self = .utf32LittleEndian + default: + return nil + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/URLConvertible+URLRequestConvertible.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/URLConvertible+URLRequestConvertible.swift new file mode 100644 index 0000000..455c4bc --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/URLConvertible+URLRequestConvertible.swift @@ -0,0 +1,105 @@ +// +// URLConvertible+URLRequestConvertible.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Types adopting the `URLConvertible` protocol can be used to construct `URL`s, which can then be used to construct +/// `URLRequests`. +public protocol URLConvertible { + /// Returns a `URL` from the conforming instance or throws. + /// + /// - Returns: The `URL` created from the instance. + /// - Throws: Any error thrown while creating the `URL`. + func asURL() throws -> URL +} + +extension String: URLConvertible { + /// Returns a `URL` if `self` can be used to initialize a `URL` instance, otherwise throws. + /// + /// - Returns: The `URL` initialized with `self`. + /// - Throws: An `AFError.invalidURL` instance. + public func asURL() throws -> URL { + guard let url = URL(string: self) else { throw AFError.invalidURL(url: self) } + + return url + } +} + +extension URL: URLConvertible { + /// Returns `self`. + public func asURL() throws -> URL { self } +} + +extension URLComponents: URLConvertible { + /// Returns a `URL` if the `self`'s `url` is not nil, otherwise throws. + /// + /// - Returns: The `URL` from the `url` property. + /// - Throws: An `AFError.invalidURL` instance. + public func asURL() throws -> URL { + guard let url = url else { throw AFError.invalidURL(url: self) } + + return url + } +} + +// MARK: - + +/// Types adopting the `URLRequestConvertible` protocol can be used to safely construct `URLRequest`s. +public protocol URLRequestConvertible { + /// Returns a `URLRequest` or throws if an `Error` was encountered. + /// + /// - Returns: A `URLRequest`. + /// - Throws: Any error thrown while constructing the `URLRequest`. + func asURLRequest() throws -> URLRequest +} + +extension URLRequestConvertible { + /// The `URLRequest` returned by discarding any `Error` encountered. + public var urlRequest: URLRequest? { try? asURLRequest() } +} + +extension URLRequest: URLRequestConvertible { + /// Returns `self`. + public func asURLRequest() throws -> URLRequest { self } +} + +// MARK: - + +extension URLRequest { + /// Creates an instance with the specified `url`, `method`, and `headers`. + /// + /// - Parameters: + /// - url: The `URLConvertible` value. + /// - method: The `HTTPMethod`. + /// - headers: The `HTTPHeaders`, `nil` by default. + /// - Throws: Any error thrown while converting the `URLConvertible` to a `URL`. + public init(url: URLConvertible, method: HTTPMethod, headers: HTTPHeaders? = nil) throws { + let url = try url.asURL() + + self.init(url: url) + + httpMethod = method.rawValue + allHTTPHeaderFields = headers?.dictionary + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/URLEncodedFormEncoder.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/URLEncodedFormEncoder.swift new file mode 100644 index 0000000..dfadc2e --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/URLEncodedFormEncoder.swift @@ -0,0 +1,982 @@ +// +// URLEncodedFormEncoder.swift +// +// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// An object that encodes instances into URL-encoded query strings. +/// +/// There is no published specification for how to encode collection types. By default, the convention of appending +/// `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for +/// nested dictionary values (`foo[bar]=baz`) is used. Optionally, `ArrayEncoding` can be used to omit the +/// square brackets appended to array keys. +/// +/// `BoolEncoding` can be used to configure how `Bool` values are encoded. The default behavior is to encode +/// `true` as 1 and `false` as 0. +/// +/// `DateEncoding` can be used to configure how `Date` values are encoded. By default, the `.deferredToDate` +/// strategy is used, which formats dates from their structure. +/// +/// `SpaceEncoding` can be used to configure how spaces are encoded. Modern encodings use percent replacement (`%20`), +/// while older encodings may expect spaces to be replaced with `+`. +/// +/// This type is largely based on Vapor's [`url-encoded-form`](https://github.com/vapor/url-encoded-form) project. +public final class URLEncodedFormEncoder { + /// Encoding to use for `Array` values. + public enum ArrayEncoding { + /// An empty set of square brackets ("[]") are appended to the key for every value. This is the default encoding. + case brackets + /// No brackets are appended to the key and the key is encoded as is. + case noBrackets + /// Brackets containing the item index are appended. This matches the jQuery and Node.js behavior. + case indexInBrackets + + /// Encodes the key according to the encoding. + /// + /// - Parameters: + /// - key: The `key` to encode. + /// - index: When this enum instance is `.indexInBrackets`, the `index` to encode. + /// + /// - Returns: The encoded key. + func encode(_ key: String, atIndex index: Int) -> String { + switch self { + case .brackets: return "\(key)[]" + case .noBrackets: return key + case .indexInBrackets: return "\(key)[\(index)]" + } + } + } + + /// Encoding to use for `Bool` values. + public enum BoolEncoding { + /// Encodes `true` as `1`, `false` as `0`. + case numeric + /// Encodes `true` as "true", `false` as "false". This is the default encoding. + case literal + + /// Encodes the given `Bool` as a `String`. + /// + /// - Parameter value: The `Bool` to encode. + /// + /// - Returns: The encoded `String`. + func encode(_ value: Bool) -> String { + switch self { + case .numeric: return value ? "1" : "0" + case .literal: return value ? "true" : "false" + } + } + } + + /// Encoding to use for `Data` values. + public enum DataEncoding { + /// Defers encoding to the `Data` type. + case deferredToData + /// Encodes `Data` as a Base64-encoded string. This is the default encoding. + case base64 + /// Encode the `Data` as a custom value encoded by the given closure. + case custom((Data) throws -> String) + + /// Encodes `Data` according to the encoding. + /// + /// - Parameter data: The `Data` to encode. + /// + /// - Returns: The encoded `String`, or `nil` if the `Data` should be encoded according to its + /// `Encodable` implementation. + func encode(_ data: Data) throws -> String? { + switch self { + case .deferredToData: return nil + case .base64: return data.base64EncodedString() + case let .custom(encoding): return try encoding(data) + } + } + } + + /// Encoding to use for `Date` values. + public enum DateEncoding { + /// ISO8601 and RFC3339 formatter. + private static let iso8601Formatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = .withInternetDateTime + return formatter + }() + + /// Defers encoding to the `Date` type. This is the default encoding. + case deferredToDate + /// Encodes `Date`s as seconds since midnight UTC on January 1, 1970. + case secondsSince1970 + /// Encodes `Date`s as milliseconds since midnight UTC on January 1, 1970. + case millisecondsSince1970 + /// Encodes `Date`s according to the ISO8601 and RFC3339 standards. + case iso8601 + /// Encodes `Date`s using the given `DateFormatter`. + case formatted(DateFormatter) + /// Encodes `Date`s using the given closure. + case custom((Date) throws -> String) + + /// Encodes the date according to the encoding. + /// + /// - Parameter date: The `Date` to encode. + /// + /// - Returns: The encoded `String`, or `nil` if the `Date` should be encoded according to its + /// `Encodable` implementation. + func encode(_ date: Date) throws -> String? { + switch self { + case .deferredToDate: + return nil + case .secondsSince1970: + return String(date.timeIntervalSince1970) + case .millisecondsSince1970: + return String(date.timeIntervalSince1970 * 1000.0) + case .iso8601: + return DateEncoding.iso8601Formatter.string(from: date) + case let .formatted(formatter): + return formatter.string(from: date) + case let .custom(closure): + return try closure(date) + } + } + } + + /// Encoding to use for keys. + /// + /// This type is derived from [`JSONEncoder`'s `KeyEncodingStrategy`](https://github.com/apple/swift/blob/6aa313b8dd5f05135f7f878eccc1db6f9fbe34ff/stdlib/public/Darwin/Foundation/JSONEncoder.swift#L128) + /// and [`XMLEncoder`s `KeyEncodingStrategy`](https://github.com/MaxDesiatov/XMLCoder/blob/master/Sources/XMLCoder/Encoder/XMLEncoder.swift#L102). + public enum KeyEncoding { + /// Use the keys specified by each type. This is the default encoding. + case useDefaultKeys + /// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key. + /// + /// Capital characters are determined by testing membership in + /// `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters` + /// (Unicode General Categories Lu and Lt). + /// The conversion to lower case uses `Locale.system`, also known as + /// the ICU "root" locale. This means the result is consistent + /// regardless of the current user's locale and language preferences. + /// + /// Converting from camel case to snake case: + /// 1. Splits words at the boundary of lower-case to upper-case + /// 2. Inserts `_` between words + /// 3. Lowercases the entire string + /// 4. Preserves starting and ending `_`. + /// + /// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`. + /// + /// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted. + case convertToSnakeCase + /// Same as convertToSnakeCase, but using `-` instead of `_`. + /// For example `oneTwoThree` becomes `one-two-three`. + case convertToKebabCase + /// Capitalize the first letter only. + /// For example `oneTwoThree` becomes `OneTwoThree`. + case capitalized + /// Uppercase all letters. + /// For example `oneTwoThree` becomes `ONETWOTHREE`. + case uppercased + /// Lowercase all letters. + /// For example `oneTwoThree` becomes `onetwothree`. + case lowercased + /// A custom encoding using the provided closure. + case custom((String) -> String) + + func encode(_ key: String) -> String { + switch self { + case .useDefaultKeys: return key + case .convertToSnakeCase: return convertToSnakeCase(key) + case .convertToKebabCase: return convertToKebabCase(key) + case .capitalized: return String(key.prefix(1).uppercased() + key.dropFirst()) + case .uppercased: return key.uppercased() + case .lowercased: return key.lowercased() + case let .custom(encoding): return encoding(key) + } + } + + private func convertToSnakeCase(_ key: String) -> String { + convert(key, usingSeparator: "_") + } + + private func convertToKebabCase(_ key: String) -> String { + convert(key, usingSeparator: "-") + } + + private func convert(_ key: String, usingSeparator separator: String) -> String { + guard !key.isEmpty else { return key } + + var words: [Range] = [] + // The general idea of this algorithm is to split words on + // transition from lower to upper case, then on transition of >1 + // upper case characters to lowercase + // + // myProperty -> my_property + // myURLProperty -> my_url_property + // + // It is assumed, per Swift naming conventions, that the first character of the key is lowercase. + var wordStart = key.startIndex + var searchRange = key.index(after: wordStart)..1 capital letters. Turn those into a word, stopping at the capital before the lower case character. + let beforeLowerIndex = key.index(before: lowerCaseRange.lowerBound) + words.append(upperCaseRange.lowerBound.. String { + switch self { + case .percentEscaped: return string.replacingOccurrences(of: " ", with: "%20") + case .plusReplaced: return string.replacingOccurrences(of: " ", with: "+") + } + } + } + + /// `URLEncodedFormEncoder` error. + public enum Error: Swift.Error { + /// An invalid root object was created by the encoder. Only keyed values are valid. + case invalidRootObject(String) + + var localizedDescription: String { + switch self { + case let .invalidRootObject(object): + return "URLEncodedFormEncoder requires keyed root object. Received \(object) instead." + } + } + } + + /// Whether or not to sort the encoded key value pairs. + /// + /// - Note: This setting ensures a consistent ordering for all encodings of the same parameters. When set to `false`, + /// encoded `Dictionary` values may have a different encoded order each time they're encoded due to + /// ` Dictionary`'s random storage order, but `Encodable` types will maintain their encoded order. + public let alphabetizeKeyValuePairs: Bool + /// The `ArrayEncoding` to use. + public let arrayEncoding: ArrayEncoding + /// The `BoolEncoding` to use. + public let boolEncoding: BoolEncoding + /// THe `DataEncoding` to use. + public let dataEncoding: DataEncoding + /// The `DateEncoding` to use. + public let dateEncoding: DateEncoding + /// The `KeyEncoding` to use. + public let keyEncoding: KeyEncoding + /// The `SpaceEncoding` to use. + public let spaceEncoding: SpaceEncoding + /// The `CharacterSet` of allowed (non-escaped) characters. + public var allowedCharacters: CharacterSet + + /// Creates an instance from the supplied parameters. + /// + /// - Parameters: + /// - alphabetizeKeyValuePairs: Whether or not to sort the encoded key value pairs. `true` by default. + /// - arrayEncoding: The `ArrayEncoding` to use. `.brackets` by default. + /// - boolEncoding: The `BoolEncoding` to use. `.numeric` by default. + /// - dataEncoding: The `DataEncoding` to use. `.base64` by default. + /// - dateEncoding: The `DateEncoding` to use. `.deferredToDate` by default. + /// - keyEncoding: The `KeyEncoding` to use. `.useDefaultKeys` by default. + /// - spaceEncoding: The `SpaceEncoding` to use. `.percentEscaped` by default. + /// - allowedCharacters: The `CharacterSet` of allowed (non-escaped) characters. `.afURLQueryAllowed` by + /// default. + public init(alphabetizeKeyValuePairs: Bool = true, + arrayEncoding: ArrayEncoding = .brackets, + boolEncoding: BoolEncoding = .numeric, + dataEncoding: DataEncoding = .base64, + dateEncoding: DateEncoding = .deferredToDate, + keyEncoding: KeyEncoding = .useDefaultKeys, + spaceEncoding: SpaceEncoding = .percentEscaped, + allowedCharacters: CharacterSet = .afURLQueryAllowed) { + self.alphabetizeKeyValuePairs = alphabetizeKeyValuePairs + self.arrayEncoding = arrayEncoding + self.boolEncoding = boolEncoding + self.dataEncoding = dataEncoding + self.dateEncoding = dateEncoding + self.keyEncoding = keyEncoding + self.spaceEncoding = spaceEncoding + self.allowedCharacters = allowedCharacters + } + + func encode(_ value: Encodable) throws -> URLEncodedFormComponent { + let context = URLEncodedFormContext(.object([])) + let encoder = _URLEncodedFormEncoder(context: context, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + try value.encode(to: encoder) + + return context.component + } + + /// Encodes the `value` as a URL form encoded `String`. + /// + /// - Parameter value: The `Encodable` value.` + /// + /// - Returns: The encoded `String`. + /// - Throws: An `Error` or `EncodingError` instance if encoding fails. + public func encode(_ value: Encodable) throws -> String { + let component: URLEncodedFormComponent = try encode(value) + + guard case let .object(object) = component else { + throw Error.invalidRootObject("\(component)") + } + + let serializer = URLEncodedFormSerializer(alphabetizeKeyValuePairs: alphabetizeKeyValuePairs, + arrayEncoding: arrayEncoding, + keyEncoding: keyEncoding, + spaceEncoding: spaceEncoding, + allowedCharacters: allowedCharacters) + let query = serializer.serialize(object) + + return query + } + + /// Encodes the value as `Data`. This is performed by first creating an encoded `String` and then returning the + /// `.utf8` data. + /// + /// - Parameter value: The `Encodable` value. + /// + /// - Returns: The encoded `Data`. + /// + /// - Throws: An `Error` or `EncodingError` instance if encoding fails. + public func encode(_ value: Encodable) throws -> Data { + let string: String = try encode(value) + + return Data(string.utf8) + } +} + +final class _URLEncodedFormEncoder { + var codingPath: [CodingKey] + // Returns an empty dictionary, as this encoder doesn't support userInfo. + var userInfo: [CodingUserInfoKey: Any] { [:] } + + let context: URLEncodedFormContext + + private let boolEncoding: URLEncodedFormEncoder.BoolEncoding + private let dataEncoding: URLEncodedFormEncoder.DataEncoding + private let dateEncoding: URLEncodedFormEncoder.DateEncoding + + init(context: URLEncodedFormContext, + codingPath: [CodingKey] = [], + boolEncoding: URLEncodedFormEncoder.BoolEncoding, + dataEncoding: URLEncodedFormEncoder.DataEncoding, + dateEncoding: URLEncodedFormEncoder.DateEncoding) { + self.context = context + self.codingPath = codingPath + self.boolEncoding = boolEncoding + self.dataEncoding = dataEncoding + self.dateEncoding = dateEncoding + } +} + +extension _URLEncodedFormEncoder: Encoder { + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { + let container = _URLEncodedFormEncoder.KeyedContainer(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + return KeyedEncodingContainer(container) + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + _URLEncodedFormEncoder.UnkeyedContainer(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + } + + func singleValueContainer() -> SingleValueEncodingContainer { + _URLEncodedFormEncoder.SingleValueContainer(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + } +} + +final class URLEncodedFormContext { + var component: URLEncodedFormComponent + + init(_ component: URLEncodedFormComponent) { + self.component = component + } +} + +enum URLEncodedFormComponent { + typealias Object = [(key: String, value: URLEncodedFormComponent)] + + case string(String) + case array([URLEncodedFormComponent]) + case object(Object) + + /// Converts self to an `[URLEncodedFormData]` or returns `nil` if not convertible. + var array: [URLEncodedFormComponent]? { + switch self { + case let .array(array): return array + default: return nil + } + } + + /// Converts self to an `Object` or returns `nil` if not convertible. + var object: Object? { + switch self { + case let .object(object): return object + default: return nil + } + } + + /// Sets self to the supplied value at a given path. + /// + /// data.set(to: "hello", at: ["path", "to", "value"]) + /// + /// - parameters: + /// - value: Value of `Self` to set at the supplied path. + /// - path: `CodingKey` path to update with the supplied value. + public mutating func set(to value: URLEncodedFormComponent, at path: [CodingKey]) { + set(&self, to: value, at: path) + } + + /// Recursive backing method to `set(to:at:)`. + private func set(_ context: inout URLEncodedFormComponent, to value: URLEncodedFormComponent, at path: [CodingKey]) { + guard !path.isEmpty else { + context = value + return + } + + let end = path[0] + var child: URLEncodedFormComponent + switch path.count { + case 1: + child = value + case 2...: + if let index = end.intValue { + let array = context.array ?? [] + if array.count > index { + child = array[index] + } else { + child = .array([]) + } + set(&child, to: value, at: Array(path[1...])) + } else { + child = context.object?.first { $0.key == end.stringValue }?.value ?? .object(.init()) + set(&child, to: value, at: Array(path[1...])) + } + default: fatalError("Unreachable") + } + + if let index = end.intValue { + if var array = context.array { + if array.count > index { + array[index] = child + } else { + array.append(child) + } + context = .array(array) + } else { + context = .array([child]) + } + } else { + if var object = context.object { + if let index = object.firstIndex(where: { $0.key == end.stringValue }) { + object[index] = (key: end.stringValue, value: child) + } else { + object.append((key: end.stringValue, value: child)) + } + context = .object(object) + } else { + context = .object([(key: end.stringValue, value: child)]) + } + } + } +} + +struct AnyCodingKey: CodingKey, Hashable { + let stringValue: String + let intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + intValue = nil + } + + init?(intValue: Int) { + stringValue = "\(intValue)" + self.intValue = intValue + } + + init(_ base: Key) where Key: CodingKey { + if let intValue = base.intValue { + self.init(intValue: intValue)! + } else { + self.init(stringValue: base.stringValue)! + } + } +} + +extension _URLEncodedFormEncoder { + final class KeyedContainer where Key: CodingKey { + var codingPath: [CodingKey] + + private let context: URLEncodedFormContext + private let boolEncoding: URLEncodedFormEncoder.BoolEncoding + private let dataEncoding: URLEncodedFormEncoder.DataEncoding + private let dateEncoding: URLEncodedFormEncoder.DateEncoding + + init(context: URLEncodedFormContext, + codingPath: [CodingKey], + boolEncoding: URLEncodedFormEncoder.BoolEncoding, + dataEncoding: URLEncodedFormEncoder.DataEncoding, + dateEncoding: URLEncodedFormEncoder.DateEncoding) { + self.context = context + self.codingPath = codingPath + self.boolEncoding = boolEncoding + self.dataEncoding = dataEncoding + self.dateEncoding = dateEncoding + } + + private func nestedCodingPath(for key: CodingKey) -> [CodingKey] { + codingPath + [key] + } + } +} + +extension _URLEncodedFormEncoder.KeyedContainer: KeyedEncodingContainerProtocol { + func encodeNil(forKey key: Key) throws { + let context = EncodingError.Context(codingPath: codingPath, + debugDescription: "URLEncodedFormEncoder cannot encode nil values.") + throw EncodingError.invalidValue("\(key): nil", context) + } + + func encode(_ value: T, forKey key: Key) throws where T: Encodable { + var container = nestedSingleValueEncoder(for: key) + try container.encode(value) + } + + func nestedSingleValueEncoder(for key: Key) -> SingleValueEncodingContainer { + let container = _URLEncodedFormEncoder.SingleValueContainer(context: context, + codingPath: nestedCodingPath(for: key), + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + + return container + } + + func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + let container = _URLEncodedFormEncoder.UnkeyedContainer(context: context, + codingPath: nestedCodingPath(for: key), + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + + return container + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey: CodingKey { + let container = _URLEncodedFormEncoder.KeyedContainer(context: context, + codingPath: nestedCodingPath(for: key), + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + + return KeyedEncodingContainer(container) + } + + func superEncoder() -> Encoder { + _URLEncodedFormEncoder(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + } + + func superEncoder(forKey key: Key) -> Encoder { + _URLEncodedFormEncoder(context: context, + codingPath: nestedCodingPath(for: key), + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + } +} + +extension _URLEncodedFormEncoder { + final class SingleValueContainer { + var codingPath: [CodingKey] + + private var canEncodeNewValue = true + + private let context: URLEncodedFormContext + private let boolEncoding: URLEncodedFormEncoder.BoolEncoding + private let dataEncoding: URLEncodedFormEncoder.DataEncoding + private let dateEncoding: URLEncodedFormEncoder.DateEncoding + + init(context: URLEncodedFormContext, + codingPath: [CodingKey], + boolEncoding: URLEncodedFormEncoder.BoolEncoding, + dataEncoding: URLEncodedFormEncoder.DataEncoding, + dateEncoding: URLEncodedFormEncoder.DateEncoding) { + self.context = context + self.codingPath = codingPath + self.boolEncoding = boolEncoding + self.dataEncoding = dataEncoding + self.dateEncoding = dateEncoding + } + + private func checkCanEncode(value: Any?) throws { + guard canEncodeNewValue else { + let context = EncodingError.Context(codingPath: codingPath, + debugDescription: "Attempt to encode value through single value container when previously value already encoded.") + throw EncodingError.invalidValue(value as Any, context) + } + } + } +} + +extension _URLEncodedFormEncoder.SingleValueContainer: SingleValueEncodingContainer { + func encodeNil() throws { + try checkCanEncode(value: nil) + defer { canEncodeNewValue = false } + + let context = EncodingError.Context(codingPath: codingPath, + debugDescription: "URLEncodedFormEncoder cannot encode nil values.") + throw EncodingError.invalidValue("nil", context) + } + + func encode(_ value: Bool) throws { + try encode(value, as: String(boolEncoding.encode(value))) + } + + func encode(_ value: String) throws { + try encode(value, as: value) + } + + func encode(_ value: Double) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Float) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int8) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int16) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int32) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: Int64) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt8) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt16) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt32) throws { + try encode(value, as: String(value)) + } + + func encode(_ value: UInt64) throws { + try encode(value, as: String(value)) + } + + private func encode(_ value: T, as string: String) throws where T: Encodable { + try checkCanEncode(value: value) + defer { canEncodeNewValue = false } + + context.component.set(to: .string(string), at: codingPath) + } + + func encode(_ value: T) throws where T: Encodable { + switch value { + case let date as Date: + guard let string = try dateEncoding.encode(date) else { + try attemptToEncode(value) + return + } + + try encode(value, as: string) + case let data as Data: + guard let string = try dataEncoding.encode(data) else { + try attemptToEncode(value) + return + } + + try encode(value, as: string) + case let decimal as Decimal: + // Decimal's `Encodable` implementation returns an object, not a single value, so override it. + try encode(value, as: String(describing: decimal)) + default: + try attemptToEncode(value) + } + } + + private func attemptToEncode(_ value: T) throws where T: Encodable { + try checkCanEncode(value: value) + defer { canEncodeNewValue = false } + + let encoder = _URLEncodedFormEncoder(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + try value.encode(to: encoder) + } +} + +extension _URLEncodedFormEncoder { + final class UnkeyedContainer { + var codingPath: [CodingKey] + + var count = 0 + var nestedCodingPath: [CodingKey] { + codingPath + [AnyCodingKey(intValue: count)!] + } + + private let context: URLEncodedFormContext + private let boolEncoding: URLEncodedFormEncoder.BoolEncoding + private let dataEncoding: URLEncodedFormEncoder.DataEncoding + private let dateEncoding: URLEncodedFormEncoder.DateEncoding + + init(context: URLEncodedFormContext, + codingPath: [CodingKey], + boolEncoding: URLEncodedFormEncoder.BoolEncoding, + dataEncoding: URLEncodedFormEncoder.DataEncoding, + dateEncoding: URLEncodedFormEncoder.DateEncoding) { + self.context = context + self.codingPath = codingPath + self.boolEncoding = boolEncoding + self.dataEncoding = dataEncoding + self.dateEncoding = dateEncoding + } + } +} + +extension _URLEncodedFormEncoder.UnkeyedContainer: UnkeyedEncodingContainer { + func encodeNil() throws { + let context = EncodingError.Context(codingPath: codingPath, + debugDescription: "URLEncodedFormEncoder cannot encode nil values.") + throw EncodingError.invalidValue("nil", context) + } + + func encode(_ value: T) throws where T: Encodable { + var container = nestedSingleValueContainer() + try container.encode(value) + } + + func nestedSingleValueContainer() -> SingleValueEncodingContainer { + defer { count += 1 } + + return _URLEncodedFormEncoder.SingleValueContainer(context: context, + codingPath: nestedCodingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey: CodingKey { + defer { count += 1 } + let container = _URLEncodedFormEncoder.KeyedContainer(context: context, + codingPath: nestedCodingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + + return KeyedEncodingContainer(container) + } + + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + defer { count += 1 } + + return _URLEncodedFormEncoder.UnkeyedContainer(context: context, + codingPath: nestedCodingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + } + + func superEncoder() -> Encoder { + defer { count += 1 } + + return _URLEncodedFormEncoder(context: context, + codingPath: codingPath, + boolEncoding: boolEncoding, + dataEncoding: dataEncoding, + dateEncoding: dateEncoding) + } +} + +final class URLEncodedFormSerializer { + private let alphabetizeKeyValuePairs: Bool + private let arrayEncoding: URLEncodedFormEncoder.ArrayEncoding + private let keyEncoding: URLEncodedFormEncoder.KeyEncoding + private let spaceEncoding: URLEncodedFormEncoder.SpaceEncoding + private let allowedCharacters: CharacterSet + + init(alphabetizeKeyValuePairs: Bool, + arrayEncoding: URLEncodedFormEncoder.ArrayEncoding, + keyEncoding: URLEncodedFormEncoder.KeyEncoding, + spaceEncoding: URLEncodedFormEncoder.SpaceEncoding, + allowedCharacters: CharacterSet) { + self.alphabetizeKeyValuePairs = alphabetizeKeyValuePairs + self.arrayEncoding = arrayEncoding + self.keyEncoding = keyEncoding + self.spaceEncoding = spaceEncoding + self.allowedCharacters = allowedCharacters + } + + func serialize(_ object: URLEncodedFormComponent.Object) -> String { + var output: [String] = [] + for (key, component) in object { + let value = serialize(component, forKey: key) + output.append(value) + } + output = alphabetizeKeyValuePairs ? output.sorted() : output + + return output.joinedWithAmpersands() + } + + func serialize(_ component: URLEncodedFormComponent, forKey key: String) -> String { + switch component { + case let .string(string): return "\(escape(keyEncoding.encode(key)))=\(escape(string))" + case let .array(array): return serialize(array, forKey: key) + case let .object(object): return serialize(object, forKey: key) + } + } + + func serialize(_ object: URLEncodedFormComponent.Object, forKey key: String) -> String { + var segments: [String] = object.map { subKey, value in + let keyPath = "[\(subKey)]" + return serialize(value, forKey: key + keyPath) + } + segments = alphabetizeKeyValuePairs ? segments.sorted() : segments + + return segments.joinedWithAmpersands() + } + + func serialize(_ array: [URLEncodedFormComponent], forKey key: String) -> String { + var segments: [String] = array.enumerated().map { index, component in + let keyPath = arrayEncoding.encode(key, atIndex: index) + return serialize(component, forKey: keyPath) + } + segments = alphabetizeKeyValuePairs ? segments.sorted() : segments + + return segments.joinedWithAmpersands() + } + + func escape(_ query: String) -> String { + var allowedCharactersWithSpace = allowedCharacters + allowedCharactersWithSpace.insert(charactersIn: " ") + let escapedQuery = query.addingPercentEncoding(withAllowedCharacters: allowedCharactersWithSpace) ?? query + let spaceEncodedQuery = spaceEncoding.encode(escapedQuery) + + return spaceEncodedQuery + } +} + +extension Array where Element == String { + func joinedWithAmpersands() -> String { + joined(separator: "&") + } +} + +extension CharacterSet { + /// Creates a CharacterSet from RFC 3986 allowed characters. + /// + /// RFC 3986 states that the following characters are "reserved" characters. + /// + /// - General Delimiters: ":", "#", "[", "]", "@", "?", "/" + /// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "=" + /// + /// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow + /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/" + /// should be percent-escaped in the query string. + public static let afURLQueryAllowed: CharacterSet = { + let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 + let subDelimitersToEncode = "!$&'()*+,;=" + let encodableDelimiters = CharacterSet(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") + + return CharacterSet.urlQueryAllowed.subtracting(encodableDelimiters) + }() +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/URLRequest+Alamofire.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/URLRequest+Alamofire.swift new file mode 100644 index 0000000..be27c8e --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/URLRequest+Alamofire.swift @@ -0,0 +1,39 @@ +// +// URLRequest+Alamofire.swift +// +// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension URLRequest { + /// Returns the `httpMethod` as Alamofire's `HTTPMethod` type. + public var method: HTTPMethod? { + get { httpMethod.flatMap(HTTPMethod.init) } + set { httpMethod = newValue?.rawValue } + } + + public func validate() throws { + if method == .get, let bodyData = httpBody { + throw AFError.urlRequestValidationFailed(reason: .bodyDataInGETRequest(bodyData)) + } + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/URLSessionConfiguration+Alamofire.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/URLSessionConfiguration+Alamofire.swift new file mode 100644 index 0000000..292a8fe --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/URLSessionConfiguration+Alamofire.swift @@ -0,0 +1,46 @@ +// +// URLSessionConfiguration+Alamofire.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension URLSessionConfiguration: AlamofireExtended {} +extension AlamofireExtension where ExtendedType: URLSessionConfiguration { + /// Alamofire's default configuration. Same as `URLSessionConfiguration.default` but adds Alamofire default + /// `Accept-Language`, `Accept-Encoding`, and `User-Agent` headers. + public static var `default`: URLSessionConfiguration { + let configuration = URLSessionConfiguration.default + configuration.headers = .default + + return configuration + } + + /// `.ephemeral` configuration with Alamofire's default `Accept-Language`, `Accept-Encoding`, and `User-Agent` + /// headers. + public static var ephemeral: URLSessionConfiguration { + let configuration = URLSessionConfiguration.ephemeral + configuration.headers = .default + + return configuration + } +} diff --git a/jaem/week7/CatStaGram/Pods/Alamofire/Source/Validation.swift b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Validation.swift new file mode 100644 index 0000000..1dc3025 --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Alamofire/Source/Validation.swift @@ -0,0 +1,302 @@ +// +// Validation.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension Request { + // MARK: Helper Types + + fileprivate typealias ErrorReason = AFError.ResponseValidationFailureReason + + /// Used to represent whether a validation succeeded or failed. + public typealias ValidationResult = Result + + fileprivate struct MIMEType { + let type: String + let subtype: String + + var isWildcard: Bool { type == "*" && subtype == "*" } + + init?(_ string: String) { + let components: [String] = { + let stripped = string.trimmingCharacters(in: .whitespacesAndNewlines) + let split = stripped[..<(stripped.range(of: ";")?.lowerBound ?? stripped.endIndex)] + + return split.components(separatedBy: "/") + }() + + if let type = components.first, let subtype = components.last { + self.type = type + self.subtype = subtype + } else { + return nil + } + } + + func matches(_ mime: MIMEType) -> Bool { + switch (type, subtype) { + case (mime.type, mime.subtype), (mime.type, "*"), ("*", mime.subtype), ("*", "*"): + return true + default: + return false + } + } + } + + // MARK: Properties + + fileprivate var acceptableStatusCodes: Range { 200..<300 } + + fileprivate var acceptableContentTypes: [String] { + if let accept = request?.value(forHTTPHeaderField: "Accept") { + return accept.components(separatedBy: ",") + } + + return ["*/*"] + } + + // MARK: Status Code + + fileprivate func validate(statusCode acceptableStatusCodes: S, + response: HTTPURLResponse) + -> ValidationResult + where S.Iterator.Element == Int { + if acceptableStatusCodes.contains(response.statusCode) { + return .success(()) + } else { + let reason: ErrorReason = .unacceptableStatusCode(code: response.statusCode) + return .failure(AFError.responseValidationFailed(reason: reason)) + } + } + + // MARK: Content Type + + fileprivate func validate(contentType acceptableContentTypes: S, + response: HTTPURLResponse, + data: Data?) + -> ValidationResult + where S.Iterator.Element == String { + guard let data = data, !data.isEmpty else { return .success(()) } + + return validate(contentType: acceptableContentTypes, response: response) + } + + fileprivate func validate(contentType acceptableContentTypes: S, + response: HTTPURLResponse) + -> ValidationResult + where S.Iterator.Element == String { + guard + let responseContentType = response.mimeType, + let responseMIMEType = MIMEType(responseContentType) + else { + for contentType in acceptableContentTypes { + if let mimeType = MIMEType(contentType), mimeType.isWildcard { + return .success(()) + } + } + + let error: AFError = { + let reason: ErrorReason = .missingContentType(acceptableContentTypes: acceptableContentTypes.sorted()) + return AFError.responseValidationFailed(reason: reason) + }() + + return .failure(error) + } + + for contentType in acceptableContentTypes { + if let acceptableMIMEType = MIMEType(contentType), acceptableMIMEType.matches(responseMIMEType) { + return .success(()) + } + } + + let error: AFError = { + let reason: ErrorReason = .unacceptableContentType(acceptableContentTypes: acceptableContentTypes.sorted(), + responseContentType: responseContentType) + + return AFError.responseValidationFailed(reason: reason) + }() + + return .failure(error) + } +} + +// MARK: - + +extension DataRequest { + /// A closure used to validate a request that takes a URL request, a URL response and data, and returns whether the + /// request was valid. + public typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult + + /// Validates that the response has a status code in the specified sequence. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes. + /// + /// - Returns: The instance. + @discardableResult + public func validate(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int { + validate { [unowned self] _, response, _ in + self.validate(statusCode: acceptableStatusCodes, response: response) + } + } + + /// Validates that the response has a content type in the specified sequence. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes. + /// + /// - returns: The request. + @discardableResult + public func validate(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String { + validate { [unowned self] _, response, data in + self.validate(contentType: acceptableContentTypes(), response: response, data: data) + } + } + + /// Validates that the response has a status code in the default acceptable range of 200...299, and that the content + /// type matches any specified in the Accept HTTP header field. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - returns: The request. + @discardableResult + public func validate() -> Self { + let contentTypes: () -> [String] = { [unowned self] in + self.acceptableContentTypes + } + return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes()) + } +} + +extension DataStreamRequest { + /// A closure used to validate a request that takes a `URLRequest` and `HTTPURLResponse` and returns whether the + /// request was valid. + public typealias Validation = (_ request: URLRequest?, _ response: HTTPURLResponse) -> ValidationResult + + /// Validates that the response has a status code in the specified sequence. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes. + /// + /// - Returns: The instance. + @discardableResult + public func validate(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int { + validate { [unowned self] _, response in + self.validate(statusCode: acceptableStatusCodes, response: response) + } + } + + /// Validates that the response has a content type in the specified sequence. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes. + /// + /// - returns: The request. + @discardableResult + public func validate(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String { + validate { [unowned self] _, response in + self.validate(contentType: acceptableContentTypes(), response: response) + } + } + + /// Validates that the response has a status code in the default acceptable range of 200...299, and that the content + /// type matches any specified in the Accept HTTP header field. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - Returns: The instance. + @discardableResult + public func validate() -> Self { + let contentTypes: () -> [String] = { [unowned self] in + self.acceptableContentTypes + } + return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes()) + } +} + +// MARK: - + +extension DownloadRequest { + /// A closure used to validate a request that takes a URL request, a URL response, a temporary URL and a + /// destination URL, and returns whether the request was valid. + public typealias Validation = (_ request: URLRequest?, + _ response: HTTPURLResponse, + _ fileURL: URL?) + -> ValidationResult + + /// Validates that the response has a status code in the specified sequence. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes. + /// + /// - Returns: The instance. + @discardableResult + public func validate(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int { + validate { [unowned self] _, response, _ in + self.validate(statusCode: acceptableStatusCodes, response: response) + } + } + + /// Validates that the response has a content type in the specified sequence. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes. + /// + /// - returns: The request. + @discardableResult + public func validate(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String { + validate { [unowned self] _, response, fileURL in + guard let validFileURL = fileURL else { + return .failure(AFError.responseValidationFailed(reason: .dataFileNil)) + } + + do { + let data = try Data(contentsOf: validFileURL) + return self.validate(contentType: acceptableContentTypes(), response: response, data: data) + } catch { + return .failure(AFError.responseValidationFailed(reason: .dataFileReadFailed(at: validFileURL))) + } + } + } + + /// Validates that the response has a status code in the default acceptable range of 200...299, and that the content + /// type matches any specified in the Accept HTTP header field. + /// + /// If validation fails, subsequent calls to response handlers will have an associated error. + /// + /// - returns: The request. + @discardableResult + public func validate() -> Self { + let contentTypes = { [unowned self] in + self.acceptableContentTypes + } + return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes()) + } +} diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/LICENSE b/jaem/week7/CatStaGram/Pods/Kingfisher/LICENSE new file mode 100644 index 0000000..80888ba --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2019 Wei Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/jaem/week7/CatStaGram/Pods/Kingfisher/README.md b/jaem/week7/CatStaGram/Pods/Kingfisher/README.md new file mode 100644 index 0000000..b486aee --- /dev/null +++ b/jaem/week7/CatStaGram/Pods/Kingfisher/README.md @@ -0,0 +1,258 @@ +

    + + + + + +
    + + +

  • 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 literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_white.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_white.imageset/Contents.json" new file mode 100644 index 0000000..8e4ea3c --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_white.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_home_home_white.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_white.imageset/ic_home_home_white.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_white.imageset/ic_home_home_white.png" new file mode 100644 index 0000000000000000000000000000000000000000..1072f82754d59e90091ebafa15762c84717b19be GIT binary patch literal 9869 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 RqH~&O@PP0B%Dv&|{s)n$Y-<1j literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_reels.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_reels.imageset/Contents.json" new file mode 100644 index 0000000..74b405e --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_reels.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_home_reels.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_reels.imageset/ic_home_reels.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_reels.imageset/ic_home_reels.png" new file mode 100644 index 0000000000000000000000000000000000000000..3bfdfe623e2026560fa071c7c992f0c46ff201ad GIT binary patch literal 12969 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 B!zBO! literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_send.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_send.imageset/Contents.json" new file mode 100644 index 0000000..41e2aaa --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_send.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_home_send.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_send.imageset/ic_home_send.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_send.imageset/ic_home_send.png" new file mode 100644 index 0000000000000000000000000000000000000000..76aae87abd384bff49f5f4be35bf6472e88810c4 GIT binary patch literal 12495 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 literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_black.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_black.imageset/Contents.json" new file mode 100644 index 0000000..07ab32c --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_black.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_home_shop_black.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_black.imageset/ic_home_shop_black.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_black.imageset/ic_home_shop_black.png" new file mode 100644 index 0000000000000000000000000000000000000000..c89519d97051f46be961c78a3dc39fccc6f481a0 GIT binary patch literal 7889 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 literal 0 HcmV?d00001 diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_white.imageset/Contents.json" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_white.imageset/Contents.json" new file mode 100644 index 0000000..7f0cf60 --- /dev/null +++ "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_white.imageset/Contents.json" @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_home_shop_white.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_white.imageset/ic_home_shop_white.png" "b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_white.imageset/ic_home_shop_white.png" new file mode 100644 index 0000000000000000000000000000000000000000..34490e5addc50d4bb3cbd5fee291f17a3ef239e5 GIT binary patch literal 7004 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 literal 0 HcmV?d00001 diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Base.lproj/LaunchScreen.storyboard b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Base.lproj/Main.storyboard b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Base.lproj/Main.storyboard new file mode 100644 index 0000000..d66ad9e --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Base.lproj/Main.storyboard @@ -0,0 +1,536 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/FeedTableViewCell.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/FeedTableViewCell.swift new file mode 100644 index 0000000..d8766a6 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/FeedTableViewCell.swift @@ -0,0 +1,60 @@ +// +// FeedTableViewCell.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/16. +// + +import UIKit + +class FeedTableViewCell: UITableViewCell { + + @IBOutlet weak var imageViewUserProfile: UIImageView! + @IBOutlet weak var labelUserName: UILabel! + @IBOutlet weak var imageViewFeed: UIImageView! + + @IBOutlet weak var imageViewMyProfile: UIImageView! + @IBOutlet weak var labelFeed: UILabel! + @IBOutlet weak var labelHowManyLike: UILabel! + @IBOutlet weak var buttonIsBookMark: UIButton! + @IBOutlet weak var buttonIsHeart: UIButton! + + @IBAction func actionIsHeart(_ sender: Any) { + if buttonIsHeart.isSelected { + buttonIsHeart.isSelected = false + }else{ + buttonIsHeart.isSelected = true + } + } + + @IBAction func actionIsBookMark(_ sender: Any) { + if buttonIsBookMark.isSelected { + buttonIsBookMark.isSelected = false + }else{ + buttonIsBookMark.isSelected = true + } + } + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + imageViewUserProfile.layer.cornerRadius = 12.5 + imageViewUserProfile.clipsToBounds = true + + imageViewMyProfile.layer.cornerRadius = 12.5 + imageViewMyProfile.clipsToBounds = true + + let fontSize = UIFont.boldSystemFont(ofSize: 9) + let attributedStr = NSMutableAttributedString(string: labelFeed.text ?? "") + attributedStr.addAttribute(.font, value: fontSize, range: NSRange.init(location: 0, length: 8)) + + labelFeed.attributedText = attributedStr + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/FeedTableViewCell.xib b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/FeedTableViewCell.xib new file mode 100644 index 0000000..c7e8eda --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/FeedTableViewCell.xib @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryCollectionViewCell.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryCollectionViewCell.swift new file mode 100644 index 0000000..c86d742 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryCollectionViewCell.swift @@ -0,0 +1,26 @@ +// +// StoryCollectionViewCell.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/16. +// + +import UIKit + +class StoryCollectionViewCell: UICollectionViewCell { + + @IBOutlet weak var viewImageViewBackground: UIView! + @IBOutlet weak var viewUserProfileBackground: UIView! + @IBOutlet weak var imageViewUserProfile: UIImageView! + + override func awakeFromNib() { + super.awakeFromNib() + + viewImageViewBackground.layer.cornerRadius = 24 + viewUserProfileBackground.layer.cornerRadius = 23.5 + imageViewUserProfile.layer.cornerRadius = 22.5 + imageViewUserProfile.clipsToBounds = true + // Initialization code + } + +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryCollectionViewCell.xib b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryCollectionViewCell.xib new file mode 100644 index 0000000..52cc4cb --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryCollectionViewCell.xib @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryTableViewCell.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryTableViewCell.swift new file mode 100644 index 0000000..2cd99ea --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryTableViewCell.swift @@ -0,0 +1,42 @@ +// +// StoryTableViewCell.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/16. +// + +import UIKit + +class StoryTableViewCell: UITableViewCell { + + @IBOutlet weak var collectionView: UICollectionView! + + func setCollectionViewDataSourceDelegate(dataSourceDelegate : UICollectionViewDelegate & UICollectionViewDataSource, forRow row: Int){ + collectionView.delegate = dataSourceDelegate + collectionView.dataSource = dataSourceDelegate + collectionView.tag = row + let storyNib = UINib(nibName: "StoryCollectionViewCell", bundle: nil) + collectionView.register(storyNib, forCellWithReuseIdentifier: "StoryCollectionViewCell") + + let flowLayout = UICollectionViewFlowLayout() + flowLayout.scrollDirection = .horizontal + flowLayout.sectionInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) + flowLayout.minimumLineSpacing = 12 + + collectionView.collectionViewLayout = flowLayout + + collectionView.reloadData() + } + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryTableViewCell.xib b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryTableViewCell.xib new file mode 100644 index 0000000..561c918 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/Cell/StoryTableViewCell.xib @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/HomeViewController.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/HomeViewController.swift new file mode 100644 index 0000000..02c344b --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Home/HomeViewController.swift @@ -0,0 +1,81 @@ +// +// HomeViewController.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/16. +// + +import UIKit + +class HomeViewController: UIViewController { + + @IBOutlet weak var tableView: UITableView! + override func viewDidLoad() { + super.viewDidLoad() + + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + let feedNib = UINib(nibName: "FeedTableViewCell", bundle: nil) + tableView.register(feedNib, forCellReuseIdentifier: "FeedTableViewCell") + let storyNib = UINib(nibName: "StoryTableViewCell", bundle: nil) + tableView.register(storyNib, forCellReuseIdentifier: "StoryTableViewCell") + } + + +} + +extension HomeViewController : UITableViewDelegate, UITableViewDataSource{ + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 10 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if indexPath.row == 0{ + guard let cell = tableView.dequeueReusableCell(withIdentifier: "StoryTableViewCell", for: indexPath) as? StoryTableViewCell else{ + return UITableViewCell() + } + return cell + } else{ + guard let cell = tableView.dequeueReusableCell(withIdentifier: "FeedTableViewCell", for: indexPath) as? FeedTableViewCell else{ + return UITableViewCell() + } + cell.selectionStyle = .none + return cell + } + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + if indexPath.row == 0{ + return 80 + }else{ + return 600 + } + } + + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard let tableViewCell = cell as? StoryTableViewCell else{ + return + } + + tableViewCell.setCollectionViewDataSourceDelegate(dataSourceDelegate: self, forRow: indexPath.row) + } +} + +extension HomeViewController : UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{ + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return 10 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "StoryCollectionViewCell", for: indexPath) as? StoryCollectionViewCell else{ + return UICollectionViewCell() + } + return cell + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: 50, height: 50) + } +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Info.plist b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Info.plist new file mode 100644 index 0000000..dd3c9af --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/LoginViewController.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/LoginViewController.swift new file mode 100644 index 0000000..41e5119 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/LoginViewController.swift @@ -0,0 +1,82 @@ +// +// LoginViewController.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/02. +// + +import UIKit + +class LoginViewController: UIViewController { + + var email = String() + var password = String() + var userInfo: UserInfo? + + @IBOutlet weak var loginBtn: UIButton! + + @IBOutlet weak var orLine1: UIView! + + override func viewDidLoad() { + super.viewDidLoad() + + loginBtn.layer.shadowColor = UIColor.black.cgColor + loginBtn.layer.masksToBounds = false + loginBtn.layer.shadowOffset = CGSize(width: 0, height: 4) + loginBtn.layer.shadowRadius = 5 + loginBtn.layer.shadowOpacity = 0.3 + } + + + @IBAction func emailTextFieldEditingChanged(_ sender: UITextField) { + let text = sender.text ?? "" //값이 없는 경우 공백을 넣겠다 + self.loginBtn.backgroundColor = text.isValidEmail() ? #colorLiteral(red: 0.2551737428, green: 0.5762303472, blue: 0.9353198409, alpha: 1) : #colorLiteral(red: 0.778111279, green: 0.8696789742, blue: 0.9796730876, alpha: 1) + self.email = text + } + + @IBAction func pwTextFieldEditingChanged(_ sender: UITextField) { + let text = sender.text ?? "" + + self.loginBtn.backgroundColor = text.count > 2 ? #colorLiteral(red: 0.2551737428, green: 0.5762303472, blue: 0.9353198409, alpha: 1) : #colorLiteral(red: 0.778111279, green: 0.8696789742, blue: 0.9796730876, alpha: 1) + + self.password = text + } + + + @IBAction func loginBtnTapped(_ sender: UIButton) { + //회원가입 정보 전달받아서, 그것과 textField 데이터 일치하면 로그인 가능 + guard let userInfo = self.userInfo else { + return + } + if userInfo.email == self.email && userInfo.password == self.password{ + let vc = storyboard?.instantiateViewController(withIdentifier: "TabBarVC") as! UITabBarController + vc.modalPresentationStyle = .fullScreen + self.present(vc, animated: true, completion: nil) + } + else{ + + } + } + + + + @IBAction func registerBtnTapped(_ sender: UIButton) { + //화면 전환 + //1. 스토리보드 생성 + let storyboard = UIStoryboard(name: "Main", bundle: nil) + + //2. 뷰컨트롤러 생성 + let registerViewController = storyboard.instantiateViewController(withIdentifier: "RegisterVC") as! RegisterViewController + + //3. 화면전환 메소드 + //self.present(registerViewController, animated: true, completion: nil) + self.navigationController?.pushViewController(registerViewController, animated: true) + + // ARC -> 강한참조 / 약한참조 -> ARC 낮춰줌 + registerViewController.userInfo = {(userInfo) in + self.userInfo = userInfo + } + + } + +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/PostCell/PostCollectionViewCell.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/PostCell/PostCollectionViewCell.swift new file mode 100644 index 0000000..3c2dd00 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/PostCell/PostCollectionViewCell.swift @@ -0,0 +1,22 @@ +// +// PostCollectionViewCell.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/01. +// + +import UIKit + +class PostCollectionViewCell: UICollectionViewCell { + static let identifier = "PostCollectionViewCell" + + @IBOutlet weak var postImageView: UIImageView! + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + public func setupData(){ + //이미지 뷰의 이미지를 업로드한다. + } +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/PostCell/PostCollectionViewCell.xib b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/PostCell/PostCollectionViewCell.xib new file mode 100644 index 0000000..d06724c --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/PostCell/PostCollectionViewCell.xib @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/ProfileCell/ProfileCollectionViewCell.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/ProfileCell/ProfileCollectionViewCell.swift new file mode 100644 index 0000000..3b6b2da --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/ProfileCell/ProfileCollectionViewCell.swift @@ -0,0 +1,45 @@ +// +// ProfileCollectionViewCell.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/30. +// + +import UIKit + +class ProfileCollectionViewCell: UICollectionViewCell { + static let identifier = "ProfileCollectionViewCell" + + @IBOutlet weak var profileImageView: UIImageView! + @IBOutlet weak var addProfileImageView: UIImageView! + + @IBOutlet weak var editButton: UIButton! + @IBOutlet weak var addFriendButton: UIButton! + + @IBOutlet weak var postingCountLabel: UILabel! + @IBOutlet weak var followerCountLabel: UILabel! + @IBOutlet weak var followingCountLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + setupAttribute() + } + + private func setupAttribute(){ + profileImageView.layer.cornerRadius = 44 + addProfileImageView.layer.cornerRadius = 12 + + profileImageView.layer.borderColor = UIColor.darkGray.cgColor + profileImageView.layer.borderWidth = 1 + + editButton.layer.cornerRadius = 5 + editButton.layer.borderColor = UIColor.lightGray.cgColor + editButton.layer.borderWidth = 1 + + addFriendButton.layer.cornerRadius = 3 + addFriendButton.layer.borderColor = UIColor.lightGray.cgColor + addFriendButton.layer.borderWidth = 1 + + [postingCountLabel, followerCountLabel, followingCountLabel].forEach{$0.text="\(Int.random(in: 0...10))"} + } +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/ProfileCell/ProfileCollectionViewCell.xib b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/ProfileCell/ProfileCollectionViewCell.xib new file mode 100644 index 0000000..9bacfc4 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/Cell/ProfileCell/ProfileCollectionViewCell.xib @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/ProfileViewController.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/ProfileViewController.swift new file mode 100644 index 0000000..c14c127 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/Profile/ProfileViewController.swift @@ -0,0 +1,98 @@ +// +// ProfileViewController.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/30. +// + +import UIKit + +class ProfileViewController: UIViewController { + //MARK: - Properties + @IBOutlet weak var profileCollectionView: UICollectionView! + + //MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + setupCollectionView() + // Do any additional setup after loading the view. + } + + //MARK: - Actions + + //MARK: - Helpers + private func setupCollectionView(){ + profileCollectionView.delegate = self + profileCollectionView.dataSource = self + + profileCollectionView.register(UINib(nibName: "ProfileCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: ProfileCollectionViewCell.identifier) + + profileCollectionView.register(UINib(nibName: "PostCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: PostCollectionViewCell.identifier) + } +} + +//MARK: - UICollectionViewDelegate, UICollectionViewDataSource +extension ProfileViewController: UICollectionViewDelegate, UICollectionViewDataSource{ + + func numberOfSections(in collectionView: UICollectionView) -> Int { + return 2 + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + switch section{ + case 0: + return 1 + default: + return 24 + } + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + + let section = indexPath.section + switch section{ + case 0: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ProfileCollectionViewCell.identifier, for: indexPath) as? ProfileCollectionViewCell else{ + fatalError("셀 타입 캐스팅 실패") + } + return cell + default: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PostCollectionViewCell.identifier, for: indexPath) as? PostCollectionViewCell else{ + fatalError("셀 타입 캐스팅 실패") + } + return cell + } + + } +} + +extension ProfileViewController: UICollectionViewDelegateFlowLayout{ + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + + let section = indexPath.section + switch section{ + case 0: + return CGSize(width: collectionView.frame.width, height: CGFloat(159)) + default: + let side = CGFloat((collectionView.frame.width / 3) - (4/3)) + return CGSize(width: side, height: side) + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + switch section{ + case 0: + return CGFloat(0) + default: + return CGFloat(1) + } + } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + switch section{ + case 0: + return CGFloat(0) + default: + return CGFloat(1) + } + } +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/RegisterViewController.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/RegisterViewController.swift new file mode 100644 index 0000000..43ccca8 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/RegisterViewController.swift @@ -0,0 +1,140 @@ +// +// RegisterViewController.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/02. +// + +import UIKit + +class RegisterViewController: UIViewController { + // MARK: - Properties + var email: String = "" + var name: String = "" + var nickname: String = "" + var password: String = "" + + var userInfo: ((UserInfo) -> Void)? + + var isValidEmail = false { + didSet{ + self.validateUserInfo() + } + } + + var isValidName = false { + didSet{ + self.validateUserInfo() + } + } + + var isValidNickname = false { + didSet{ + self.validateUserInfo() + } + } + + var isValidPassword = false { + didSet{ + self.validateUserInfo() + } + } + + @IBOutlet weak var signupBtn: UIButton! + //Textfields + @IBOutlet weak var emailTextField: UITextField! + @IBOutlet weak var nameTextField: UITextField! + @IBOutlet weak var nicknameTextField: UITextField! + @IBOutlet weak var pwTextField: UITextField! + + var textFields: [UITextField]{ + [emailTextField, nameTextField, nicknameTextField, pwTextField] + } + + //MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + setUpTextField() + + self.navigationController?.interactivePopGestureRecognizer?.delegate = nil + } + + //MARK: - Actions + @objc + func textFieldEditingChanged(_ sender: UITextField){ + let text = sender.text ?? "" + + switch sender{ + case emailTextField: + self.isValidEmail = text.isValidEmail() + self.email = text + case nameTextField: + self.isValidName = text.count > 2 + self.name = text + case nicknameTextField: + self.isValidNickname = text.count > 2 + self.nickname = text + case pwTextField: + self.isValidPassword = text.isValidPassword() + self.password = text + default: + fatalError("Missing TF") + } + } + + @IBAction func backBtnTapped(_ sender: UIBarButtonItem) { + //뒤로가기 + self.navigationController?.popViewController(animated: true) + } + + + @IBAction func registerBtnTapped(_ sender: UIButton) { + //뒤로가기 + self.navigationController?.popViewController(animated: true) + + let userInfo = UserInfo(email: self.email, name: self.name, nickname: self.nickname, password: self.password) + + self.userInfo?(userInfo) + } + + //MARK: - Helpers + private func setUpTextField(){ + textFields.forEach{tf in + tf.addTarget(self, action: #selector(textFieldEditingChanged(_:)), for: .editingChanged) + } + } + + //사용자가 입력한 회원정보를 확인하고 ui 업데이트 + private func validateUserInfo(){ + + if isValidName && isValidEmail && isValidNickname && isValidPassword{ + UIView.animate(withDuration: 0.33) { + self.signupBtn.isEnabled = true + self.signupBtn.backgroundColor = #colorLiteral(red: 0.2551737428, green: 0.5762303472, blue: 0.9353198409, alpha: 1) + } + + }else{ + UIView.animate(withDuration: 0.33){ + self.signupBtn.isEnabled = false + self.signupBtn.backgroundColor = #colorLiteral(red: 0.778111279, green: 0.8696789742, blue: 0.9796730876, alpha: 1) + } + } + } + +} + +//정규표현식 +extension String{ + func isValidPassword() -> Bool { + let regularExpression = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[$@$!%*?&])[A-Za-z\\d$@$!%*?&]{8,}" + let passwordValidation = NSPredicate.init(format: "SELF MATCHES %@", regularExpression) + + return passwordValidation.evaluate(with: self) + } + + func isValidEmail() -> Bool { + let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx) + return emailTest.evaluate(with: self) + } +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/SceneDelegate.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/SceneDelegate.swift new file mode 100644 index 0000000..4ed9336 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/02. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/UIViewController+Extension.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/UIViewController+Extension.swift new file mode 100644 index 0000000..8ee341a --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/UIViewController+Extension.swift @@ -0,0 +1,21 @@ +// +// UIViewController+Extension.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/02. +// + +import UIKit + +extension UIViewController{ + /* + func generateButtonAttribute(_ button: UIButton, texts: String..., fonts: UIFont..., colors: UIColor...) -> NSMutableAttributedString{ + guard let wholeText = button.titleLabel?.text else{ + fatalError("버튼에 텍스트가 없음.") + } + + let customFonts: [UIFont] = fonts + + } + */ +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/UIViewExtension.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/UIViewExtension.swift new file mode 100644 index 0000000..0b6df86 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/UIViewExtension.swift @@ -0,0 +1,20 @@ +// +// UIViewExtension.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/02. +// + +import UIKit + +extension UIView{ + @IBInspectable var cornerRadius: CGFloat { + get{ + return layer.cornerRadius + } + set{ + layer.cornerRadius = newValue + layer.masksToBounds = newValue > 0 + } + } +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGram/UserInfo.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/UserInfo.swift new file mode 100644 index 0000000..a619d25 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGram/UserInfo.swift @@ -0,0 +1,15 @@ +// +// UserInfo.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/02. +// + +import Foundation + +struct UserInfo{ + let email: String + let name: String + let nickname: String + let password: String +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGramTests/CatStaGramTests.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGramTests/CatStaGramTests.swift new file mode 100644 index 0000000..7a2362d --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGramTests/CatStaGramTests.swift @@ -0,0 +1,36 @@ +// +// CatStaGramTests.swift +// CatStaGramTests +// +// Created by 송재민 on 2022/04/02. +// + +import XCTest +@testable import CatStaGram + +class CatStaGramTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGramUITests/CatStaGramUITests.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGramUITests/CatStaGramUITests.swift new file mode 100644 index 0000000..6cc1585 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGramUITests/CatStaGramUITests.swift @@ -0,0 +1,42 @@ +// +// CatStaGramUITests.swift +// CatStaGramUITests +// +// Created by 송재민 on 2022/04/02. +// + +import XCTest + +class CatStaGramUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/jaem/week5/CatStaGram/CatStaGram/CatStaGramUITests/CatStaGramUITestsLaunchTests.swift b/jaem/week5/CatStaGram/CatStaGram/CatStaGramUITests/CatStaGramUITestsLaunchTests.swift new file mode 100644 index 0000000..5738e01 --- /dev/null +++ b/jaem/week5/CatStaGram/CatStaGram/CatStaGramUITests/CatStaGramUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// CatStaGramUITestsLaunchTests.swift +// CatStaGramUITests +// +// Created by 송재민 on 2022/04/02. +// + +import XCTest + +class CatStaGramUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/jaem/week6/NewsApp/NewsApp.xcodeproj/project.pbxproj b/jaem/week6/NewsApp/NewsApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000..ac46164 --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp.xcodeproj/project.pbxproj @@ -0,0 +1,621 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + BC0CD4432822C17C001DF83F /* NewsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0CD4412822C17C001DF83F /* NewsTableViewCell.swift */; }; + BC0CD4442822C17C001DF83F /* NewsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC0CD4422822C17C001DF83F /* NewsTableViewCell.xib */; }; + BC0CD4482822EA5C001DF83F /* DetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0CD4472822EA5C001DF83F /* DetailVC.swift */; }; + BC2A551128223D5C008D3505 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2A551028223D5C008D3505 /* AppDelegate.swift */; }; + BC2A551328223D5C008D3505 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2A551228223D5C008D3505 /* SceneDelegate.swift */; }; + BC2A551528223D5C008D3505 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2A551428223D5C008D3505 /* ViewController.swift */; }; + BC2A551828223D5C008D3505 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BC2A551628223D5C008D3505 /* Main.storyboard */; }; + BC2A551A28223D5C008D3505 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BC2A551928223D5C008D3505 /* Assets.xcassets */; }; + BC2A551D28223D5C008D3505 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BC2A551B28223D5C008D3505 /* LaunchScreen.storyboard */; }; + BC2A552828223D5C008D3505 /* NewsAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2A552728223D5C008D3505 /* NewsAppTests.swift */; }; + BC2A553228223D5C008D3505 /* NewsAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2A553128223D5C008D3505 /* NewsAppUITests.swift */; }; + BC2A553428223D5C008D3505 /* NewsAppUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2A553328223D5C008D3505 /* NewsAppUITestsLaunchTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + BC2A552428223D5C008D3505 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BC2A550528223D5C008D3505 /* Project object */; + proxyType = 1; + remoteGlobalIDString = BC2A550C28223D5C008D3505; + remoteInfo = NewsApp; + }; + BC2A552E28223D5C008D3505 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BC2A550528223D5C008D3505 /* Project object */; + proxyType = 1; + remoteGlobalIDString = BC2A550C28223D5C008D3505; + remoteInfo = NewsApp; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + BC0CD4412822C17C001DF83F /* NewsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsTableViewCell.swift; sourceTree = ""; }; + BC0CD4422822C17C001DF83F /* NewsTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NewsTableViewCell.xib; sourceTree = ""; }; + BC0CD4472822EA5C001DF83F /* DetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailVC.swift; sourceTree = ""; }; + BC2A550D28223D5C008D3505 /* NewsApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NewsApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + BC2A551028223D5C008D3505 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + BC2A551228223D5C008D3505 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + BC2A551428223D5C008D3505 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + BC2A551728223D5C008D3505 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + BC2A551928223D5C008D3505 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + BC2A551C28223D5C008D3505 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + BC2A551E28223D5C008D3505 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BC2A552328223D5C008D3505 /* NewsAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NewsAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BC2A552728223D5C008D3505 /* NewsAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsAppTests.swift; sourceTree = ""; }; + BC2A552D28223D5C008D3505 /* NewsAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NewsAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BC2A553128223D5C008D3505 /* NewsAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsAppUITests.swift; sourceTree = ""; }; + BC2A553328223D5C008D3505 /* NewsAppUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsAppUITestsLaunchTests.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + BC2A550A28223D5C008D3505 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC2A552028223D5C008D3505 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC2A552A28223D5C008D3505 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + BC2A550428223D5C008D3505 = { + isa = PBXGroup; + children = ( + BC2A550F28223D5C008D3505 /* NewsApp */, + BC2A552628223D5C008D3505 /* NewsAppTests */, + BC2A553028223D5C008D3505 /* NewsAppUITests */, + BC2A550E28223D5C008D3505 /* Products */, + ); + sourceTree = ""; + }; + BC2A550E28223D5C008D3505 /* Products */ = { + isa = PBXGroup; + children = ( + BC2A550D28223D5C008D3505 /* NewsApp.app */, + BC2A552328223D5C008D3505 /* NewsAppTests.xctest */, + BC2A552D28223D5C008D3505 /* NewsAppUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + BC2A550F28223D5C008D3505 /* NewsApp */ = { + isa = PBXGroup; + children = ( + BC2A551028223D5C008D3505 /* AppDelegate.swift */, + BC2A551228223D5C008D3505 /* SceneDelegate.swift */, + BC2A551428223D5C008D3505 /* ViewController.swift */, + BC2A551628223D5C008D3505 /* Main.storyboard */, + BC2A551928223D5C008D3505 /* Assets.xcassets */, + BC2A551B28223D5C008D3505 /* LaunchScreen.storyboard */, + BC2A551E28223D5C008D3505 /* Info.plist */, + BC0CD4412822C17C001DF83F /* NewsTableViewCell.swift */, + BC0CD4422822C17C001DF83F /* NewsTableViewCell.xib */, + BC0CD4472822EA5C001DF83F /* DetailVC.swift */, + ); + path = NewsApp; + sourceTree = ""; + }; + BC2A552628223D5C008D3505 /* NewsAppTests */ = { + isa = PBXGroup; + children = ( + BC2A552728223D5C008D3505 /* NewsAppTests.swift */, + ); + path = NewsAppTests; + sourceTree = ""; + }; + BC2A553028223D5C008D3505 /* NewsAppUITests */ = { + isa = PBXGroup; + children = ( + BC2A553128223D5C008D3505 /* NewsAppUITests.swift */, + BC2A553328223D5C008D3505 /* NewsAppUITestsLaunchTests.swift */, + ); + path = NewsAppUITests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + BC2A550C28223D5C008D3505 /* NewsApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = BC2A553728223D5C008D3505 /* Build configuration list for PBXNativeTarget "NewsApp" */; + buildPhases = ( + BC2A550928223D5C008D3505 /* Sources */, + BC2A550A28223D5C008D3505 /* Frameworks */, + BC2A550B28223D5C008D3505 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NewsApp; + productName = NewsApp; + productReference = BC2A550D28223D5C008D3505 /* NewsApp.app */; + productType = "com.apple.product-type.application"; + }; + BC2A552228223D5C008D3505 /* NewsAppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = BC2A553A28223D5C008D3505 /* Build configuration list for PBXNativeTarget "NewsAppTests" */; + buildPhases = ( + BC2A551F28223D5C008D3505 /* Sources */, + BC2A552028223D5C008D3505 /* Frameworks */, + BC2A552128223D5C008D3505 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BC2A552528223D5C008D3505 /* PBXTargetDependency */, + ); + name = NewsAppTests; + productName = NewsAppTests; + productReference = BC2A552328223D5C008D3505 /* NewsAppTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + BC2A552C28223D5C008D3505 /* NewsAppUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = BC2A553D28223D5C008D3505 /* Build configuration list for PBXNativeTarget "NewsAppUITests" */; + buildPhases = ( + BC2A552928223D5C008D3505 /* Sources */, + BC2A552A28223D5C008D3505 /* Frameworks */, + BC2A552B28223D5C008D3505 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BC2A552F28223D5C008D3505 /* PBXTargetDependency */, + ); + name = NewsAppUITests; + productName = NewsAppUITests; + productReference = BC2A552D28223D5C008D3505 /* NewsAppUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BC2A550528223D5C008D3505 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1320; + LastUpgradeCheck = 1320; + TargetAttributes = { + BC2A550C28223D5C008D3505 = { + CreatedOnToolsVersion = 13.2.1; + }; + BC2A552228223D5C008D3505 = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = BC2A550C28223D5C008D3505; + }; + BC2A552C28223D5C008D3505 = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = BC2A550C28223D5C008D3505; + }; + }; + }; + buildConfigurationList = BC2A550828223D5C008D3505 /* Build configuration list for PBXProject "NewsApp" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = BC2A550428223D5C008D3505; + productRefGroup = BC2A550E28223D5C008D3505 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + BC2A550C28223D5C008D3505 /* NewsApp */, + BC2A552228223D5C008D3505 /* NewsAppTests */, + BC2A552C28223D5C008D3505 /* NewsAppUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + BC2A550B28223D5C008D3505 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC2A551D28223D5C008D3505 /* LaunchScreen.storyboard in Resources */, + BC2A551A28223D5C008D3505 /* Assets.xcassets in Resources */, + BC0CD4442822C17C001DF83F /* NewsTableViewCell.xib in Resources */, + BC2A551828223D5C008D3505 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC2A552128223D5C008D3505 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC2A552B28223D5C008D3505 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + BC2A550928223D5C008D3505 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC0CD4482822EA5C001DF83F /* DetailVC.swift in Sources */, + BC2A551528223D5C008D3505 /* ViewController.swift in Sources */, + BC2A551128223D5C008D3505 /* AppDelegate.swift in Sources */, + BC0CD4432822C17C001DF83F /* NewsTableViewCell.swift in Sources */, + BC2A551328223D5C008D3505 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC2A551F28223D5C008D3505 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC2A552828223D5C008D3505 /* NewsAppTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC2A552928223D5C008D3505 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BC2A553228223D5C008D3505 /* NewsAppUITests.swift in Sources */, + BC2A553428223D5C008D3505 /* NewsAppUITestsLaunchTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + BC2A552528223D5C008D3505 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BC2A550C28223D5C008D3505 /* NewsApp */; + targetProxy = BC2A552428223D5C008D3505 /* PBXContainerItemProxy */; + }; + BC2A552F28223D5C008D3505 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BC2A550C28223D5C008D3505 /* NewsApp */; + targetProxy = BC2A552E28223D5C008D3505 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + BC2A551628223D5C008D3505 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BC2A551728223D5C008D3505 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + BC2A551B28223D5C008D3505 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BC2A551C28223D5C008D3505 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + BC2A553528223D5C008D3505 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + BC2A553628223D5C008D3505 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + BC2A553828223D5C008D3505 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NewsApp/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.NewsApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + BC2A553928223D5C008D3505 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NewsApp/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.NewsApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + BC2A553B28223D5C008D3505 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.NewsAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NewsApp.app/NewsApp"; + }; + name = Debug; + }; + BC2A553C28223D5C008D3505 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.NewsAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NewsApp.app/NewsApp"; + }; + name = Release; + }; + BC2A553E28223D5C008D3505 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.NewsAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = NewsApp; + }; + name = Debug; + }; + BC2A553F28223D5C008D3505 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = P7RWD3A283; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = jaem.NewsAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = NewsApp; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + BC2A550828223D5C008D3505 /* Build configuration list for PBXProject "NewsApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC2A553528223D5C008D3505 /* Debug */, + BC2A553628223D5C008D3505 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BC2A553728223D5C008D3505 /* Build configuration list for PBXNativeTarget "NewsApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC2A553828223D5C008D3505 /* Debug */, + BC2A553928223D5C008D3505 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BC2A553A28223D5C008D3505 /* Build configuration list for PBXNativeTarget "NewsAppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC2A553B28223D5C008D3505 /* Debug */, + BC2A553C28223D5C008D3505 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BC2A553D28223D5C008D3505 /* Build configuration list for PBXNativeTarget "NewsAppUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC2A553E28223D5C008D3505 /* Debug */, + BC2A553F28223D5C008D3505 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BC2A550528223D5C008D3505 /* Project object */; +} diff --git a/jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..692d9438f9737cc4eeb0a094bd0c7a79a5994b0d GIT binary patch literal 40344 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 E3#LGR^8f$< literal 0 HcmV?d00001 diff --git a/jaem/week6/NewsApp/NewsApp.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist b/jaem/week6/NewsApp/NewsApp.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..dc15d23 --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + NewsApp.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/jaem/week6/NewsApp/NewsApp/AppDelegate.swift b/jaem/week6/NewsApp/NewsApp/AppDelegate.swift new file mode 100644 index 0000000..b9ebb41 --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// NewsApp +// +// Created by 송재민 on 2022/05/04. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/jaem/week6/NewsApp/NewsApp/Assets.xcassets/AccentColor.colorset/Contents.json b/jaem/week6/NewsApp/NewsApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week6/NewsApp/NewsApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/jaem/week6/NewsApp/NewsApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week6/NewsApp/NewsApp/Assets.xcassets/Contents.json b/jaem/week6/NewsApp/NewsApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/jaem/week6/NewsApp/NewsApp/Base.lproj/LaunchScreen.storyboard b/jaem/week6/NewsApp/NewsApp/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week6/NewsApp/NewsApp/Base.lproj/Main.storyboard b/jaem/week6/NewsApp/NewsApp/Base.lproj/Main.storyboard new file mode 100644 index 0000000..34de09e --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/Base.lproj/Main.storyboard @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week6/NewsApp/NewsApp/DetailVC.swift b/jaem/week6/NewsApp/NewsApp/DetailVC.swift new file mode 100644 index 0000000..4858e9b --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/DetailVC.swift @@ -0,0 +1,36 @@ +// +// DetailVC.swift +// NewsApp +// +// Created by 송재민 on 2022/05/05. +// + +import UIKit + +class DetailVC : UIViewController{ + var newsTitle: String = "" + var newsContent: String = "" + var imageUrl: String = "" + + @IBOutlet weak var titleLbl: UILabel! + @IBOutlet weak var newsImg: UIImageView! + @IBOutlet weak var contentLbl: UILabel! + + + override func viewDidLoad() { + super.viewDidLoad() + self.navigationItem.title = "Detail" + + titleLbl.text = newsTitle + contentLbl.text = newsContent + let url = URL(string: imageUrl) + DispatchQueue.global().async { + if let data = try? Data(contentsOf: url!){ + DispatchQueue.main.async { + self.newsImg.image = UIImage(data: data) + } + } + + } + } +} diff --git a/jaem/week6/NewsApp/NewsApp/Info.plist b/jaem/week6/NewsApp/NewsApp/Info.plist new file mode 100644 index 0000000..dd3c9af --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.swift b/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.swift new file mode 100644 index 0000000..d482663 --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.swift @@ -0,0 +1,28 @@ +// +// NewsTableViewCell.swift +// NewsApp +// +// Created by 송재민 on 2022/05/04. +// + +import UIKit + +class NewsTableViewCell: UITableViewCell { + + + @IBOutlet weak var titleLbl: UILabel! + @IBOutlet weak var contentLbl: UILabel! + @IBOutlet weak var newsImg: UIImageView! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + +} diff --git a/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.xib b/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.xib new file mode 100644 index 0000000..ba6b4bc --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.xib @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week6/NewsApp/NewsApp/SceneDelegate.swift b/jaem/week6/NewsApp/NewsApp/SceneDelegate.swift new file mode 100644 index 0000000..fcfd68f --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// NewsApp +// +// Created by 송재민 on 2022/05/04. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/jaem/week6/NewsApp/NewsApp/ViewController.swift b/jaem/week6/NewsApp/NewsApp/ViewController.swift new file mode 100644 index 0000000..3197901 --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/ViewController.swift @@ -0,0 +1,124 @@ +// +// ViewController.swift +// NewsApp +// +// Created by 송재민 on 2022/05/04. +// + +import UIKit + +class ViewController: UIViewController { + + @IBOutlet var tableView: UITableView! + var newsData :Array>? + + /*뉴스 데이터 가져오기*/ + func getNews(){ + let task = URLSession.shared.dataTask(with: URL(string: "https://newsapi.org/v2/top-headlines?country=kr&apiKey=f2a79448bc5142e192ada2c486b2f162")!) { (data, response, error) in + if let dataJson = data{ + do{ + let json = try JSONSerialization.jsonObject(with: dataJson, options: []) as! Dictionary + let articles = json["articles"] as! Array> + self.newsData = articles + + DispatchQueue.main.async { + self.tableView.reloadData() + } + + } + catch{ + + } + } + } + + task.resume() + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + + tableView.delegate = self + tableView.dataSource = self + let newsNib = UINib(nibName: "NewsTableViewCell", bundle: nil) + tableView.register(newsNib, forCellReuseIdentifier: "NewsTableViewCell") + + self.navigationItem.title = "News" + + getNews() + } + +} + +extension ViewController : UITableViewDelegate, UITableViewDataSource{ + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if let news = newsData{ + return news.count + }else{ + return 0 + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + + let storyboard = UIStoryboard.init(name: "Main", bundle: nil) + let newsDetail = storyboard.instantiateViewController(withIdentifier: "DetailVC") as! DetailVC + + if let news = newsData{ + let row = news[indexPath.row] + if let r = row as? Dictionary{ + if let title = r["title"] as? String{ + newsDetail.newsTitle = title + } + if let content = r["description"] as? String{ + newsDetail.newsContent = content + } + if let imageUrl = r["urlToImage"] as? String{ + newsDetail.imageUrl = imageUrl + } + } + } + + showDetailViewController(newsDetail, sender: nil) + } + + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "NewsTableViewCell", for: indexPath) as? NewsTableViewCell else{ + return UITableViewCell() + } + + let idx = indexPath.row + + if let news = newsData{ + + let row = news[idx] + if let r = row as? Dictionary{ + if let title = r["title"] as? String{ + cell.titleLbl.text = title + } + if let content = r["description"] as? String{ + cell.contentLbl.text = content + } + if let imageUrl = r["urlToImage"] as? String{ + let url = URL(string: imageUrl) + DispatchQueue.global().async { + if let data = try? Data(contentsOf: url!){ + DispatchQueue.main.async { + cell.newsImg.image = UIImage(data: data) + } + } + + } + } + } + } + return cell + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 176 + } +} diff --git a/jaem/week6/NewsApp/NewsAppTests/NewsAppTests.swift b/jaem/week6/NewsApp/NewsAppTests/NewsAppTests.swift new file mode 100644 index 0000000..a29286b --- /dev/null +++ b/jaem/week6/NewsApp/NewsAppTests/NewsAppTests.swift @@ -0,0 +1,36 @@ +// +// NewsAppTests.swift +// NewsAppTests +// +// Created by 송재민 on 2022/05/04. +// + +import XCTest +@testable import NewsApp + +class NewsAppTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/jaem/week6/NewsApp/NewsAppUITests/NewsAppUITests.swift b/jaem/week6/NewsApp/NewsAppUITests/NewsAppUITests.swift new file mode 100644 index 0000000..b1dc0ed --- /dev/null +++ b/jaem/week6/NewsApp/NewsAppUITests/NewsAppUITests.swift @@ -0,0 +1,42 @@ +// +// NewsAppUITests.swift +// NewsAppUITests +// +// Created by 송재민 on 2022/05/04. +// + +import XCTest + +class NewsAppUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/jaem/week6/NewsApp/NewsAppUITests/NewsAppUITestsLaunchTests.swift b/jaem/week6/NewsApp/NewsAppUITests/NewsAppUITestsLaunchTests.swift new file mode 100644 index 0000000..8ced369 --- /dev/null +++ b/jaem/week6/NewsApp/NewsAppUITests/NewsAppUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// NewsAppUITestsLaunchTests.swift +// NewsAppUITests +// +// Created by 송재민 on 2022/05/04. +// + +import XCTest + +class NewsAppUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} From b09440e901cb4d93f61b7cb6ac72c18b14058994 Mon Sep 17 00:00:00 2001 From: xongjaemin Date: Thu, 5 May 2022 03:40:39 +0900 Subject: [PATCH 2/9] dev: news app basic --- .../UserInterfaceState.xcuserstate | Bin 40344 -> 39816 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate index 692d9438f9737cc4eeb0a094bd0c7a79a5994b0d..dc34d4501f80435b115d2ae11c108a2e8919b6d1 100644 GIT binary patch delta 20979 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%0DP1 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 QhdrONMLnN=?{7u?Uz?9;SO5S3 delta 21909 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 Date: Thu, 5 May 2022 21:36:31 +0900 Subject: [PATCH 3/9] fix : detail page navbar --- .../UserInterfaceState.xcuserstate | Bin 39816 -> 41914 bytes .../NewsApp/NewsApp/NewsTableViewCell.xib | 8 ++++---- .../NewsApp/NewsApp/ViewController.swift | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week6/NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate index dc34d4501f80435b115d2ae11c108a2e8919b6d1..15b14dd8beb819a404241dc0cbd7a526a0d9b1cb 100644 GIT binary patch literal 41914 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 xXZ&S6HeM#{BvX$zj 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 Qm|xj?&HqLEugo6xACllg*#H0l diff --git a/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.xib b/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.xib index ba6b4bc..604e79a 100644 --- a/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.xib +++ b/jaem/week6/NewsApp/NewsApp/NewsTableViewCell.xib @@ -18,27 +18,27 @@ - + - + diff --git a/jaem/week6/NewsApp/NewsApp/ViewController.swift b/jaem/week6/NewsApp/NewsApp/ViewController.swift index 3197901..767738f 100644 --- a/jaem/week6/NewsApp/NewsApp/ViewController.swift +++ b/jaem/week6/NewsApp/NewsApp/ViewController.swift @@ -81,7 +81,8 @@ extension ViewController : UITableViewDelegate, UITableViewDataSource{ } } - showDetailViewController(newsDetail, sender: nil) + //showDetailViewController(newsDetail, sender: nil) + self.navigationController?.pushViewController(newsDetail, animated: true) } From f925a10cfe0b5c7a4df5efcd511dd3b4dbb65709 Mon Sep 17 00:00:00 2001 From: xongjaemin Date: Wed, 18 May 2022 04:46:20 +0900 Subject: [PATCH 4/9] =?UTF-8?q?dev:=20mvvm=20=ED=88=AC=EB=91=90=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A7=84=ED=96=89=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NewsApp/ArticleModel/Article.swift | 14 ++ .../NewsApp/ArticleModel/ArticleModel.swift | 46 +++++ .../NewsApp/ArticleModel/ArticleService.swift | 14 ++ .../CatStaGram/App/AppDelegate.swift | 36 ++++ .../CatStaGram/App/SceneDelegate.swift | 52 +++++ .../Authentication/LoginViewController.swift | 82 ++++++++ .../RegisterViewController.swift | 140 ++++++++++++++ .../Home/Cell/FeedTableViewCell.swift | 60 ++++++ .../Home/Cell/FeedTableViewCell.xib | 183 ++++++++++++++++++ .../Home/Cell/StoryCollectionViewCell.swift | 26 +++ .../Home/Cell/StoryCollectionViewCell.xib | 100 ++++++++++ .../Home/Cell/StoryTableViewCell.swift | 42 ++++ .../Home/Cell/StoryTableViewCell.xib | 44 +++++ .../Home/FeedDataManager/FeedAPIInput.swift | 11 ++ .../FeedDataManager/FeedDataManager.swift | 21 ++ .../Home/FeedDataManager/FeedModel.swift | 11 ++ .../FeedUploadDataManager.swift | 26 +++ .../FeedUploadInput.swift | 11 ++ .../FeedUploadModel.swift | 17 ++ .../Home/HomeViewController.swift | 119 ++++++++++++ .../PostCell/PostCollectionViewCell.swift | 22 +++ .../Cell/PostCell/PostCollectionViewCell.xib | 38 ++++ .../ProfileCollectionViewCell.swift | 45 +++++ .../ProfileCell/ProfileCollectionViewCell.xib | 156 +++++++++++++++ .../Profile/ProfileViewController.swift | 98 ++++++++++ .../DeleteUserFeed/DeleteUserFeed.swift | 8 + .../UserFeed/UserFeedDataManager.swift | 22 +++ .../UserFeed/UserFeedModel.swift | 40 ++++ .../UserFeedDataManager.swift | 8 + .../Todo_MVVM/Apps/AppDelegate.swift | 36 ++++ .../Todo_MVVM/Apps/SceneDelegate.swift | 52 +++++ .../Todo_MVVM/Cell/TodoTableViewCell.swift | 8 + .../Todo_MVVM/Todo_MVVM/Model/TodoModel.swift | 8 + .../Todo_MVVM/View/Base.lproj/Main.storyboard | 24 +++ .../ViewController/ViewController.swift | 19 ++ .../Todo_MVVM/ViewModel/TodoVM.swift | 8 + 36 files changed, 1647 insertions(+) create mode 100644 jaem/week6/NewsApp/NewsApp/ArticleModel/Article.swift create mode 100644 jaem/week6/NewsApp/NewsApp/ArticleModel/ArticleModel.swift create mode 100644 jaem/week6/NewsApp/NewsApp/ArticleModel/ArticleService.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/App/AppDelegate.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/App/SceneDelegate.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/LoginViewController.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/RegisterViewController.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/FeedTableViewCell.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/FeedTableViewCell.xib create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryCollectionViewCell.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryCollectionViewCell.xib create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryTableViewCell.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryTableViewCell.xib create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedDataManager/FeedAPIInput.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedDataManager/FeedDataManager.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedDataManager/FeedModel.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedUploadDataManager/FeedUploadDataManager.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedUploadDataManager/FeedUploadInput.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedUploadDataManager/FeedUploadModel.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/HomeViewController.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/PostCell/PostCollectionViewCell.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/PostCell/PostCollectionViewCell.xib create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/ProfileCell/ProfileCollectionViewCell.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/ProfileCell/ProfileCollectionViewCell.xib create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/ProfileViewController.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/DeleteUserFeed/DeleteUserFeed.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeed/UserFeedDataManager.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeed/UserFeedModel.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeedDataManager.swift create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM/Apps/AppDelegate.swift create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM/Apps/SceneDelegate.swift create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM/Cell/TodoTableViewCell.swift create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM/Model/TodoModel.swift create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM/View/Base.lproj/Main.storyboard create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM/ViewController/ViewController.swift create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM/ViewModel/TodoVM.swift diff --git a/jaem/week6/NewsApp/NewsApp/ArticleModel/Article.swift b/jaem/week6/NewsApp/NewsApp/ArticleModel/Article.swift new file mode 100644 index 0000000..58197d7 --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/ArticleModel/Article.swift @@ -0,0 +1,14 @@ +// +// News.swift +// NewsApp +// +// Created by 송재민 on 2022/05/12. +// + +import Foundation + +struct Article:Codable{ + let title: String? + let content: String? + let imgUrl: String? +} diff --git a/jaem/week6/NewsApp/NewsApp/ArticleModel/ArticleModel.swift b/jaem/week6/NewsApp/NewsApp/ArticleModel/ArticleModel.swift new file mode 100644 index 0000000..2b4ffda --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/ArticleModel/ArticleModel.swift @@ -0,0 +1,46 @@ +// +// NewsModel.swift +// NewsApp +// +// Created by 송재민 on 2022/05/12. +// + +import Foundation + +protocol ArticleModelProtocol { + func articleRetrieved(articles:[Article]) +} + +class ArticleModel{ + var delegate:ArticleModelProtocol? + + func getArticles(){ + let urlString = "https://newsapi.org/v2/top-headlines?country=kr&apiKey=f2a79448bc5142e192ada2c486b2f162" + let url = URL(string: urlString) + + guard url != nil else{ + print("Coudln't create url object") + return + } + let session = URLSession.shared + + let datatask = session.dataTask(with: url!){ + (data, response, error) in + if error == nil&&data != nil{ + let decoder = JSONDecoder() + + do{ + let articleService = try decoder.decode(ArticleService.self, from: data!) + + DispatchQueue.main.async { + self.delegate?.articleRetrieved(articles: articleService.articles!) + } + } + catch{ + print("Error parsing the json") + } + } + } + datatask.resume() + } +} diff --git a/jaem/week6/NewsApp/NewsApp/ArticleModel/ArticleService.swift b/jaem/week6/NewsApp/NewsApp/ArticleModel/ArticleService.swift new file mode 100644 index 0000000..c5ce06d --- /dev/null +++ b/jaem/week6/NewsApp/NewsApp/ArticleModel/ArticleService.swift @@ -0,0 +1,14 @@ +// +// NewsService.swift +// NewsApp +// +// Created by 송재민 on 2022/05/12. +// + +import Foundation + +struct ArticleService:Codable{ + var status:String? + var totalResults:Int? + var articles:[Article]? +} diff --git a/jaem/week7/CatStaGram/CatStaGram/App/AppDelegate.swift b/jaem/week7/CatStaGram/CatStaGram/App/AppDelegate.swift new file mode 100644 index 0000000..7bed9e0 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/App/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/02. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/jaem/week7/CatStaGram/CatStaGram/App/SceneDelegate.swift b/jaem/week7/CatStaGram/CatStaGram/App/SceneDelegate.swift new file mode 100644 index 0000000..4ed9336 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/App/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/02. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/LoginViewController.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/LoginViewController.swift new file mode 100644 index 0000000..41e5119 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/LoginViewController.swift @@ -0,0 +1,82 @@ +// +// LoginViewController.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/02. +// + +import UIKit + +class LoginViewController: UIViewController { + + var email = String() + var password = String() + var userInfo: UserInfo? + + @IBOutlet weak var loginBtn: UIButton! + + @IBOutlet weak var orLine1: UIView! + + override func viewDidLoad() { + super.viewDidLoad() + + loginBtn.layer.shadowColor = UIColor.black.cgColor + loginBtn.layer.masksToBounds = false + loginBtn.layer.shadowOffset = CGSize(width: 0, height: 4) + loginBtn.layer.shadowRadius = 5 + loginBtn.layer.shadowOpacity = 0.3 + } + + + @IBAction func emailTextFieldEditingChanged(_ sender: UITextField) { + let text = sender.text ?? "" //값이 없는 경우 공백을 넣겠다 + self.loginBtn.backgroundColor = text.isValidEmail() ? #colorLiteral(red: 0.2551737428, green: 0.5762303472, blue: 0.9353198409, alpha: 1) : #colorLiteral(red: 0.778111279, green: 0.8696789742, blue: 0.9796730876, alpha: 1) + self.email = text + } + + @IBAction func pwTextFieldEditingChanged(_ sender: UITextField) { + let text = sender.text ?? "" + + self.loginBtn.backgroundColor = text.count > 2 ? #colorLiteral(red: 0.2551737428, green: 0.5762303472, blue: 0.9353198409, alpha: 1) : #colorLiteral(red: 0.778111279, green: 0.8696789742, blue: 0.9796730876, alpha: 1) + + self.password = text + } + + + @IBAction func loginBtnTapped(_ sender: UIButton) { + //회원가입 정보 전달받아서, 그것과 textField 데이터 일치하면 로그인 가능 + guard let userInfo = self.userInfo else { + return + } + if userInfo.email == self.email && userInfo.password == self.password{ + let vc = storyboard?.instantiateViewController(withIdentifier: "TabBarVC") as! UITabBarController + vc.modalPresentationStyle = .fullScreen + self.present(vc, animated: true, completion: nil) + } + else{ + + } + } + + + + @IBAction func registerBtnTapped(_ sender: UIButton) { + //화면 전환 + //1. 스토리보드 생성 + let storyboard = UIStoryboard(name: "Main", bundle: nil) + + //2. 뷰컨트롤러 생성 + let registerViewController = storyboard.instantiateViewController(withIdentifier: "RegisterVC") as! RegisterViewController + + //3. 화면전환 메소드 + //self.present(registerViewController, animated: true, completion: nil) + self.navigationController?.pushViewController(registerViewController, animated: true) + + // ARC -> 강한참조 / 약한참조 -> ARC 낮춰줌 + registerViewController.userInfo = {(userInfo) in + self.userInfo = userInfo + } + + } + +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/RegisterViewController.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/RegisterViewController.swift new file mode 100644 index 0000000..43ccca8 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Authentication/RegisterViewController.swift @@ -0,0 +1,140 @@ +// +// RegisterViewController.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/02. +// + +import UIKit + +class RegisterViewController: UIViewController { + // MARK: - Properties + var email: String = "" + var name: String = "" + var nickname: String = "" + var password: String = "" + + var userInfo: ((UserInfo) -> Void)? + + var isValidEmail = false { + didSet{ + self.validateUserInfo() + } + } + + var isValidName = false { + didSet{ + self.validateUserInfo() + } + } + + var isValidNickname = false { + didSet{ + self.validateUserInfo() + } + } + + var isValidPassword = false { + didSet{ + self.validateUserInfo() + } + } + + @IBOutlet weak var signupBtn: UIButton! + //Textfields + @IBOutlet weak var emailTextField: UITextField! + @IBOutlet weak var nameTextField: UITextField! + @IBOutlet weak var nicknameTextField: UITextField! + @IBOutlet weak var pwTextField: UITextField! + + var textFields: [UITextField]{ + [emailTextField, nameTextField, nicknameTextField, pwTextField] + } + + //MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + setUpTextField() + + self.navigationController?.interactivePopGestureRecognizer?.delegate = nil + } + + //MARK: - Actions + @objc + func textFieldEditingChanged(_ sender: UITextField){ + let text = sender.text ?? "" + + switch sender{ + case emailTextField: + self.isValidEmail = text.isValidEmail() + self.email = text + case nameTextField: + self.isValidName = text.count > 2 + self.name = text + case nicknameTextField: + self.isValidNickname = text.count > 2 + self.nickname = text + case pwTextField: + self.isValidPassword = text.isValidPassword() + self.password = text + default: + fatalError("Missing TF") + } + } + + @IBAction func backBtnTapped(_ sender: UIBarButtonItem) { + //뒤로가기 + self.navigationController?.popViewController(animated: true) + } + + + @IBAction func registerBtnTapped(_ sender: UIButton) { + //뒤로가기 + self.navigationController?.popViewController(animated: true) + + let userInfo = UserInfo(email: self.email, name: self.name, nickname: self.nickname, password: self.password) + + self.userInfo?(userInfo) + } + + //MARK: - Helpers + private func setUpTextField(){ + textFields.forEach{tf in + tf.addTarget(self, action: #selector(textFieldEditingChanged(_:)), for: .editingChanged) + } + } + + //사용자가 입력한 회원정보를 확인하고 ui 업데이트 + private func validateUserInfo(){ + + if isValidName && isValidEmail && isValidNickname && isValidPassword{ + UIView.animate(withDuration: 0.33) { + self.signupBtn.isEnabled = true + self.signupBtn.backgroundColor = #colorLiteral(red: 0.2551737428, green: 0.5762303472, blue: 0.9353198409, alpha: 1) + } + + }else{ + UIView.animate(withDuration: 0.33){ + self.signupBtn.isEnabled = false + self.signupBtn.backgroundColor = #colorLiteral(red: 0.778111279, green: 0.8696789742, blue: 0.9796730876, alpha: 1) + } + } + } + +} + +//정규표현식 +extension String{ + func isValidPassword() -> Bool { + let regularExpression = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[$@$!%*?&])[A-Za-z\\d$@$!%*?&]{8,}" + let passwordValidation = NSPredicate.init(format: "SELF MATCHES %@", regularExpression) + + return passwordValidation.evaluate(with: self) + } + + func isValidEmail() -> Bool { + let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx) + return emailTest.evaluate(with: self) + } +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/FeedTableViewCell.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/FeedTableViewCell.swift new file mode 100644 index 0000000..d8766a6 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/FeedTableViewCell.swift @@ -0,0 +1,60 @@ +// +// FeedTableViewCell.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/16. +// + +import UIKit + +class FeedTableViewCell: UITableViewCell { + + @IBOutlet weak var imageViewUserProfile: UIImageView! + @IBOutlet weak var labelUserName: UILabel! + @IBOutlet weak var imageViewFeed: UIImageView! + + @IBOutlet weak var imageViewMyProfile: UIImageView! + @IBOutlet weak var labelFeed: UILabel! + @IBOutlet weak var labelHowManyLike: UILabel! + @IBOutlet weak var buttonIsBookMark: UIButton! + @IBOutlet weak var buttonIsHeart: UIButton! + + @IBAction func actionIsHeart(_ sender: Any) { + if buttonIsHeart.isSelected { + buttonIsHeart.isSelected = false + }else{ + buttonIsHeart.isSelected = true + } + } + + @IBAction func actionIsBookMark(_ sender: Any) { + if buttonIsBookMark.isSelected { + buttonIsBookMark.isSelected = false + }else{ + buttonIsBookMark.isSelected = true + } + } + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + imageViewUserProfile.layer.cornerRadius = 12.5 + imageViewUserProfile.clipsToBounds = true + + imageViewMyProfile.layer.cornerRadius = 12.5 + imageViewMyProfile.clipsToBounds = true + + let fontSize = UIFont.boldSystemFont(ofSize: 9) + let attributedStr = NSMutableAttributedString(string: labelFeed.text ?? "") + attributedStr.addAttribute(.font, value: fontSize, range: NSRange.init(location: 0, length: 8)) + + labelFeed.attributedText = attributedStr + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/FeedTableViewCell.xib b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/FeedTableViewCell.xib new file mode 100644 index 0000000..c7e8eda --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/FeedTableViewCell.xib @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryCollectionViewCell.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryCollectionViewCell.swift new file mode 100644 index 0000000..c86d742 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryCollectionViewCell.swift @@ -0,0 +1,26 @@ +// +// StoryCollectionViewCell.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/16. +// + +import UIKit + +class StoryCollectionViewCell: UICollectionViewCell { + + @IBOutlet weak var viewImageViewBackground: UIView! + @IBOutlet weak var viewUserProfileBackground: UIView! + @IBOutlet weak var imageViewUserProfile: UIImageView! + + override func awakeFromNib() { + super.awakeFromNib() + + viewImageViewBackground.layer.cornerRadius = 24 + viewUserProfileBackground.layer.cornerRadius = 23.5 + imageViewUserProfile.layer.cornerRadius = 22.5 + imageViewUserProfile.clipsToBounds = true + // Initialization code + } + +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryCollectionViewCell.xib b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryCollectionViewCell.xib new file mode 100644 index 0000000..52cc4cb --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryCollectionViewCell.xib @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryTableViewCell.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryTableViewCell.swift new file mode 100644 index 0000000..2cd99ea --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryTableViewCell.swift @@ -0,0 +1,42 @@ +// +// StoryTableViewCell.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/16. +// + +import UIKit + +class StoryTableViewCell: UITableViewCell { + + @IBOutlet weak var collectionView: UICollectionView! + + func setCollectionViewDataSourceDelegate(dataSourceDelegate : UICollectionViewDelegate & UICollectionViewDataSource, forRow row: Int){ + collectionView.delegate = dataSourceDelegate + collectionView.dataSource = dataSourceDelegate + collectionView.tag = row + let storyNib = UINib(nibName: "StoryCollectionViewCell", bundle: nil) + collectionView.register(storyNib, forCellWithReuseIdentifier: "StoryCollectionViewCell") + + let flowLayout = UICollectionViewFlowLayout() + flowLayout.scrollDirection = .horizontal + flowLayout.sectionInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) + flowLayout.minimumLineSpacing = 12 + + collectionView.collectionViewLayout = flowLayout + + collectionView.reloadData() + } + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryTableViewCell.xib b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryTableViewCell.xib new file mode 100644 index 0000000..561c918 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/Cell/StoryTableViewCell.xib @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedDataManager/FeedAPIInput.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedDataManager/FeedAPIInput.swift new file mode 100644 index 0000000..3bc90ac --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedDataManager/FeedAPIInput.swift @@ -0,0 +1,11 @@ +// +// FeedAPIInput.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/14. +// + +struct FeedAPIInput : Encodable{ + var limit : Int? + var page : Int? +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedDataManager/FeedDataManager.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedDataManager/FeedDataManager.swift new file mode 100644 index 0000000..d115136 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedDataManager/FeedDataManager.swift @@ -0,0 +1,21 @@ +// +// FeedDataManager.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/14. +// + +import Alamofire + +class FeedDataManager { + func feedDataManager(_ parameters : FeedAPIInput, _ viewController : HomeViewController){ + AF.request("https://api.thecatapi.com/v1/images/search", method: .get, parameters: parameters).validate().responseDecodable(of: [FeedModel].self) { response in + switch response.result{ + case .success(let result): + viewController.successAPI(result) + case .failure(let error): + print(error.localizedDescription) + } + } + } +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedDataManager/FeedModel.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedDataManager/FeedModel.swift new file mode 100644 index 0000000..d8137a1 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedDataManager/FeedModel.swift @@ -0,0 +1,11 @@ +// +// FeedModel.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/14. +// + +struct FeedModel : Decodable{ + var id : String? + var url : String? +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedUploadDataManager/FeedUploadDataManager.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedUploadDataManager/FeedUploadDataManager.swift new file mode 100644 index 0000000..598d94b --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedUploadDataManager/FeedUploadDataManager.swift @@ -0,0 +1,26 @@ +// +// FeedUploadDataManager.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/15. +// + +import Alamofire + +class FeedUploadDataManager{ + func posts(_ viewController : HomeViewController, _ parameter: FeedUploadInput){ + AF.request("https://edu-api-ios-test.softsquared.com/posts" + , method: .post, parameters: parameter, encoder: JSONParameterEncoder.default, headers: nil).validate().responseDecodable(of: FeedUploadModel.self){response in + switch response.result{ + case .success(let result): + if result.isSuccess{ + print("성공") + } else{ + print(result.message) + } + case .failure(let error): + print(error.localizedDescription) + } + } + } +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedUploadDataManager/FeedUploadInput.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedUploadDataManager/FeedUploadInput.swift new file mode 100644 index 0000000..cb203fd --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedUploadDataManager/FeedUploadInput.swift @@ -0,0 +1,11 @@ +// +// FeedUploadInput.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/15. +// + +struct FeedUploadInput : Encodable{ + var content : String? + var postImgsUrl : [String]? +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedUploadDataManager/FeedUploadModel.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedUploadDataManager/FeedUploadModel.swift new file mode 100644 index 0000000..16a8f7a --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/FeedUploadDataManager/FeedUploadModel.swift @@ -0,0 +1,17 @@ +// +// FeedUploadModel.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/15. +// + +struct FeedUploadModel : Decodable{ + var isSuccess : Bool + var code : Int + var message : String + var result : FeedUploadResult? +} + +struct FeedUploadResult : Decodable{ + var postIdx : Int? +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/HomeViewController.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/HomeViewController.swift new file mode 100644 index 0000000..126c477 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Home/HomeViewController.swift @@ -0,0 +1,119 @@ +// +// HomeViewController.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/16. +// + +import UIKit +import Kingfisher + +class HomeViewController: UIViewController { + + @IBOutlet weak var tableView: UITableView! + var arrayCat : [FeedModel] = [] + + let imagePickerViewController = UIImagePickerController() + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + let feedNib = UINib(nibName: "FeedTableViewCell", bundle: nil) + tableView.register(feedNib, forCellReuseIdentifier: "FeedTableViewCell") + let storyNib = UINib(nibName: "StoryTableViewCell", bundle: nil) + tableView.register(storyNib, forCellReuseIdentifier: "StoryTableViewCell") + + let input = FeedAPIInput(limit: 30, page: 10) + FeedDataManager().feedDataManager(input, self) + + imagePickerViewController.delegate = self + } + + + @IBAction func buttonGoAlbum(_ sender: Any) { + self.imagePickerViewController.sourceType = .photoLibrary + self.present(imagePickerViewController, animated: true, completion: nil) + } +} + +extension HomeViewController : UITableViewDelegate, UITableViewDataSource{ + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return arrayCat.count + 1 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if indexPath.row == 0{ + guard let cell = tableView.dequeueReusableCell(withIdentifier: "StoryTableViewCell", for: indexPath) as? StoryTableViewCell else{ + return UITableViewCell() + } + return cell + } else{ + guard let cell = tableView.dequeueReusableCell(withIdentifier: "FeedTableViewCell", for: indexPath) as? FeedTableViewCell else{ + return UITableViewCell() + } + if let urlString = arrayCat[indexPath.row - 1].url{ + let url = URL(string: urlString) + cell.imageViewFeed.kf.setImage(with: url) + } + + return cell + } + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + if indexPath.row == 0{ + return 80 + }else{ + return 600 + } + } + + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard let tableViewCell = cell as? StoryTableViewCell else{ + return + } + + tableViewCell.setCollectionViewDataSourceDelegate(dataSourceDelegate: self, forRow: indexPath.row) + } +} + +extension HomeViewController : UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{ + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return 10 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "StoryCollectionViewCell", for: indexPath) as? StoryCollectionViewCell else{ + return UICollectionViewCell() + } + return cell + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: 50, height: 50) + } +} + +extension HomeViewController { + func successAPI(_ result : [FeedModel]){ + arrayCat = result + tableView.reloadData() + } +} + +extension HomeViewController : UIImagePickerControllerDelegate, UINavigationControllerDelegate { + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage{ + let imageString = "https://firebasestorage.googleapis.com/v0/b/catstargram-d7fbf.appspot.com/o/Cat1?alt=media&token=e92d1af6-ceb3-4a0c-9ba9-acd5cf534a42" + + let input = FeedUploadInput(content: "귀여운 고양이", postImgsUrl: [imageString]) + FeedUploadDataManager().posts(self, input) + + self.dismiss(animated: true, completion: nil) + } + } +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/PostCell/PostCollectionViewCell.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/PostCell/PostCollectionViewCell.swift new file mode 100644 index 0000000..3c2dd00 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/PostCell/PostCollectionViewCell.swift @@ -0,0 +1,22 @@ +// +// PostCollectionViewCell.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/01. +// + +import UIKit + +class PostCollectionViewCell: UICollectionViewCell { + static let identifier = "PostCollectionViewCell" + + @IBOutlet weak var postImageView: UIImageView! + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + public func setupData(){ + //이미지 뷰의 이미지를 업로드한다. + } +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/PostCell/PostCollectionViewCell.xib b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/PostCell/PostCollectionViewCell.xib new file mode 100644 index 0000000..d06724c --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/PostCell/PostCollectionViewCell.xib @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/ProfileCell/ProfileCollectionViewCell.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/ProfileCell/ProfileCollectionViewCell.swift new file mode 100644 index 0000000..3b6b2da --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/ProfileCell/ProfileCollectionViewCell.swift @@ -0,0 +1,45 @@ +// +// ProfileCollectionViewCell.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/30. +// + +import UIKit + +class ProfileCollectionViewCell: UICollectionViewCell { + static let identifier = "ProfileCollectionViewCell" + + @IBOutlet weak var profileImageView: UIImageView! + @IBOutlet weak var addProfileImageView: UIImageView! + + @IBOutlet weak var editButton: UIButton! + @IBOutlet weak var addFriendButton: UIButton! + + @IBOutlet weak var postingCountLabel: UILabel! + @IBOutlet weak var followerCountLabel: UILabel! + @IBOutlet weak var followingCountLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + setupAttribute() + } + + private func setupAttribute(){ + profileImageView.layer.cornerRadius = 44 + addProfileImageView.layer.cornerRadius = 12 + + profileImageView.layer.borderColor = UIColor.darkGray.cgColor + profileImageView.layer.borderWidth = 1 + + editButton.layer.cornerRadius = 5 + editButton.layer.borderColor = UIColor.lightGray.cgColor + editButton.layer.borderWidth = 1 + + addFriendButton.layer.cornerRadius = 3 + addFriendButton.layer.borderColor = UIColor.lightGray.cgColor + addFriendButton.layer.borderWidth = 1 + + [postingCountLabel, followerCountLabel, followingCountLabel].forEach{$0.text="\(Int.random(in: 0...10))"} + } +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/ProfileCell/ProfileCollectionViewCell.xib b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/ProfileCell/ProfileCollectionViewCell.xib new file mode 100644 index 0000000..9bacfc4 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/Cell/ProfileCell/ProfileCollectionViewCell.xib @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/ProfileViewController.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/ProfileViewController.swift new file mode 100644 index 0000000..c14c127 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/ProfileViewController.swift @@ -0,0 +1,98 @@ +// +// ProfileViewController.swift +// CatStaGram +// +// Created by 송재민 on 2022/04/30. +// + +import UIKit + +class ProfileViewController: UIViewController { + //MARK: - Properties + @IBOutlet weak var profileCollectionView: UICollectionView! + + //MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + setupCollectionView() + // Do any additional setup after loading the view. + } + + //MARK: - Actions + + //MARK: - Helpers + private func setupCollectionView(){ + profileCollectionView.delegate = self + profileCollectionView.dataSource = self + + profileCollectionView.register(UINib(nibName: "ProfileCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: ProfileCollectionViewCell.identifier) + + profileCollectionView.register(UINib(nibName: "PostCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: PostCollectionViewCell.identifier) + } +} + +//MARK: - UICollectionViewDelegate, UICollectionViewDataSource +extension ProfileViewController: UICollectionViewDelegate, UICollectionViewDataSource{ + + func numberOfSections(in collectionView: UICollectionView) -> Int { + return 2 + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + switch section{ + case 0: + return 1 + default: + return 24 + } + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + + let section = indexPath.section + switch section{ + case 0: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ProfileCollectionViewCell.identifier, for: indexPath) as? ProfileCollectionViewCell else{ + fatalError("셀 타입 캐스팅 실패") + } + return cell + default: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PostCollectionViewCell.identifier, for: indexPath) as? PostCollectionViewCell else{ + fatalError("셀 타입 캐스팅 실패") + } + return cell + } + + } +} + +extension ProfileViewController: UICollectionViewDelegateFlowLayout{ + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + + let section = indexPath.section + switch section{ + case 0: + return CGSize(width: collectionView.frame.width, height: CGFloat(159)) + default: + let side = CGFloat((collectionView.frame.width / 3) - (4/3)) + return CGSize(width: side, height: side) + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + switch section{ + case 0: + return CGFloat(0) + default: + return CGFloat(1) + } + } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + switch section{ + case 0: + return CGFloat(0) + default: + return CGFloat(1) + } + } +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/DeleteUserFeed/DeleteUserFeed.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/DeleteUserFeed/DeleteUserFeed.swift new file mode 100644 index 0000000..bb071d4 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/DeleteUserFeed/DeleteUserFeed.swift @@ -0,0 +1,8 @@ +// +// DeleteUserFeed.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/15. +// + +import Foundation diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeed/UserFeedDataManager.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeed/UserFeedDataManager.swift new file mode 100644 index 0000000..71fa167 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeed/UserFeedDataManager.swift @@ -0,0 +1,22 @@ +// +// UserFeedDataManager.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/15. +// + + +import Alamofire + +class UserFeedDataManager { + func getUserFeed(_ viewController : ProfileViewController, _ userID : Int = 2){ + AF.request("https://edu-api-ios-test.softsquared.com/users/\(userID)", method: .get, parameters: nil).validate().responseDecodable(of: UserFeedModel.self) { response in + switch response.result{ + case .success(let result): + viewController.successFeedAPI(result) + case .failure(let error): + print(error.localizedDescription) + } + } + } +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeed/UserFeedModel.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeed/UserFeedModel.swift new file mode 100644 index 0000000..cb38439 --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeed/UserFeedModel.swift @@ -0,0 +1,40 @@ +// +// UserFeedModel.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/15. +// + +import Foundation + +struct UserFeedModel : Decodable{ + + let isSuccess: Bool? + let code: Int? + let message: String? + let result: UserFeedModelResult? + +} + +struct UserFeedModelResult: Decodable{ + let _isMyFeed: Bool? + let getUserInfo: GetUserInfo? + let getUserPosts : [GetUserPosts]? +} + +struct GetUserInfo: Decodable{ + let userIdx: Int? + let nickName: String? + let name: String? + let profileImgUrl: String? + let website: String? + let introduction: String? + let followerCount: Int? + let followingCount: Int? + let postCount: Int? +} + +struct GetUserPosts : Decodable{ + let postIdx: Int? + let postImgUrl: String? +} diff --git a/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeedDataManager.swift b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeedDataManager.swift new file mode 100644 index 0000000..8a4766a --- /dev/null +++ b/jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeedDataManager.swift @@ -0,0 +1,8 @@ +// +// UserFeedDataManager.swift +// CatStaGram +// +// Created by 송재민 on 2022/05/15. +// + +import Foundation diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/Apps/AppDelegate.swift b/jaem/week8/Todo_MVVM/Todo_MVVM/Apps/AppDelegate.swift new file mode 100644 index 0000000..fa67c86 --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/Apps/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// Todo_MVVM +// +// Created by 송재민 on 2022/05/18. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/Apps/SceneDelegate.swift b/jaem/week8/Todo_MVVM/Todo_MVVM/Apps/SceneDelegate.swift new file mode 100644 index 0000000..d6df93a --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/Apps/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// Todo_MVVM +// +// Created by 송재민 on 2022/05/18. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/Cell/TodoTableViewCell.swift b/jaem/week8/Todo_MVVM/Todo_MVVM/Cell/TodoTableViewCell.swift new file mode 100644 index 0000000..9a0d400 --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/Cell/TodoTableViewCell.swift @@ -0,0 +1,8 @@ +// +// TodoTableViewCell.swift +// Todo_MVVM +// +// Created by 송재민 on 2022/05/18. +// + +import Foundation diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/Model/TodoModel.swift b/jaem/week8/Todo_MVVM/Todo_MVVM/Model/TodoModel.swift new file mode 100644 index 0000000..affeacf --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/Model/TodoModel.swift @@ -0,0 +1,8 @@ +// +// TodoModel.swift +// Todo_MVVM +// +// Created by 송재민 on 2022/05/18. +// + +import Foundation diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/View/Base.lproj/Main.storyboard b/jaem/week8/Todo_MVVM/Todo_MVVM/View/Base.lproj/Main.storyboard new file mode 100644 index 0000000..25a7638 --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/View/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/ViewController/ViewController.swift b/jaem/week8/Todo_MVVM/Todo_MVVM/ViewController/ViewController.swift new file mode 100644 index 0000000..ff705b4 --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/ViewController/ViewController.swift @@ -0,0 +1,19 @@ +// +// ViewController.swift +// Todo_MVVM +// +// Created by 송재민 on 2022/05/18. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + + +} + diff --git a/jaem/week8/Todo_MVVM/Todo_MVVM/ViewModel/TodoVM.swift b/jaem/week8/Todo_MVVM/Todo_MVVM/ViewModel/TodoVM.swift new file mode 100644 index 0000000..e1dfcee --- /dev/null +++ b/jaem/week8/Todo_MVVM/Todo_MVVM/ViewModel/TodoVM.swift @@ -0,0 +1,8 @@ +// +// TodoVM.swift +// Todo_MVVM +// +// Created by 송재민 on 2022/05/18. +// + +import Foundation From f45ccf1ed18a9e935cc15157f8118945a69300ab Mon Sep 17 00:00:00 2001 From: xongjaemin Date: Wed, 18 May 2022 04:46:46 +0900 Subject: [PATCH 5/9] =?UTF-8?q?dev:=20mvvm=20=ED=88=AC=EB=91=90=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A7=84=ED=96=89=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserInterfaceState.xcuserstate | Bin 37028 -> 41646 bytes jaem/week5/CatStaGram/CatStaGram/.Podfile.swo | Bin 0 -> 12288 bytes jaem/week5/CatStaGram/CatStaGram/.Podfile.swp | Bin 0 -> 12288 bytes jaem/week5/CatStaGram/CatStaGram/Podfile | 19 + .../NewsApp/NewsApp.xcodeproj/project.pbxproj | 174 ++ .../UserInterfaceState.xcuserstate | Bin 41914 -> 47596 bytes .../xcschemes/xcschememanagement.plist | 2 +- .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 41442 bytes .../xcdebugger/Breakpoints_v2.xcbkptlist | 24 + .../NewsApp/ArticleModel/Article.swift | 6 +- .../NewsApp/Base.lproj/Main.storyboard | 53 +- jaem/week6/NewsApp/NewsApp/DetailVC.swift | 19 +- jaem/week6/NewsApp/NewsApp/Info.plist | 5 + .../NewsApp/NewsApp/NewsTableViewCell.swift | 43 +- .../NewsApp/NewsApp/NewsTableViewCell.xib | 13 + .../NewsApp/NewsApp/ViewController.swift | 141 +- jaem/week6/NewsApp/Podfile | 21 + jaem/week6/NewsApp/Podfile.lock | 20 + jaem/week6/NewsApp/Pods/Alamofire/LICENSE | 19 + jaem/week6/NewsApp/Pods/Alamofire/README.md | 227 ++ .../Pods/Alamofire/Source/AFError.swift | 870 ++++++++ .../Pods/Alamofire/Source/Alamofire.swift | 40 + .../Alamofire/Source/AlamofireExtended.swift | 61 + .../Source/AuthenticationInterceptor.swift | 403 ++++ .../Source/CachedResponseHandler.swift | 109 + .../Pods/Alamofire/Source/Combine.swift | 655 ++++++ .../Pods/Alamofire/Source/Concurrency.swift | 704 ++++++ .../Source/DispatchQueue+Alamofire.swift | 37 + .../Pods/Alamofire/Source/EventMonitor.swift | 892 ++++++++ .../Pods/Alamofire/Source/HTTPHeaders.swift | 447 ++++ .../Pods/Alamofire/Source/HTTPMethod.swift | 56 + .../Alamofire/Source/MultipartFormData.swift | 584 +++++ .../Alamofire/Source/MultipartUpload.swift | 89 + .../Source/NetworkReachabilityManager.swift | 267 +++ .../Pods/Alamofire/Source/Notifications.swift | 115 + .../Source/OperationQueue+Alamofire.swift | 49 + .../Alamofire/Source/ParameterEncoder.swift | 217 ++ .../Alamofire/Source/ParameterEncoding.swift | 321 +++ .../Pods/Alamofire/Source/Protected.swift | 161 ++ .../Alamofire/Source/RedirectHandler.swift | 113 + .../Pods/Alamofire/Source/Request.swift | 1912 +++++++++++++++++ .../Alamofire/Source/RequestInterceptor.swift | 357 +++ .../Alamofire/Source/RequestTaskMap.swift | 149 ++ .../Pods/Alamofire/Source/Response.swift | 453 ++++ .../Source/ResponseSerialization.swift | 1290 +++++++++++ .../Alamofire/Source/Result+Alamofire.swift | 120 ++ .../Pods/Alamofire/Source/RetryPolicy.swift | 434 ++++ .../Source/ServerTrustEvaluation.swift | 739 +++++++ .../Pods/Alamofire/Source/Session.swift | 1264 +++++++++++ .../Alamofire/Source/SessionDelegate.swift | 336 +++ .../Source/StringEncoding+Alamofire.swift | 55 + ...URLConvertible+URLRequestConvertible.swift | 105 + .../Source/URLEncodedFormEncoder.swift | 982 +++++++++ .../Source/URLRequest+Alamofire.swift | 39 + .../URLSessionConfiguration+Alamofire.swift | 46 + .../Pods/Alamofire/Source/Validation.swift | 302 +++ jaem/week6/NewsApp/Pods/Kingfisher/LICENSE | 22 + jaem/week6/NewsApp/Pods/Kingfisher/README.md | 258 +++ .../Sources/Cache/CacheSerializer.swift | 117 + .../Sources/Cache/DiskStorage.swift | 586 +++++ .../FormatIndicatedCacheSerializer.swift | 118 + .../Kingfisher/Sources/Cache/ImageCache.swift | 875 ++++++++ .../Sources/Cache/MemoryStorage.swift | 285 +++ .../Kingfisher/Sources/Cache/Storage.swift | 113 + .../Extensions/CPListItem+Kingfisher.swift | 258 +++ .../Extensions/ImageView+Kingfisher.swift | 545 +++++ .../Extensions/NSButton+Kingfisher.swift | 370 ++++ .../NSTextAttachment+Kingfisher.swift | 279 +++ .../TVMonogramView+Kingfisher.swift | 217 ++ .../Extensions/UIButton+Kingfisher.swift | 416 ++++ .../WKInterfaceImage+Kingfisher.swift | 212 ++ .../AVAssetImageDataProvider.swift | 137 ++ .../ImageSource/ImageDataProvider.swift | 170 ++ .../General/ImageSource/Resource.swift | 115 + .../Sources/General/ImageSource/Source.swift | 123 ++ .../Pods/Kingfisher/Sources/General/KF.swift | 442 ++++ .../Sources/General/KFOptionsSetter.swift | 707 ++++++ .../Sources/General/Kingfisher.swift | 106 + .../Sources/General/KingfisherError.swift | 461 ++++ .../Sources/General/KingfisherManager.swift | 739 +++++++ .../General/KingfisherOptionsInfo.swift | 400 ++++ .../Kingfisher/Sources/Image/Filter.swift | 146 ++ .../Sources/Image/GIFAnimatedImage.swift | 121 ++ .../Sources/Image/GraphicsContext.swift | 88 + .../Pods/Kingfisher/Sources/Image/Image.swift | 377 ++++ .../Sources/Image/ImageDrawing.swift | 632 ++++++ .../Sources/Image/ImageFormat.swift | 130 ++ .../Sources/Image/ImageProcessor.swift | 935 ++++++++ .../Sources/Image/ImageProgressive.swift | 328 +++ .../Sources/Image/ImageTransition.swift | 118 + .../Sources/Image/Placeholder.swift | 82 + .../AuthenticationChallengeResponsable.swift | 94 + .../Networking/ImageDataProcessor.swift | 74 + .../Sources/Networking/ImageDownloader.swift | 488 +++++ .../Networking/ImageDownloaderDelegate.swift | 154 ++ .../Sources/Networking/ImageModifier.swift | 116 + .../Sources/Networking/ImagePrefetcher.swift | 442 ++++ .../Sources/Networking/RedirectHandler.swift | 76 + .../Sources/Networking/RequestModifier.swift | 108 + .../Sources/Networking/RetryStrategy.swift | 153 ++ .../Sources/Networking/SessionDataTask.swift | 127 ++ .../Sources/Networking/SessionDelegate.swift | 263 +++ .../Sources/SwiftUI/ImageBinder.swift | 130 ++ .../Sources/SwiftUI/ImageContext.swift | 99 + .../Sources/SwiftUI/KFAnimatedImage.swift | 96 + .../Kingfisher/Sources/SwiftUI/KFImage.swift | 106 + .../Sources/SwiftUI/KFImageOptions.swift | 138 ++ .../Sources/SwiftUI/KFImageProtocol.swift | 93 + .../Sources/SwiftUI/KFImageRenderer.swift | 105 + .../Pods/Kingfisher/Sources/Utility/Box.swift | 34 + .../Sources/Utility/CallbackQueue.swift | 83 + .../Kingfisher/Sources/Utility/Delegate.swift | 132 ++ .../Sources/Utility/ExtensionHelpers.swift | 125 ++ .../Kingfisher/Sources/Utility/Result.swift | 71 + .../Kingfisher/Sources/Utility/Runtime.swift | 35 + .../Sources/Utility/SizeExtensions.swift | 110 + .../Sources/Utility/String+MD5.swift | 288 +++ .../Sources/Views/AnimatedImageView.swift | 660 ++++++ .../Kingfisher/Sources/Views/Indicator.swift | 231 ++ jaem/week6/NewsApp/Pods/Manifest.lock | 20 + .../Pods/Pods.xcodeproj/project.pbxproj | 1555 ++++++++++++++ .../xcschemes/Alamofire.xcscheme | 58 + .../xcschemes/Kingfisher.xcscheme | 58 + .../Pods-NewsApp-NewsAppUITests.xcscheme | 58 + .../xcschemes/Pods-NewsApp.xcscheme | 58 + .../xcschemes/Pods-NewsAppTests.xcscheme | 58 + .../xcschemes/xcschememanagement.plist | 46 + .../Alamofire/Alamofire-Info.plist | 26 + .../Alamofire/Alamofire-dummy.m | 5 + .../Alamofire/Alamofire-prefix.pch | 12 + .../Alamofire/Alamofire-umbrella.h | 16 + .../Alamofire/Alamofire.debug.xcconfig | 14 + .../Alamofire/Alamofire.modulemap | 6 + .../Alamofire/Alamofire.release.xcconfig | 14 + .../Kingfisher/Kingfisher-Info.plist | 26 + .../Kingfisher/Kingfisher-dummy.m | 5 + .../Kingfisher/Kingfisher-prefix.pch | 12 + .../Kingfisher/Kingfisher-umbrella.h | 16 + .../Kingfisher/Kingfisher.debug.xcconfig | 14 + .../Kingfisher/Kingfisher.modulemap | 6 + .../Kingfisher/Kingfisher.release.xcconfig | 14 + .../Pods-NewsApp-NewsAppUITests-Info.plist | 26 + ...p-NewsAppUITests-acknowledgements.markdown | 52 + ...sApp-NewsAppUITests-acknowledgements.plist | 90 + .../Pods-NewsApp-NewsAppUITests-dummy.m | 5 + ...ts-frameworks-Debug-input-files.xcfilelist | 3 + ...s-frameworks-Debug-output-files.xcfilelist | 2 + ...-frameworks-Release-input-files.xcfilelist | 3 + ...frameworks-Release-output-files.xcfilelist | 2 + .../Pods-NewsApp-NewsAppUITests-frameworks.sh | 188 ++ .../Pods-NewsApp-NewsAppUITests-umbrella.h | 16 + ...Pods-NewsApp-NewsAppUITests.debug.xcconfig | 15 + .../Pods-NewsApp-NewsAppUITests.modulemap | 6 + ...ds-NewsApp-NewsAppUITests.release.xcconfig | 15 + .../Pods-NewsApp/Pods-NewsApp-Info.plist | 26 + .../Pods-NewsApp-acknowledgements.markdown | 52 + .../Pods-NewsApp-acknowledgements.plist | 90 + .../Pods-NewsApp/Pods-NewsApp-dummy.m | 5 + ...pp-frameworks-Debug-input-files.xcfilelist | 3 + ...p-frameworks-Debug-output-files.xcfilelist | 2 + ...-frameworks-Release-input-files.xcfilelist | 3 + ...frameworks-Release-output-files.xcfilelist | 2 + .../Pods-NewsApp/Pods-NewsApp-frameworks.sh | 188 ++ .../Pods-NewsApp/Pods-NewsApp-umbrella.h | 16 + .../Pods-NewsApp/Pods-NewsApp.debug.xcconfig | 15 + .../Pods-NewsApp/Pods-NewsApp.modulemap | 6 + .../Pods-NewsApp.release.xcconfig | 15 + .../Pods-NewsAppTests-Info.plist | 26 + ...ods-NewsAppTests-acknowledgements.markdown | 3 + .../Pods-NewsAppTests-acknowledgements.plist | 29 + .../Pods-NewsAppTests-dummy.m | 5 + .../Pods-NewsAppTests-umbrella.h | 16 + .../Pods-NewsAppTests.debug.xcconfig | 11 + .../Pods-NewsAppTests.modulemap | 6 + .../Pods-NewsAppTests.release.xcconfig | 11 + .../CatStaGram.xcodeproj/project.pbxproj | 891 ++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 72112 bytes .../xcdebugger/Breakpoints_v2.xcbkptlist | 6 + .../xcschemes/xcschememanagement.plist | 14 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 60381 bytes .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 + .../Assets.xcassets/Colors/Contents.json | 6 + .../Contents.json | 38 + .../facebookColor.colorset/Contents.json | 38 + .../CatStaGram/Assets.xcassets/Contents.json | 6 + .../ic_catstagram_logo.imageset/Contents.json | 21 + .../ic_catstagram_logo.png | Bin 0 -> 11616 bytes .../Contents.json" | 6 + .../ic_login_facebook.imageset/Contents.json" | 21 + .../ic_login_facebook.png" | Bin 0 -> 6350 bytes .../ic_login_hidden.imageset/Contents.json" | 21 + .../ic_login_hidden.png" | Bin 0 -> 11970 bytes .../ic_login_show.imageset/Contents.json" | 21 + .../ic_login_show.imageset/ic_login_show.png" | Bin 0 -> 14608 bytes .../\353\246\264\354\212\244/Contents.json" | 6 + .../ic_reels_comment.imageset/Contents.json" | 21 + .../ic_reels_comment.png" | Bin 0 -> 12247 bytes .../ic_reels_heart.imageset/Contents.json" | 21 + .../ic_reels_heart.png" | Bin 0 -> 17486 bytes .../Contents.json" | 21 + .../ic_reels_heart_full.png" | Bin 0 -> 11641 bytes .../ic_reels_more.imageset/Contents.json" | 21 + .../ic_reels_more.imageset/ic_reels_more.png" | Bin 0 -> 4354 bytes .../ic_reels_send.imageset/Contents.json" | 21 + .../ic_reels_send.imageset/ic_reels_send.png" | Bin 0 -> 18159 bytes .../\353\247\210\354\235\264/Contents.json" | 6 + .../ic_my_hamburger.imageset/Contents.json" | 21 + .../ic_my_hamburger.png" | Bin 0 -> 2793 bytes .../ic_my_invite.imageset/Contents.json" | 21 + .../ic_my_invite.imageset/ic_my_invite.png" | Bin 0 -> 13230 bytes .../ic_my_upload.imageset/Contents.json" | 21 + .../ic_my_upload.imageset/ic_my_upload.png" | Bin 0 -> 9855 bytes .../\355\231\210/Contents.json" | 6 + .../ic_bookmark_black.imageset/Contents.json" | 21 + .../ic_bookmark_black.png" | Bin 0 -> 5921 bytes .../ic_bookmark_white.imageset/Contents.json" | 21 + .../ic_bookmark_white.png" | Bin 0 -> 5408 bytes .../Contents.json" | 21 + .../ic_homd_search_light.png" | Bin 0 -> 14511 bytes .../ic_home_comment.imageset/Contents.json" | 21 + .../ic_home_comment.png" | Bin 0 -> 10913 bytes .../ic_home_heart.imageset/Contents.json" | 21 + .../ic_home_heart.imageset/ic_home_heart.png" | Bin 0 -> 17743 bytes .../Contents.json" | 21 + .../ic_home_heart_full.png" | Bin 0 -> 11641 bytes .../Contents.json" | 21 + .../ic_home_home_black.png" | Bin 0 -> 5094 bytes .../Contents.json" | 21 + .../ic_home_home_white.png" | Bin 0 -> 9869 bytes .../ic_home_more.imageset/Contents.json" | 21 + .../ic_home_more.imageset/ic_home_more.png" | Bin 0 -> 3425 bytes .../ic_home_reels.imageset/Contents.json" | 21 + .../ic_home_reels.imageset/ic_home_reels.png" | Bin 0 -> 12969 bytes .../Contents.json" | 21 + .../ic_home_search_bold.png" | Bin 0 -> 12367 bytes .../ic_home_send.imageset/Contents.json" | 21 + .../ic_home_send.imageset/ic_home_send.png" | Bin 0 -> 12495 bytes .../Contents.json" | 21 + .../ic_home_shop_black.png" | Bin 0 -> 7889 bytes .../Contents.json" | 21 + .../ic_home_shop_white.png" | Bin 0 -> 7004 bytes .../ic_home_upload.imageset/Contents.json" | 21 + .../ic_home_upload.png" | Bin 0 -> 9855 bytes .../Base.lproj/LaunchScreen.storyboard | 25 + .../CatStaGram/Base.lproj/Main.storyboard | 541 +++++ jaem/week7/CatStaGram/CatStaGram/Info.plist | 25 + .../UIViewController+Extension.swift | 21 + .../CatStaGram/UIViewExtension.swift | 20 + .../CatStaGram/CatStaGram/UserInfo.swift | 15 + .../PostCell/PostCollectionViewCell.swift | 9 +- .../Profile/ProfileViewController.swift | 67 +- .../DeleteUserFeed/DeleteUserFeed.swift | 7 + .../UserFeed/UserFeedDataManager.swift | 13 + .../UserFeedDataManager.swift | 8 - .../CatStaGramTests/CatStaGramTests.swift | 36 + .../CatStaGramUITests/CatStaGramUITests.swift | 42 + .../CatStaGramUITestsLaunchTests.swift | 32 + jaem/week7/CatStaGram/Podfile | 11 + jaem/week7/CatStaGram/Podfile.lock | 20 + jaem/week7/CatStaGram/Pods/Alamofire/LICENSE | 19 + .../week7/CatStaGram/Pods/Alamofire/README.md | 227 ++ .../Pods/Alamofire/Source/AFError.swift | 870 ++++++++ .../Pods/Alamofire/Source/Alamofire.swift | 40 + .../Alamofire/Source/AlamofireExtended.swift | 61 + .../Source/AuthenticationInterceptor.swift | 403 ++++ .../Source/CachedResponseHandler.swift | 109 + .../Pods/Alamofire/Source/Combine.swift | 655 ++++++ .../Pods/Alamofire/Source/Concurrency.swift | 704 ++++++ .../Source/DispatchQueue+Alamofire.swift | 37 + .../Pods/Alamofire/Source/EventMonitor.swift | 892 ++++++++ .../Pods/Alamofire/Source/HTTPHeaders.swift | 447 ++++ .../Pods/Alamofire/Source/HTTPMethod.swift | 56 + .../Alamofire/Source/MultipartFormData.swift | 584 +++++ .../Alamofire/Source/MultipartUpload.swift | 89 + .../Source/NetworkReachabilityManager.swift | 267 +++ .../Pods/Alamofire/Source/Notifications.swift | 115 + .../Source/OperationQueue+Alamofire.swift | 49 + .../Alamofire/Source/ParameterEncoder.swift | 217 ++ .../Alamofire/Source/ParameterEncoding.swift | 321 +++ .../Pods/Alamofire/Source/Protected.swift | 161 ++ .../Alamofire/Source/RedirectHandler.swift | 113 + .../Pods/Alamofire/Source/Request.swift | 1912 +++++++++++++++++ .../Alamofire/Source/RequestInterceptor.swift | 357 +++ .../Alamofire/Source/RequestTaskMap.swift | 149 ++ .../Pods/Alamofire/Source/Response.swift | 453 ++++ .../Source/ResponseSerialization.swift | 1290 +++++++++++ .../Alamofire/Source/Result+Alamofire.swift | 120 ++ .../Pods/Alamofire/Source/RetryPolicy.swift | 434 ++++ .../Source/ServerTrustEvaluation.swift | 739 +++++++ .../Pods/Alamofire/Source/Session.swift | 1264 +++++++++++ .../Alamofire/Source/SessionDelegate.swift | 336 +++ .../Source/StringEncoding+Alamofire.swift | 55 + ...URLConvertible+URLRequestConvertible.swift | 105 + .../Source/URLEncodedFormEncoder.swift | 982 +++++++++ .../Source/URLRequest+Alamofire.swift | 39 + .../URLSessionConfiguration+Alamofire.swift | 46 + .../Pods/Alamofire/Source/Validation.swift | 302 +++ jaem/week7/CatStaGram/Pods/Kingfisher/LICENSE | 22 + .../CatStaGram/Pods/Kingfisher/README.md | 258 +++ .../Sources/Cache/CacheSerializer.swift | 117 + .../Sources/Cache/DiskStorage.swift | 586 +++++ .../FormatIndicatedCacheSerializer.swift | 118 + .../Kingfisher/Sources/Cache/ImageCache.swift | 875 ++++++++ .../Sources/Cache/MemoryStorage.swift | 285 +++ .../Kingfisher/Sources/Cache/Storage.swift | 113 + .../Extensions/CPListItem+Kingfisher.swift | 258 +++ .../Extensions/ImageView+Kingfisher.swift | 545 +++++ .../Extensions/NSButton+Kingfisher.swift | 370 ++++ .../NSTextAttachment+Kingfisher.swift | 279 +++ .../TVMonogramView+Kingfisher.swift | 217 ++ .../Extensions/UIButton+Kingfisher.swift | 416 ++++ .../WKInterfaceImage+Kingfisher.swift | 212 ++ .../AVAssetImageDataProvider.swift | 137 ++ .../ImageSource/ImageDataProvider.swift | 170 ++ .../General/ImageSource/Resource.swift | 115 + .../Sources/General/ImageSource/Source.swift | 123 ++ .../Pods/Kingfisher/Sources/General/KF.swift | 442 ++++ .../Sources/General/KFOptionsSetter.swift | 707 ++++++ .../Sources/General/Kingfisher.swift | 106 + .../Sources/General/KingfisherError.swift | 461 ++++ .../Sources/General/KingfisherManager.swift | 739 +++++++ .../General/KingfisherOptionsInfo.swift | 400 ++++ .../Kingfisher/Sources/Image/Filter.swift | 146 ++ .../Sources/Image/GIFAnimatedImage.swift | 121 ++ .../Sources/Image/GraphicsContext.swift | 88 + .../Pods/Kingfisher/Sources/Image/Image.swift | 377 ++++ .../Sources/Image/ImageDrawing.swift | 632 ++++++ .../Sources/Image/ImageFormat.swift | 130 ++ .../Sources/Image/ImageProcessor.swift | 935 ++++++++ .../Sources/Image/ImageProgressive.swift | 328 +++ .../Sources/Image/ImageTransition.swift | 118 + .../Sources/Image/Placeholder.swift | 82 + .../AuthenticationChallengeResponsable.swift | 94 + .../Networking/ImageDataProcessor.swift | 74 + .../Sources/Networking/ImageDownloader.swift | 488 +++++ .../Networking/ImageDownloaderDelegate.swift | 154 ++ .../Sources/Networking/ImageModifier.swift | 116 + .../Sources/Networking/ImagePrefetcher.swift | 442 ++++ .../Sources/Networking/RedirectHandler.swift | 76 + .../Sources/Networking/RequestModifier.swift | 108 + .../Sources/Networking/RetryStrategy.swift | 153 ++ .../Sources/Networking/SessionDataTask.swift | 127 ++ .../Sources/Networking/SessionDelegate.swift | 263 +++ .../Sources/SwiftUI/ImageBinder.swift | 130 ++ .../Sources/SwiftUI/ImageContext.swift | 99 + .../Sources/SwiftUI/KFAnimatedImage.swift | 96 + .../Kingfisher/Sources/SwiftUI/KFImage.swift | 106 + .../Sources/SwiftUI/KFImageOptions.swift | 138 ++ .../Sources/SwiftUI/KFImageProtocol.swift | 93 + .../Sources/SwiftUI/KFImageRenderer.swift | 105 + .../Pods/Kingfisher/Sources/Utility/Box.swift | 34 + .../Sources/Utility/CallbackQueue.swift | 83 + .../Kingfisher/Sources/Utility/Delegate.swift | 132 ++ .../Sources/Utility/ExtensionHelpers.swift | 125 ++ .../Kingfisher/Sources/Utility/Result.swift | 71 + .../Kingfisher/Sources/Utility/Runtime.swift | 35 + .../Sources/Utility/SizeExtensions.swift | 110 + .../Sources/Utility/String+MD5.swift | 288 +++ .../Sources/Views/AnimatedImageView.swift | 660 ++++++ .../Kingfisher/Sources/Views/Indicator.swift | 231 ++ jaem/week7/CatStaGram/Pods/Manifest.lock | 20 + .../Pods/Pods.xcodeproj/project.pbxproj | 1183 ++++++++++ .../xcschemes/Alamofire.xcscheme | 58 + .../xcschemes/Kingfisher.xcscheme | 58 + .../xcschemes/Pods-CatStaGram.xcscheme | 58 + .../xcschemes/xcschememanagement.plist | 32 + .../Alamofire/Alamofire-Info.plist | 26 + .../Alamofire/Alamofire-dummy.m | 5 + .../Alamofire/Alamofire-prefix.pch | 12 + .../Alamofire/Alamofire-umbrella.h | 16 + .../Alamofire/Alamofire.debug.xcconfig | 14 + .../Alamofire/Alamofire.modulemap | 6 + .../Alamofire/Alamofire.release.xcconfig | 14 + .../Kingfisher/Kingfisher-Info.plist | 26 + .../Kingfisher/Kingfisher-dummy.m | 5 + .../Kingfisher/Kingfisher-prefix.pch | 12 + .../Kingfisher/Kingfisher-umbrella.h | 16 + .../Kingfisher/Kingfisher.debug.xcconfig | 14 + .../Kingfisher/Kingfisher.modulemap | 6 + .../Kingfisher/Kingfisher.release.xcconfig | 14 + .../Pods-CatStaGram-Info.plist | 26 + .../Pods-CatStaGram-acknowledgements.markdown | 52 + .../Pods-CatStaGram-acknowledgements.plist | 90 + .../Pods-CatStaGram/Pods-CatStaGram-dummy.m | 5 + ...am-frameworks-Debug-input-files.xcfilelist | 3 + ...m-frameworks-Debug-output-files.xcfilelist | 2 + ...-frameworks-Release-input-files.xcfilelist | 3 + ...frameworks-Release-output-files.xcfilelist | 2 + .../Pods-CatStaGram-frameworks.sh | 188 ++ .../Pods-CatStaGram-umbrella.h | 16 + .../Pods-CatStaGram.debug.xcconfig | 15 + .../Pods-CatStaGram/Pods-CatStaGram.modulemap | 6 + .../Pods-CatStaGram.release.xcconfig | 15 + .../Todo_MVVM.xcodeproj/project.pbxproj | 669 ++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 32069 bytes .../xcschemes/xcschememanagement.plist | 14 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 + .../Todo_MVVM/Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Todo_MVVM/Cell/TodoTableViewCell.swift | 15 +- jaem/week8/Todo_MVVM/Todo_MVVM/Info.plist | 25 + .../Todo_MVVM/Todo_MVVM/Model/TodoModel.swift | 27 + .../Todo_MVVM/View/Base.lproj/Main.storyboard | 89 +- .../ViewController/ViewController.swift | 48 +- .../Todo_MVVM/ViewModel/TodoVM.swift | 22 +- .../Todo_MVVMTests/Todo_MVVMTests.swift | 36 + .../Todo_MVVMUITests/Todo_MVVMUITests.swift | 42 + .../Todo_MVVMUITestsLaunchTests.swift | 32 + 418 files changed, 70685 insertions(+), 95 deletions(-) create mode 100644 jaem/week5/CatStaGram/CatStaGram/.Podfile.swo create mode 100644 jaem/week5/CatStaGram/CatStaGram/.Podfile.swp create mode 100644 jaem/week5/CatStaGram/CatStaGram/Podfile create mode 100644 jaem/week6/NewsApp/NewsApp.xcworkspace/contents.xcworkspacedata create mode 100644 jaem/week6/NewsApp/NewsApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 jaem/week6/NewsApp/NewsApp.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 jaem/week6/NewsApp/NewsApp.xcworkspace/xcuserdata/songjaemin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 jaem/week6/NewsApp/Podfile create mode 100644 jaem/week6/NewsApp/Podfile.lock create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/LICENSE create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/README.md create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/AFError.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/Alamofire.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/AlamofireExtended.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/AuthenticationInterceptor.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/CachedResponseHandler.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/Combine.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/Concurrency.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/DispatchQueue+Alamofire.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/EventMonitor.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/HTTPHeaders.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/HTTPMethod.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/MultipartFormData.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/MultipartUpload.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/NetworkReachabilityManager.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/Notifications.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/OperationQueue+Alamofire.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/ParameterEncoder.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/ParameterEncoding.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/Protected.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/RedirectHandler.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/Request.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/RequestInterceptor.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/RequestTaskMap.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/Response.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/ResponseSerialization.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/Result+Alamofire.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/RetryPolicy.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/ServerTrustEvaluation.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/Session.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/SessionDelegate.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/StringEncoding+Alamofire.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/URLConvertible+URLRequestConvertible.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/URLEncodedFormEncoder.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/URLRequest+Alamofire.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/URLSessionConfiguration+Alamofire.swift create mode 100644 jaem/week6/NewsApp/Pods/Alamofire/Source/Validation.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/LICENSE create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/README.md create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/DiskStorage.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/ImageCache.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Cache/Storage.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/TVMonogramView+Kingfisher.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Extensions/WKInterfaceImage+Kingfisher.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/ImageSource/Source.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KF.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/Kingfisher.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KingfisherError.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KingfisherManager.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/Filter.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/GraphicsContext.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/Image.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageDrawing.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageFormat.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageProcessor.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageProgressive.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/ImageTransition.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Image/Placeholder.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImageModifier.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/RequestModifier.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Box.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Delegate.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Result.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/Runtime.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Utility/String+MD5.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift create mode 100644 jaem/week6/NewsApp/Pods/Kingfisher/Sources/Views/Indicator.swift create mode 100644 jaem/week6/NewsApp/Pods/Manifest.lock create mode 100644 jaem/week6/NewsApp/Pods/Pods.xcodeproj/project.pbxproj create mode 100644 jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Alamofire.xcscheme create mode 100644 jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Kingfisher.xcscheme create mode 100644 jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-NewsApp-NewsAppUITests.xcscheme create mode 100644 jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-NewsApp.xcscheme create mode 100644 jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-NewsAppTests.xcscheme create mode 100644 jaem/week6/NewsApp/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-Info.plist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-dummy.m create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-prefix.pch create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire-umbrella.h create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire.debug.xcconfig create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire.modulemap create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Alamofire/Alamofire.release.xcconfig create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-Info.plist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-acknowledgements.markdown create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-acknowledgements.plist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-dummy.m create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Debug-input-files.xcfilelist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Debug-output-files.xcfilelist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Release-input-files.xcfilelist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-Release-output-files.xcfilelist create mode 100755 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks.sh create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-umbrella.h create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.debug.xcconfig create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.modulemap create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.release.xcconfig create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-Info.plist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-acknowledgements.markdown create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-acknowledgements.plist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-dummy.m create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Debug-input-files.xcfilelist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Debug-output-files.xcfilelist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Release-input-files.xcfilelist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-Release-output-files.xcfilelist create mode 100755 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks.sh create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp-umbrella.h create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp.debug.xcconfig create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp.modulemap create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsApp/Pods-NewsApp.release.xcconfig create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-Info.plist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-acknowledgements.markdown create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-acknowledgements.plist create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-dummy.m create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests-umbrella.h create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.debug.xcconfig create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.modulemap create mode 100644 jaem/week6/NewsApp/Pods/Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.release.xcconfig create mode 100644 jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.pbxproj create mode 100644 jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 jaem/week7/CatStaGram/CatStaGram.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 jaem/week7/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 jaem/week7/CatStaGram/CatStaGram.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 jaem/week7/CatStaGram/CatStaGram.xcworkspace/contents.xcworkspacedata create mode 100644 jaem/week7/CatStaGram/CatStaGram.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 jaem/week7/CatStaGram/CatStaGram.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/Colors/Contents.json create mode 100644 jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/Colors/disabledButtonColor.colorset/Contents.json create mode 100644 jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/Colors/facebookColor.colorset/Contents.json create mode 100644 jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/Contents.json create mode 100644 jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/ic_catstagram_logo.imageset/Contents.json create mode 100644 jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/ic_catstagram_logo.imageset/ic_catstagram_logo.png create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_facebook.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_facebook.imageset/ic_login_facebook.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_hidden.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_hidden.imageset/ic_login_hidden.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_show.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\241\234\352\267\270\354\235\270/ic_login_show.imageset/ic_login_show.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_comment.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_comment.imageset/ic_reels_comment.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_heart.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_heart.imageset/ic_reels_heart.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_heart_full.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_heart_full.imageset/ic_reels_heart_full.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_more.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_more.imageset/ic_reels_more.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_send.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\246\264\354\212\244/ic_reels_send.imageset/ic_reels_send.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_hamburger.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_hamburger.imageset/ic_my_hamburger.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_invite.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_invite.imageset/ic_my_invite.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_upload.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\353\247\210\354\235\264/ic_my_upload.imageset/ic_my_upload.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_black.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_black.imageset/ic_bookmark_black.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_white.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_bookmark_white.imageset/ic_bookmark_white.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_homd_search_light.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_homd_search_light.imageset/ic_homd_search_light.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_comment.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_comment.imageset/ic_home_comment.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart.imageset/ic_home_heart.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart_full.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_heart_full.imageset/ic_home_heart_full.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_black.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_black.imageset/ic_home_home_black.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_white.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_home_white.imageset/ic_home_home_white.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_more.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_more.imageset/ic_home_more.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_reels.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_reels.imageset/ic_home_reels.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_search_bold.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_search_bold.imageset/ic_home_search_bold.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_send.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_send.imageset/ic_home_send.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_black.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_black.imageset/ic_home_shop_black.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_white.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_shop_white.imageset/ic_home_shop_white.png" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_upload.imageset/Contents.json" create mode 100644 "jaem/week7/CatStaGram/CatStaGram/Assets.xcassets/\355\231\210/ic_home_upload.imageset/ic_home_upload.png" create mode 100644 jaem/week7/CatStaGram/CatStaGram/Base.lproj/LaunchScreen.storyboard create mode 100644 jaem/week7/CatStaGram/CatStaGram/Base.lproj/Main.storyboard create mode 100644 jaem/week7/CatStaGram/CatStaGram/Info.plist create mode 100644 jaem/week7/CatStaGram/CatStaGram/UIViewController+Extension.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UIViewExtension.swift create mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInfo.swift delete mode 100644 jaem/week7/CatStaGram/CatStaGram/UserInterface/Profile/UserFeedDataManager/UserFeedDataManager.swift create mode 100644 jaem/week7/CatStaGram/CatStaGramTests/CatStaGramTests.swift create mode 100644 jaem/week7/CatStaGram/CatStaGramUITests/CatStaGramUITests.swift create mode 100644 jaem/week7/CatStaGram/CatStaGramUITests/CatStaGramUITestsLaunchTests.swift create mode 100644 jaem/week7/CatStaGram/Podfile create mode 100644 jaem/week7/CatStaGram/Podfile.lock create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/LICENSE create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/README.md create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/AFError.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/Alamofire.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/AlamofireExtended.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/AuthenticationInterceptor.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/CachedResponseHandler.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/Combine.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/Concurrency.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/DispatchQueue+Alamofire.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/EventMonitor.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/HTTPHeaders.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/HTTPMethod.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/MultipartFormData.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/MultipartUpload.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/NetworkReachabilityManager.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/Notifications.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/OperationQueue+Alamofire.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/ParameterEncoder.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/ParameterEncoding.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/Protected.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/RedirectHandler.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/Request.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/RequestInterceptor.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/RequestTaskMap.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/Response.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/ResponseSerialization.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/Result+Alamofire.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/RetryPolicy.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/ServerTrustEvaluation.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/Session.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/SessionDelegate.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/StringEncoding+Alamofire.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/URLConvertible+URLRequestConvertible.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/URLEncodedFormEncoder.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/URLRequest+Alamofire.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/URLSessionConfiguration+Alamofire.swift create mode 100644 jaem/week7/CatStaGram/Pods/Alamofire/Source/Validation.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/LICENSE create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/README.md create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/DiskStorage.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/ImageCache.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Cache/Storage.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/TVMonogramView+Kingfisher.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Extensions/WKInterfaceImage+Kingfisher.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/ImageSource/Source.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KF.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/Kingfisher.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KingfisherError.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KingfisherManager.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/Filter.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/GraphicsContext.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/Image.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageDrawing.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageFormat.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageProcessor.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageProgressive.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/ImageTransition.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Image/Placeholder.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImageModifier.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/RequestModifier.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Box.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Delegate.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Result.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/Runtime.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Utility/String+MD5.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift create mode 100644 jaem/week7/CatStaGram/Pods/Kingfisher/Sources/Views/Indicator.swift create mode 100644 jaem/week7/CatStaGram/Pods/Manifest.lock create mode 100644 jaem/week7/CatStaGram/Pods/Pods.xcodeproj/project.pbxproj create mode 100644 jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Alamofire.xcscheme create mode 100644 jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Kingfisher.xcscheme create mode 100644 jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/Pods-CatStaGram.xcscheme create mode 100644 jaem/week7/CatStaGram/Pods/Pods.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-Info.plist create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-dummy.m create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-prefix.pch create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire-umbrella.h create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire.debug.xcconfig create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire.modulemap create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Alamofire/Alamofire.release.xcconfig create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-Info.plist create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.markdown create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-acknowledgements.plist create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-dummy.m create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-input-files.xcfilelist create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Debug-output-files.xcfilelist create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-input-files.xcfilelist create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks-Release-output-files.xcfilelist create mode 100755 jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-frameworks.sh create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram-umbrella.h create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.debug.xcconfig create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.modulemap create mode 100644 jaem/week7/CatStaGram/Pods/Target Support Files/Pods-CatStaGram/Pods-CatStaGram.release.xcconfig create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.pbxproj create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM.xcodeproj/xcuserdata/songjaemin.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM/Assets.xcassets/Contents.json create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM/Base.lproj/LaunchScreen.storyboard create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVM/Info.plist create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVMTests/Todo_MVVMTests.swift create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVMUITests/Todo_MVVMUITests.swift create mode 100644 jaem/week8/Todo_MVVM/Todo_MVVMUITests/Todo_MVVMUITestsLaunchTests.swift diff --git a/jaem/week2/CarrotmarketClone/CarrotmarketClone.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate b/jaem/week2/CarrotmarketClone/CarrotmarketClone.xcodeproj/project.xcworkspace/xcuserdata/songjaemin.xcuserdatad/UserInterfaceState.xcuserstate index 698fc0a277b98f6af3e62ad7a835425c256bd042..055faaef92bbe29d241e5aa6ab089cb0e701af85 100644 GIT binary patch delta 19157 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