├── .github └── FUNDING.yml ├── .gitignore ├── .swift-version ├── .travis.yml ├── Cartfile.private ├── Cartfile.resolved ├── LICENSE ├── Package.swift ├── README.md ├── README_KR.md ├── SwiftyGif.podspec ├── SwiftyGif.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ ├── SwiftyGif.xcscheme │ │ ├── SwiftyGifExample.xcscheme │ │ └── SwiftyGifMacExample.xcscheme └── xcuserdata │ ├── alex.xcuserdatad │ └── xcschemes │ │ └── xcschememanagement.plist │ └── travasonig.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── SwiftyGif ├── Info.plist ├── NSImage+SwiftyGif.swift ├── NSImageView+SwiftyGif.swift ├── ObjcAssociatedWeakObject.swift ├── PrivacyInfo.xcprivacy ├── SwiftyGif.h ├── SwiftyGifError.swift ├── SwiftyGifManager.swift ├── UIImage+SwiftyGif.swift └── UIImageView+SwiftyGif.swift ├── SwiftyGifExample ├── 2.gif ├── 3.gif ├── 4.gif ├── 5.gif ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Cell.swift ├── DetailController.swift ├── Info.plist └── ViewController.swift ├── SwiftyGifMacExample ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Info.plist ├── SwiftyGifMacExample.entitlements └── ViewController.swift ├── SwiftyGifTests ├── Images │ ├── 20000x20000.gif │ ├── no_property_dictionary.gif │ └── single_frame_Zt2012.gif ├── Info.plist ├── SwiftyGifTests.swift └── __Snapshots__ │ └── SwiftyGifTests │ ├── testThat15MBGIFCanBeLoaded.1.png │ ├── testThatGIFWithoutkCGImagePropertyGIFDictionaryCanBeLoaded.1.png │ ├── testThatImageViewCanBeRecycled.1.png │ ├── testThatImageViewCanBeRecycledForGIF.2.png │ ├── testThatImageViewCanBeRecycledForNormalImage.1.png │ ├── testThatNonAnimatedGIFCanBeLoaded.1.png │ └── testThatNonAnimatedGIFCanBeLoadedWithUIImage.1.png ├── example.gif └── projec-file-explain.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [kirualex] 4 | custom: ['https://www.paypal.me/alexiscreuzot'] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/xcode,swift,objective-c 3 | 4 | .DS_Store 5 | 6 | ### Xcode ### 7 | # Xcode 8 | # 9 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 10 | 11 | ## Build generated 12 | build/ 13 | DerivedData/ 14 | 15 | ## Various settings 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | xcuserdata/ 25 | 26 | ## Other 27 | *.moved-aside 28 | *.xccheckout 29 | *.xcscmblueprint 30 | 31 | 32 | ### Swift ### 33 | # Xcode 34 | # 35 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 36 | 37 | ## Build generated 38 | build/ 39 | DerivedData/ 40 | 41 | ## Various settings 42 | *.pbxuser 43 | !default.pbxuser 44 | *.mode1v3 45 | !default.mode1v3 46 | *.mode2v3 47 | !default.mode2v3 48 | *.perspectivev3 49 | !default.perspectivev3 50 | xcuserdata/ 51 | 52 | ## Other 53 | *.moved-aside 54 | *.xcuserstate 55 | 56 | ## Obj-C/Swift specific 57 | *.hmap 58 | *.ipa 59 | *.dSYM.zip 60 | *.dSYM 61 | 62 | ## Playgrounds 63 | timeline.xctimeline 64 | playground.xcworkspace 65 | 66 | # Swift Package Manager 67 | # 68 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 69 | # Packages/ 70 | .build/ 71 | 72 | # CocoaPods 73 | # 74 | # We recommend against adding the Pods directory to your .gitignore. However 75 | # you should judge for yourself, the pros and cons are mentioned at: 76 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 77 | # 78 | # Pods/ 79 | 80 | # Carthage 81 | # 82 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 83 | # Carthage/Checkouts 84 | 85 | Carthage/Build 86 | 87 | # fastlane 88 | # 89 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 90 | # screenshots whenever they are needed. 91 | # For more information about the recommended setup visit: 92 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 93 | 94 | fastlane/report.xml 95 | fastlane/Preview.html 96 | fastlane/screenshots 97 | fastlane/test_output 98 | 99 | 100 | ### Objective-C ### 101 | # Xcode 102 | # 103 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 104 | 105 | ## Build generated 106 | build/ 107 | DerivedData/ 108 | 109 | ## Various settings 110 | *.pbxuser 111 | !default.pbxuser 112 | *.mode1v3 113 | !default.mode1v3 114 | *.mode2v3 115 | !default.mode2v3 116 | *.perspectivev3 117 | !default.perspectivev3 118 | xcuserdata/ 119 | 120 | ## Other 121 | *.moved-aside 122 | *.xcuserstate 123 | 124 | ## Obj-C/Swift specific 125 | *.hmap 126 | *.ipa 127 | *.dSYM.zip 128 | *.dSYM 129 | 130 | # CocoaPods 131 | # 132 | # We recommend against adding the Pods directory to your .gitignore. However 133 | # you should judge for yourself, the pros and cons are mentioned at: 134 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 135 | # 136 | # Pods/ 137 | 138 | # Carthage 139 | # 140 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 141 | # Carthage/Checkouts 142 | 143 | Carthage/Build 144 | 145 | # fastlane 146 | # 147 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 148 | # screenshots whenever they are needed. 149 | # For more information about the recommended setup visit: 150 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 151 | 152 | fastlane/report.xml 153 | fastlane/Preview.html 154 | fastlane/screenshots 155 | fastlane/test_output 156 | 157 | # Code Injection 158 | # 159 | # After new code Injection tools there's a generated folder /iOSInjectionProject 160 | # https://github.com/johnno1962/injectionforxcode 161 | 162 | iOSInjectionProject/ 163 | 164 | ### Objective-C Patch ### 165 | *.xcscmblueprint -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode10 3 | 4 | branches: 5 | only: 6 | - master 7 | 8 | script: 9 | - xcodebuild build -project SwiftyGif.xcodeproj -scheme SwiftyGif -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone X,OS=11.3' | xcpretty 10 | -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "pointfreeco/swift-snapshot-testing" ~> 1.5 2 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "pointfreeco/swift-snapshot-testing" "1.5.0" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alexis Creuzot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SwiftyGif", 7 | platforms: [ 8 | .iOS("9.0"), .macOS(.v10_14), 9 | ], 10 | products: [ 11 | .library(name: "SwiftyGif", targets: ["SwiftyGif"]), 12 | .library(name: "SwiftyGif-Dynamic", type: .dynamic, targets: ["SwiftyGif"]), 13 | ], 14 | dependencies: [], 15 | targets: [ 16 | .target( 17 | name: "SwiftyGif", 18 | dependencies: [], 19 | path: "SwiftyGif", 20 | resources: [.copy("PrivacyInfo.xcprivacy")] 21 | ), 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Language](https://img.shields.io/badge/swift-5.0-blue.svg)](http://swift.org) 2 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/SwiftyGif.svg)](https://img.shields.io/cocoapods/v/SwiftyGif.svg) 3 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 4 | [![Build Status](https://travis-ci.org/kirualex/SwiftyGif.svg?branch=master)](https://travis-ci.org/kirualex/SwiftyGif) 5 | [![Pod License](http://img.shields.io/cocoapods/l/SDWebImage.svg?style=flat)](https://raw.githubusercontent.com/kirualex/SwiftyGif/master/LICENSE) 6 | 7 | # SwiftyGif 8 | High performance & easy to use Gif engine 9 | 10 |

11 |
12 | 13 |

14 | 15 | > Language Switch: [한국어](README_KR.md) 16 | 17 | ## Features 18 | - [x] UIImage and UIImageView extension based 19 | - [x] Remote GIFs with customizable loader 20 | - [x] Great CPU/Memory performances 21 | - [x] Control playback 22 | - [x] Allow control of display quality by using 'levelOfIntegrity' 23 | - [x] Allow control CPU/memory tradeoff via 'memoryLimit' 24 | 25 | ## Installation 26 | 27 | ### With CocoaPods 28 | ```ruby 29 | source 'https://github.com/CocoaPods/Specs.git' 30 | use_frameworks! 31 | pod 'SwiftyGif' 32 | ``` 33 | 34 | ### With Carthage 35 | Follow the usual Carthage instructions on how to [add a framework to an application](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application). When adding SwiftyGif among the frameworks listed in `Cartfile`, apply its syntax for [GitHub repositories](https://github.com/Carthage/Carthage/blob/master/Documentation/Artifacts.md#github-repositories): 36 | 37 | ``` 38 | github "kirualex/SwiftyGif" 39 | ``` 40 | 41 | ### With Swift Package Manager 42 | ```ruby 43 | https://github.com/kirualex/SwiftyGif.git 44 | ``` 45 | 46 | ## How to Use 47 | 48 | ### Project files 49 | ![projec-file-explain](projec-file-explain.png) 50 | 51 | As of now, Xcode `xcassets` folders do not recognize `.gif` as images. This means you need to put your `.gif` outside of the assets. I recommend creating a group `gif` for instance. 52 | 53 | ### Quick Start 54 | 55 | SwiftyGif uses familiar `UIImage` and `UIImageView` to display gifs. 56 | 57 | #### Programmaticaly 58 | 59 | ```swift 60 | import SwiftyGif 61 | 62 | do { 63 | let gif = try UIImage(gifName: "MyImage.gif") 64 | let imageview = UIImageView(gifImage: gif, loopCount: 3) // Will loop 3 times 65 | imageview.frame = view.bounds 66 | view.addSubview(imageview) 67 | } catch { 68 | print(error) 69 | } 70 | ``` 71 | 72 | #### Directly from nib/storyboard 73 | 74 | ```swift 75 | @IBOutlet var myImageView : UIImageView! 76 | ... 77 | 78 | let gif = try UIImage(gifName: "MyImage.gif") 79 | self.myImageView.setGifImage(gif, loopCount: -1) // Will loop forever 80 | ``` 81 | 82 | #### Remote GIFs 83 | 84 | ```swift 85 | // You can also set it with an URL pointing to your gif 86 | let url = URL(string: "...") 87 | let loader = UIActivityIndicatorView(style: .white) 88 | cell.gifImageView.setGifFromURL(url, customLoader: loader) 89 | ``` 90 | 91 | 92 | #### SwiftUI 93 | 94 | Add this `UIViewRepresentable` to your code. 95 | 96 | ```swift 97 | struct AnimatedGifView: UIViewRepresentable { 98 | @Binding var url: URL 99 | 100 | func makeUIView(context: Context) -> UIImageView { 101 | let imageView = UIImageView(gifURL: self.url) 102 | imageView.contentMode = .scaleAspectFit 103 | return imageView 104 | } 105 | 106 | func updateUIView(_ uiView: UIImageView, context: Context) { 107 | uiView.setGifFromURL(self.url) 108 | } 109 | } 110 | ``` 111 | 112 | Then to use it: 113 | 114 | ```swift 115 | AnimatedGifView(url: Binding(get: { myModel.gif.url }, set: { _ in })) 116 | ``` 117 | 118 | ### Performances 119 | A `SwiftyGifManager` can hold one or several UIImageView using the same memory pool. This allows you to tune the memory limits to your convenience. If no manager is declared, SwiftyGif will just use the `SwiftyGifManager.defaultManager`. 120 | 121 | #### Level of integrity 122 | Setting a lower level of integrity will allow for frame skipping, lowering both CPU and memory usage. This can be a good option if you need to preview a lot of gifs at the same time. 123 | 124 | ```swift 125 | do { 126 | let gif = try UIImage(gifName: "MyImage.gif", levelOfIntegrity:0.5) 127 | } catch { 128 | print(error) 129 | } 130 | ``` 131 | 132 | ### Controls 133 | SwiftyGif offers various controls on the current `UIImageView` playing your gif file. 134 | 135 | ```swift 136 | self.myImageView.startAnimatingGif() 137 | self.myImageView.stopAnimatingGif() 138 | self.myImageView.showFrameAtIndexDelta(delta: Int) 139 | self.myImageView.showFrameAtIndex(index: Int) 140 | ``` 141 | 142 | To allow easy use of those controls, some utility methods are provided : 143 | 144 | ```swift 145 | self.myImageView.isAnimatingGif() // Returns whether the gif is currently playing 146 | self.myImageView.gifImage!.framesCount() // Returns number of frames for this gif 147 | ``` 148 | 149 | ### Delegate 150 | You can declare a SwiftyGifDelegate to receive updates on the gif lifecycle. 151 | For instance, if you want your controller `MyController` to act as the delegate: 152 | ```swift 153 | override func viewDidLoad() { 154 | super.viewDidLoad() 155 | self.imageView.delegate = self 156 | } 157 | ``` 158 | 159 | Then simply add an extension: 160 | 161 | ```swift 162 | extension MyController : SwiftyGifDelegate { 163 | 164 | func gifURLDidFinish(sender: UIImageView) { 165 | print("gifURLDidFinish") 166 | } 167 | 168 | func gifURLDidFail(sender: UIImageView) { 169 | print("gifURLDidFail") 170 | } 171 | 172 | func gifDidStart(sender: UIImageView) { 173 | print("gifDidStart") 174 | } 175 | 176 | func gifDidLoop(sender: UIImageView) { 177 | print("gifDidLoop") 178 | } 179 | 180 | func gifDidStop(sender: UIImageView) { 181 | print("gifDidStop") 182 | } 183 | } 184 | ``` 185 | 186 | ## Benchmark 187 | ### Display 1 Image 188 | | |CPU Usage(average) |Memory Usage(average) | 189 | |:-------------:|:-----------------:|:-----------------------:| 190 | |FLAnimatedImage|35% |9,5Mb | 191 | |SwiftyGif |2% |18,4Mb | 192 | |SwiftyGif(memoryLimit:10)|34% |9,5Mb | 193 | 194 | ### Display 6 Images 195 | | |CPU Usage(average) |Memory Usage(average) | 196 | |:-------------:|:-----------------:|:-----------------------:| 197 | |FLAnimatedImage|65% |25,1Mb | 198 | |SwiftyGif |22% |105Mb | 199 | |SwiftyGif(memoryLimit:20)|45% |26Mb | 200 | 201 | Measured on an iPhone 6S, iOS 9.3.1 and Xcode 7.3. 202 | -------------------------------------------------------------------------------- /README_KR.md: -------------------------------------------------------------------------------- 1 | [![Language](https://img.shields.io/badge/swift-5.0-blue.svg)](http://swift.org) 2 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/SwiftyGif.svg)](https://img.shields.io/cocoapods/v/SwiftyGif.svg) 3 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 4 | [![Build Status](https://travis-ci.org/kirualex/SwiftyGif.svg?branch=master)](https://travis-ci.org/kirualex/SwiftyGif) 5 | [![Pod License](http://img.shields.io/cocoapods/l/SDWebImage.svg?style=flat)](https://raw.githubusercontent.com/kirualex/SwiftyGif/master/LICENSE) 6 | 7 | # SwiftyGif 8 | 쉽고 빠르게 Gif를 사용할 수 있습니다. 9 | 10 |

11 |
12 | 13 |

14 | 15 | ## 기능 16 | - [x] UIImage와 UIImageView extension을 기반으로 구현했습니다. 17 | - [x] 원격 GIF를 불러올 수 있고, 로딩 바를 설정할 수 있습니다. 18 | - [x] 뛰어난 CPU/메모리 성능을 보입니다. 19 | - [x] 반복 재생 횟수를 조정할 수 있습니다. 20 | - [x] 'levelOfIntegrity' 파라미터를 통해 Integrity 레벨을 조절합니다. 21 | - [x] 'memoryLimit' 파라미터를 통해 CPU/메모리 사용량을 조절합니다. (CPU - 메모리는 tradeoff 관계) 22 | 23 | ## 설치 24 | 25 | ### CocoaPods 사용 26 | ```ruby 27 | source 'https://github.com/CocoaPods/Specs.git' 28 | use_frameworks! 29 | pod 'SwiftyGif' 30 | ``` 31 | 32 | ### Carthage 사용 33 | [애플리케이션에 프레임워크 추가하기](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application)에 나온 일반적인 카르타고 사용법을 사용합니다. 34 | `Cartfile`에 나열된 프레임워크 중에서 SwityGif를 추가할 때, [GitHub repositories](https://github.com/Carthage/Carthage/blob/master/Documentation/Artifacts.md#github-repositories)에 구문을 적용합니다.: 35 | 36 | ``` 37 | github "kirualex/SwiftyGif" 38 | ``` 39 | 40 | ### Swift Package Manager 사용 41 | ```ruby 42 | https://github.com/kirualex/SwiftyGif.git 43 | ``` 44 | 45 | ## 사용법 46 | 47 | ### 프로젝트 파일 48 | ![projec-file-explain](projec-file-explain.png) 49 | 50 | 현재 Xcode `xcassets` 폴더는 `.gif` 파일을 이미지로 인식하지 못합니다. 즉 assets밖에 '.gif' 파일을 위치시켜야 합니다. `gif` 폴더를 만들어 그 안에서 관리하는 것을 추천합니다. 51 | 52 | ### 빠른 시작 53 | 54 | SwiftyGif는 `UIImage`와 `UIImageView` 를 사용합니다. 55 | 56 | #### 코드 57 | 58 | ```swift 59 | import SwiftyGif 60 | 61 | do { 62 | let gif = try UIImage(gifName: "MyImage.gif") 63 | let imageview = UIImageView(gifImage: gif, loopCount: 3) // 3번 반복 64 | imageview.frame = view.bounds 65 | view.addSubview(imageview) 66 | } catch { 67 | print(error) 68 | } 69 | ``` 70 | 71 | #### nib/storyboard 사용 72 | 73 | ```swift 74 | @IBOutlet var myImageView : UIImageView! 75 | ... 76 | 77 | let gif = try UIImage(gifName: "MyImage.gif") 78 | self.myImageView.setGifImage(gif, loopCount: -1) // 무한 반복 79 | ``` 80 | 81 | #### 원격 GIFs 82 | 83 | ```swift 84 | // GIF를 나타내는 URL을 설정합니다 85 | let url = URL(string: "...") 86 | let loader = UIActivityIndicatorView(style: .white) 87 | cell.gifImageView.setGifFromURL(url, customLoader: loader) 88 | ``` 89 | 90 | ### 성능 91 | `SwiftyGifManager`는 같은 메모리 풀은 사용해 하나 이상의 UIImageView를 관리합니다. 이를 사용해 쉽게 메모리 제한을 할 수 있습니다. 따로 매니저를 선언하지 않는다면 `SwiftyGifManager.defaultManager`를 사용합니다. 92 | 93 | #### Integrity 레벨 94 | Integrity 레벨을 낮게 설정하면 프레임을 건너뜁니다. CPU와 메모리 사용량도 낮춥니다. 많은 gif를 한 번에 미리보기 할 때 유용한 옵션입니다. 95 | 96 | ```swift 97 | do { 98 | let gif = try UIImage(gifName: "MyImage.gif", levelOfIntegrity:0.5) 99 | } catch { 100 | print(error) 101 | } 102 | ``` 103 | 104 | ### 제어 105 | SwiftyGif는 `UIImageVIew`에 대해 다양한 제어 기능을 제공합니다. 106 | 107 | ```swift 108 | self.myImageView.startAnimatingGif() 109 | self.myImageView.stopAnimatingGif() 110 | self.myImageView.showFrameAtIndexDelta(delta: Int) 111 | self.myImageView.showFrameAtIndex(index: Int) 112 | ``` 113 | 114 | 더 쉽게 사용하기 위한 메소드들이 제공됩니다. : 115 | 116 | ```swift 117 | self.myImageView.isAnimatingGif() // 현재 gif가 재생중인지 아닌지 118 | self.myImageView.gifImage!.framesCount() // 해당 gif 프레임 수 리턴 119 | ``` 120 | 121 | ### Delegate 122 | SwiftyGifDelegate를 선언해 gif 생명 주기(=Lifecycle)를 받습니다. 예를 들어 `MyController`라는 ViewController를 delegate로 사용합니다.: 123 | 124 | ```swift 125 | override func viewDidLoad() { 126 | super.viewDidLoad() 127 | self.imageView.delegate = self 128 | } 129 | ``` 130 | 131 | extension에 채택해 쉽게 사용할 수 있습니다.: 132 | 133 | ```swift 134 | extension MyController : SwiftyGifDelegate { 135 | 136 | func gifURLDidFinish(sender: UIImageView) { 137 | print("gifURLDidFinish") 138 | } 139 | 140 | func gifURLDidFail(sender: UIImageView) { 141 | print("gifURLDidFail") 142 | } 143 | 144 | func gifDidStart(sender: UIImageView) { 145 | print("gifDidStart") 146 | } 147 | 148 | func gifDidLoop(sender: UIImageView) { 149 | print("gifDidLoop") 150 | } 151 | 152 | func gifDidStop(sender: UIImageView) { 153 | print("gifDidStop") 154 | } 155 | } 156 | ``` 157 | 158 | ## 벤치마크 159 | ### 이미지 1개 160 | | |CPU 사용량(평균) |메모리 사용량(평균) | 161 | |:-------------:|:-----------------:|:-----------------------:| 162 | |FLAnimatedImage|35% |9,5Mb | 163 | |SwiftyGif |2% |18,4Mb | 164 | |SwiftyGif(memoryLimit:10)|34% |9,5Mb | 165 | 166 | ### 이미지 6개 167 | | |CPU 사용량(평균) |메모리 사용량(평균) | 168 | |:-------------:|:-----------------:|:-----------------------:| 169 | |FLAnimatedImage|65% |25,1Mb | 170 | |SwiftyGif |22% |105Mb | 171 | |SwiftyGif(memoryLimit:20)|45% |26Mb | 172 | 173 | 테스트 환경 : iPhone 6S, iOS 9.3.1, Xcode 7.3. 174 | 메모리를 제한하면 CPU 사용량이 늘어나는 것을 확인할 수 있습니다. (trade-off) -------------------------------------------------------------------------------- /SwiftyGif.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'SwiftyGif' 3 | s.version = '5.4.5' 4 | s.summary = 'High performance Gif engine in Swift. Add and control Gif images easily!' 5 | s.homepage = 'https://github.com/kirualex/SwiftyGif' 6 | s.license = { :type => "MIT", :file => "LICENSE" } 7 | s.author = { "Alexis Creuzot" => "alexis.creuzot@gmail.com" } 8 | s.source = { :git => "https://github.com/kirualex/SwiftyGif.git", :tag => s.version.to_s } 9 | s.platform = :ios, '9.0' 10 | s.requires_arc = true 11 | s.source_files = 'SwiftyGif/*{.h,.swift}' 12 | s.resource_bundles = {'SwiftyGif' => ['SwiftyGif/PrivacyInfo.xcprivacy']} 13 | s.swift_version = '5.0' 14 | end 15 | -------------------------------------------------------------------------------- /SwiftyGif.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 230188A124D95CB800EFE1BC /* SwiftyGifManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE121E11CB2A3DD00960D00 /* SwiftyGifManager.swift */; }; 11 | 230188A324D9615700EFE1BC /* NSImageView+SwiftyGif.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230188A224D9614900EFE1BC /* NSImageView+SwiftyGif.swift */; }; 12 | 230188A524D961D800EFE1BC /* NSImage+SwiftyGif.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230188A424D961CD00EFE1BC /* NSImage+SwiftyGif.swift */; }; 13 | 230188C124D964FE00EFE1BC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230188C024D964FE00EFE1BC /* AppDelegate.swift */; }; 14 | 230188C324D964FE00EFE1BC /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230188C224D964FE00EFE1BC /* ViewController.swift */; }; 15 | 230188C524D964FF00EFE1BC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 230188C424D964FF00EFE1BC /* Assets.xcassets */; }; 16 | 230188C824D964FF00EFE1BC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 230188C624D964FF00EFE1BC /* Main.storyboard */; }; 17 | 230188D024D965E800EFE1BC /* libSwiftyGifMac.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2301889824D95CA100EFE1BC /* libSwiftyGifMac.a */; }; 18 | 230188D224D9684100EFE1BC /* 3.gif in Resources */ = {isa = PBXBuildFile; fileRef = FAF19DDC1CC90F9200454324 /* 3.gif */; }; 19 | 230188D324D9684100EFE1BC /* 2.gif in Resources */ = {isa = PBXBuildFile; fileRef = FAC7163A1CCA26AE0018A0CF /* 2.gif */; }; 20 | 230188D424D9684100EFE1BC /* 4.gif in Resources */ = {isa = PBXBuildFile; fileRef = FAC7163B1CCA26AE0018A0CF /* 4.gif */; }; 21 | 230188D524D9684100EFE1BC /* 5.gif in Resources */ = {isa = PBXBuildFile; fileRef = FA57CC951CB3EDAA000F3476 /* 5.gif */; }; 22 | 3B18BAF81E289899009C125A /* SwiftyGif.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B18BAF61E289899009C125A /* SwiftyGif.h */; settings = {ATTRIBUTES = (Public, ); }; }; 23 | 3B18BAFB1E289899009C125A /* SwiftyGif.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B18BAF41E289899009C125A /* SwiftyGif.framework */; }; 24 | 3B18BAFC1E289899009C125A /* SwiftyGif.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B18BAF41E289899009C125A /* SwiftyGif.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 25 | 3B18BB011E2898A1009C125A /* SwiftyGifManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE121E11CB2A3DD00960D00 /* SwiftyGifManager.swift */; }; 26 | 3B18BB021E2898A5009C125A /* UIImage+SwiftyGif.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE121E21CB2A3DD00960D00 /* UIImage+SwiftyGif.swift */; }; 27 | 3B18BB031E2898A9009C125A /* UIImageView+SwiftyGif.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE121E31CB2A3DD00960D00 /* UIImageView+SwiftyGif.swift */; }; 28 | 6F2FA2CF2C5417B600971497 /* SwiftyGifError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F2FA2CE2C5417B600971497 /* SwiftyGifError.swift */; }; 29 | 6F2FA2D02C5417BA00971497 /* SwiftyGifError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F2FA2CE2C5417B600971497 /* SwiftyGifError.swift */; }; 30 | 7A6E2EDC2B2278AE00A3ABF1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7A6E2EDB2B2278AE00A3ABF1 /* PrivacyInfo.xcprivacy */; }; 31 | AD938875276BBDBD00013AB1 /* ObjcAssociatedWeakObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD938874276BBDBD00013AB1 /* ObjcAssociatedWeakObject.swift */; }; 32 | AD938876276BBDBD00013AB1 /* ObjcAssociatedWeakObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD938874276BBDBD00013AB1 /* ObjcAssociatedWeakObject.swift */; }; 33 | EF34CB4D22A51591002A6C92 /* SwiftyGifTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF34CB4C22A51591002A6C92 /* SwiftyGifTests.swift */; }; 34 | EF34CB4F22A51591002A6C92 /* SwiftyGif.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B18BAF41E289899009C125A /* SwiftyGif.framework */; }; 35 | EF34CB5522A524F6002A6C92 /* single_frame_Zt2012.gif in Resources */ = {isa = PBXBuildFile; fileRef = EF26CB8A22A167B100E92383 /* single_frame_Zt2012.gif */; }; 36 | EF34CB5622A524FA002A6C92 /* 20000x20000.gif in Resources */ = {isa = PBXBuildFile; fileRef = EF26CB8C22A16DB900E92383 /* 20000x20000.gif */; }; 37 | EF34CB5722A524FE002A6C92 /* no_property_dictionary.gif in Resources */ = {isa = PBXBuildFile; fileRef = EF26CB8822A166E400E92383 /* no_property_dictionary.gif */; }; 38 | EF34CB5F22A52909002A6C92 /* SnapshotTesting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EF34CB5E22A52909002A6C92 /* SnapshotTesting.framework */; }; 39 | EF34CB6122A52929002A6C92 /* SnapshotTesting.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = EF34CB5E22A52909002A6C92 /* SnapshotTesting.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 40 | EF34CB6322A54271002A6C92 /* 15MB_Einstein_rings_zoom.gif in Resources */ = {isa = PBXBuildFile; fileRef = EF34CB6222A54271002A6C92 /* 15MB_Einstein_rings_zoom.gif */; }; 41 | EF34CB6422A54ABC002A6C92 /* no_property_dictionary.gif in Resources */ = {isa = PBXBuildFile; fileRef = EF26CB8822A166E400E92383 /* no_property_dictionary.gif */; }; 42 | EF34CB6522A54AC0002A6C92 /* single_frame_Zt2012.gif in Resources */ = {isa = PBXBuildFile; fileRef = EF26CB8A22A167B100E92383 /* single_frame_Zt2012.gif */; }; 43 | EF34CB6722A54ACE002A6C92 /* 20000x20000.gif in Resources */ = {isa = PBXBuildFile; fileRef = EF26CB8C22A16DB900E92383 /* 20000x20000.gif */; }; 44 | FA57CC981CB3EDAA000F3476 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA57CC871CB3EDAA000F3476 /* AppDelegate.swift */; }; 45 | FA57CC991CB3EDAA000F3476 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FA57CC881CB3EDAA000F3476 /* Assets.xcassets */; }; 46 | FA57CC9A1CB3EDAA000F3476 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FA57CC8A1CB3EDAA000F3476 /* LaunchScreen.storyboard */; }; 47 | FA57CC9B1CB3EDAA000F3476 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FA57CC8C1CB3EDAA000F3476 /* Main.storyboard */; }; 48 | FA57CC9C1CB3EDAA000F3476 /* Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA57CC8E1CB3EDAA000F3476 /* Cell.swift */; }; 49 | FA57CC9D1CB3EDAA000F3476 /* DetailController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA57CC8F1CB3EDAA000F3476 /* DetailController.swift */; }; 50 | FA57CCA31CB3EDAA000F3476 /* 5.gif in Resources */ = {isa = PBXBuildFile; fileRef = FA57CC951CB3EDAA000F3476 /* 5.gif */; }; 51 | FA57CCA51CB3EDAA000F3476 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA57CC971CB3EDAA000F3476 /* ViewController.swift */; }; 52 | FAC7163C1CCA26AE0018A0CF /* 2.gif in Resources */ = {isa = PBXBuildFile; fileRef = FAC7163A1CCA26AE0018A0CF /* 2.gif */; }; 53 | FAC7163D1CCA26AE0018A0CF /* 4.gif in Resources */ = {isa = PBXBuildFile; fileRef = FAC7163B1CCA26AE0018A0CF /* 4.gif */; }; 54 | FAF19DE11CC90F9200454324 /* 3.gif in Resources */ = {isa = PBXBuildFile; fileRef = FAF19DDC1CC90F9200454324 /* 3.gif */; }; 55 | /* End PBXBuildFile section */ 56 | 57 | /* Begin PBXContainerItemProxy section */ 58 | 230188CE24D965DE00EFE1BC /* PBXContainerItemProxy */ = { 59 | isa = PBXContainerItemProxy; 60 | containerPortal = FA29E92A1CA9340E00E579D5 /* Project object */; 61 | proxyType = 1; 62 | remoteGlobalIDString = 2301889724D95CA100EFE1BC; 63 | remoteInfo = SwiftyGifMac; 64 | }; 65 | 3B18BAF91E289899009C125A /* PBXContainerItemProxy */ = { 66 | isa = PBXContainerItemProxy; 67 | containerPortal = FA29E92A1CA9340E00E579D5 /* Project object */; 68 | proxyType = 1; 69 | remoteGlobalIDString = 3B18BAF31E289899009C125A; 70 | remoteInfo = SwiftyGif; 71 | }; 72 | EF34CB5022A51591002A6C92 /* PBXContainerItemProxy */ = { 73 | isa = PBXContainerItemProxy; 74 | containerPortal = FA29E92A1CA9340E00E579D5 /* Project object */; 75 | proxyType = 1; 76 | remoteGlobalIDString = 3B18BAF31E289899009C125A; 77 | remoteInfo = SwiftyGif; 78 | }; 79 | /* End PBXContainerItemProxy section */ 80 | 81 | /* Begin PBXCopyFilesBuildPhase section */ 82 | 3B18BB001E289899009C125A /* Embed Frameworks */ = { 83 | isa = PBXCopyFilesBuildPhase; 84 | buildActionMask = 2147483647; 85 | dstPath = ""; 86 | dstSubfolderSpec = 10; 87 | files = ( 88 | 3B18BAFC1E289899009C125A /* SwiftyGif.framework in Embed Frameworks */, 89 | ); 90 | name = "Embed Frameworks"; 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | EF34CB6022A52914002A6C92 /* CopyFiles */ = { 94 | isa = PBXCopyFilesBuildPhase; 95 | buildActionMask = 2147483647; 96 | dstPath = ""; 97 | dstSubfolderSpec = 10; 98 | files = ( 99 | EF34CB6122A52929002A6C92 /* SnapshotTesting.framework in CopyFiles */, 100 | ); 101 | runOnlyForDeploymentPostprocessing = 0; 102 | }; 103 | /* End PBXCopyFilesBuildPhase section */ 104 | 105 | /* Begin PBXFileReference section */ 106 | 2301889824D95CA100EFE1BC /* libSwiftyGifMac.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSwiftyGifMac.a; sourceTree = BUILT_PRODUCTS_DIR; }; 107 | 230188A224D9614900EFE1BC /* NSImageView+SwiftyGif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImageView+SwiftyGif.swift"; sourceTree = ""; }; 108 | 230188A424D961CD00EFE1BC /* NSImage+SwiftyGif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+SwiftyGif.swift"; sourceTree = ""; }; 109 | 230188BE24D964FE00EFE1BC /* SwiftyGifMacExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftyGifMacExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 110 | 230188C024D964FE00EFE1BC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 111 | 230188C224D964FE00EFE1BC /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 112 | 230188C424D964FF00EFE1BC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 113 | 230188C724D964FF00EFE1BC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 114 | 230188C924D964FF00EFE1BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 115 | 230188CA24D964FF00EFE1BC /* SwiftyGifMacExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftyGifMacExample.entitlements; sourceTree = ""; }; 116 | 3B18BAF41E289899009C125A /* SwiftyGif.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftyGif.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 117 | 3B18BAF61E289899009C125A /* SwiftyGif.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftyGif.h; sourceTree = ""; }; 118 | 3B18BAF71E289899009C125A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 119 | 6F2FA2CE2C5417B600971497 /* SwiftyGifError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyGifError.swift; sourceTree = ""; }; 120 | 7A6E2EDB2B2278AE00A3ABF1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 121 | AD938874276BBDBD00013AB1 /* ObjcAssociatedWeakObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcAssociatedWeakObject.swift; sourceTree = ""; }; 122 | EF26CB8822A166E400E92383 /* no_property_dictionary.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = no_property_dictionary.gif; sourceTree = ""; }; 123 | EF26CB8A22A167B100E92383 /* single_frame_Zt2012.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = single_frame_Zt2012.gif; sourceTree = ""; }; 124 | EF26CB8C22A16DB900E92383 /* 20000x20000.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = 20000x20000.gif; sourceTree = ""; }; 125 | EF34CB4A22A51591002A6C92 /* SwiftyGifTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftyGifTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 126 | EF34CB4C22A51591002A6C92 /* SwiftyGifTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyGifTests.swift; sourceTree = ""; }; 127 | EF34CB4E22A51591002A6C92 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 128 | EF34CB5922A52876002A6C92 /* Cartfile.resolved */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Cartfile.resolved; sourceTree = SOURCE_ROOT; }; 129 | EF34CB5A22A52876002A6C92 /* Cartfile.private */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Cartfile.private; sourceTree = SOURCE_ROOT; }; 130 | EF34CB5E22A52909002A6C92 /* SnapshotTesting.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SnapshotTesting.framework; path = Carthage/Build/iOS/SnapshotTesting.framework; sourceTree = ""; }; 131 | EF34CB6222A54271002A6C92 /* 15MB_Einstein_rings_zoom.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; name = 15MB_Einstein_rings_zoom.gif; path = ../../../../../Downloads/15MB_Einstein_rings_zoom.gif; sourceTree = ""; }; 132 | FA29E9321CA9340E00E579D5 /* SwiftyGifExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftyGifExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 133 | FA29E94A1CA9340F00E579D5 /* SwiftyGifTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyGifTests.swift; sourceTree = ""; }; 134 | FA29E94C1CA9340F00E579D5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 135 | FA57CC871CB3EDAA000F3476 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 136 | FA57CC881CB3EDAA000F3476 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 137 | FA57CC8B1CB3EDAA000F3476 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = LaunchScreen.storyboard; sourceTree = ""; }; 138 | FA57CC8D1CB3EDAA000F3476 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Main.storyboard; sourceTree = ""; }; 139 | FA57CC8E1CB3EDAA000F3476 /* Cell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cell.swift; sourceTree = ""; }; 140 | FA57CC8F1CB3EDAA000F3476 /* DetailController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailController.swift; sourceTree = ""; }; 141 | FA57CC951CB3EDAA000F3476 /* 5.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = 5.gif; sourceTree = ""; }; 142 | FA57CC961CB3EDAA000F3476 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 143 | FA57CC971CB3EDAA000F3476 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 144 | FAC7163A1CCA26AE0018A0CF /* 2.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = 2.gif; sourceTree = ""; }; 145 | FAC7163B1CCA26AE0018A0CF /* 4.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = 4.gif; sourceTree = ""; }; 146 | FAE121E11CB2A3DD00960D00 /* SwiftyGifManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyGifManager.swift; sourceTree = ""; }; 147 | FAE121E21CB2A3DD00960D00 /* UIImage+SwiftyGif.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+SwiftyGif.swift"; sourceTree = ""; }; 148 | FAE121E31CB2A3DD00960D00 /* UIImageView+SwiftyGif.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImageView+SwiftyGif.swift"; sourceTree = ""; }; 149 | FAF19DDC1CC90F9200454324 /* 3.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = 3.gif; sourceTree = ""; }; 150 | /* End PBXFileReference section */ 151 | 152 | /* Begin PBXFrameworksBuildPhase section */ 153 | 2301889624D95CA100EFE1BC /* Frameworks */ = { 154 | isa = PBXFrameworksBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | ); 158 | runOnlyForDeploymentPostprocessing = 0; 159 | }; 160 | 230188BB24D964FE00EFE1BC /* Frameworks */ = { 161 | isa = PBXFrameworksBuildPhase; 162 | buildActionMask = 2147483647; 163 | files = ( 164 | 230188D024D965E800EFE1BC /* libSwiftyGifMac.a in Frameworks */, 165 | ); 166 | runOnlyForDeploymentPostprocessing = 0; 167 | }; 168 | 3B18BAF01E289899009C125A /* Frameworks */ = { 169 | isa = PBXFrameworksBuildPhase; 170 | buildActionMask = 2147483647; 171 | files = ( 172 | ); 173 | runOnlyForDeploymentPostprocessing = 0; 174 | }; 175 | EF34CB4722A51591002A6C92 /* Frameworks */ = { 176 | isa = PBXFrameworksBuildPhase; 177 | buildActionMask = 2147483647; 178 | files = ( 179 | EF34CB5F22A52909002A6C92 /* SnapshotTesting.framework in Frameworks */, 180 | EF34CB4F22A51591002A6C92 /* SwiftyGif.framework in Frameworks */, 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | FA29E92F1CA9340E00E579D5 /* Frameworks */ = { 185 | isa = PBXFrameworksBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | 3B18BAFB1E289899009C125A /* SwiftyGif.framework in Frameworks */, 189 | ); 190 | runOnlyForDeploymentPostprocessing = 0; 191 | }; 192 | /* End PBXFrameworksBuildPhase section */ 193 | 194 | /* Begin PBXGroup section */ 195 | 230188BF24D964FE00EFE1BC /* SwiftyGifMacExample */ = { 196 | isa = PBXGroup; 197 | children = ( 198 | 230188C024D964FE00EFE1BC /* AppDelegate.swift */, 199 | 230188C224D964FE00EFE1BC /* ViewController.swift */, 200 | 230188C424D964FF00EFE1BC /* Assets.xcassets */, 201 | 230188C624D964FF00EFE1BC /* Main.storyboard */, 202 | 230188C924D964FF00EFE1BC /* Info.plist */, 203 | 230188CA24D964FF00EFE1BC /* SwiftyGifMacExample.entitlements */, 204 | ); 205 | path = SwiftyGifMacExample; 206 | sourceTree = ""; 207 | }; 208 | 3B18BAF51E289899009C125A /* SwiftyGif */ = { 209 | isa = PBXGroup; 210 | children = ( 211 | 3B18BAF61E289899009C125A /* SwiftyGif.h */, 212 | 3B18BAF71E289899009C125A /* Info.plist */, 213 | ); 214 | path = SwiftyGif; 215 | sourceTree = ""; 216 | }; 217 | EF34CB4422A514E7002A6C92 /* Images */ = { 218 | isa = PBXGroup; 219 | children = ( 220 | EF34CB6222A54271002A6C92 /* 15MB_Einstein_rings_zoom.gif */, 221 | EF26CB8822A166E400E92383 /* no_property_dictionary.gif */, 222 | EF26CB8A22A167B100E92383 /* single_frame_Zt2012.gif */, 223 | EF26CB8C22A16DB900E92383 /* 20000x20000.gif */, 224 | ); 225 | path = Images; 226 | sourceTree = ""; 227 | }; 228 | EF34CB4B22A51591002A6C92 /* SwiftyGifTests */ = { 229 | isa = PBXGroup; 230 | children = ( 231 | EF34CB4C22A51591002A6C92 /* SwiftyGifTests.swift */, 232 | EF34CB4E22A51591002A6C92 /* Info.plist */, 233 | ); 234 | path = SwiftyGifTests; 235 | sourceTree = ""; 236 | }; 237 | EF34CB5D22A52909002A6C92 /* Frameworks */ = { 238 | isa = PBXGroup; 239 | children = ( 240 | EF34CB5E22A52909002A6C92 /* SnapshotTesting.framework */, 241 | ); 242 | name = Frameworks; 243 | sourceTree = ""; 244 | }; 245 | FA29E9291CA9340E00E579D5 = { 246 | isa = PBXGroup; 247 | children = ( 248 | FA29E9551CA9342F00E579D5 /* SwiftyGif */, 249 | FA29E9341CA9340E00E579D5 /* SwiftyGifExample */, 250 | 230188BF24D964FE00EFE1BC /* SwiftyGifMacExample */, 251 | FA29E9491CA9340F00E579D5 /* SwiftyGifTests */, 252 | 3B18BAF51E289899009C125A /* SwiftyGif */, 253 | EF34CB4B22A51591002A6C92 /* SwiftyGifTests */, 254 | FA29E9331CA9340E00E579D5 /* Products */, 255 | EF34CB5D22A52909002A6C92 /* Frameworks */, 256 | ); 257 | sourceTree = ""; 258 | }; 259 | FA29E9331CA9340E00E579D5 /* Products */ = { 260 | isa = PBXGroup; 261 | children = ( 262 | FA29E9321CA9340E00E579D5 /* SwiftyGifExample.app */, 263 | 3B18BAF41E289899009C125A /* SwiftyGif.framework */, 264 | EF34CB4A22A51591002A6C92 /* SwiftyGifTests.xctest */, 265 | 2301889824D95CA100EFE1BC /* libSwiftyGifMac.a */, 266 | 230188BE24D964FE00EFE1BC /* SwiftyGifMacExample.app */, 267 | ); 268 | name = Products; 269 | sourceTree = ""; 270 | }; 271 | FA29E9341CA9340E00E579D5 /* SwiftyGifExample */ = { 272 | isa = PBXGroup; 273 | children = ( 274 | FA57CC871CB3EDAA000F3476 /* AppDelegate.swift */, 275 | FA57CC881CB3EDAA000F3476 /* Assets.xcassets */, 276 | FA57CC891CB3EDAA000F3476 /* Base.lproj */, 277 | FA57CC8E1CB3EDAA000F3476 /* Cell.swift */, 278 | FA57CC8F1CB3EDAA000F3476 /* DetailController.swift */, 279 | FA57CC971CB3EDAA000F3476 /* ViewController.swift */, 280 | FAF19DDC1CC90F9200454324 /* 3.gif */, 281 | FAC7163A1CCA26AE0018A0CF /* 2.gif */, 282 | FAC7163B1CCA26AE0018A0CF /* 4.gif */, 283 | FA57CC951CB3EDAA000F3476 /* 5.gif */, 284 | FA57CC961CB3EDAA000F3476 /* Info.plist */, 285 | ); 286 | path = SwiftyGifExample; 287 | sourceTree = ""; 288 | }; 289 | FA29E9491CA9340F00E579D5 /* SwiftyGifTests */ = { 290 | isa = PBXGroup; 291 | children = ( 292 | EF34CB5A22A52876002A6C92 /* Cartfile.private */, 293 | EF34CB5922A52876002A6C92 /* Cartfile.resolved */, 294 | EF34CB4422A514E7002A6C92 /* Images */, 295 | FA29E94A1CA9340F00E579D5 /* SwiftyGifTests.swift */, 296 | FA29E94C1CA9340F00E579D5 /* Info.plist */, 297 | ); 298 | path = SwiftyGifTests; 299 | sourceTree = ""; 300 | }; 301 | FA29E9551CA9342F00E579D5 /* SwiftyGif */ = { 302 | isa = PBXGroup; 303 | children = ( 304 | FAE121E11CB2A3DD00960D00 /* SwiftyGifManager.swift */, 305 | FAE121E21CB2A3DD00960D00 /* UIImage+SwiftyGif.swift */, 306 | FAE121E31CB2A3DD00960D00 /* UIImageView+SwiftyGif.swift */, 307 | 230188A424D961CD00EFE1BC /* NSImage+SwiftyGif.swift */, 308 | 230188A224D9614900EFE1BC /* NSImageView+SwiftyGif.swift */, 309 | AD938874276BBDBD00013AB1 /* ObjcAssociatedWeakObject.swift */, 310 | 7A6E2EDB2B2278AE00A3ABF1 /* PrivacyInfo.xcprivacy */, 311 | 6F2FA2CE2C5417B600971497 /* SwiftyGifError.swift */, 312 | ); 313 | path = SwiftyGif; 314 | sourceTree = ""; 315 | }; 316 | FA57CC891CB3EDAA000F3476 /* Base.lproj */ = { 317 | isa = PBXGroup; 318 | children = ( 319 | FA57CC8A1CB3EDAA000F3476 /* LaunchScreen.storyboard */, 320 | FA57CC8C1CB3EDAA000F3476 /* Main.storyboard */, 321 | ); 322 | path = Base.lproj; 323 | sourceTree = ""; 324 | }; 325 | /* End PBXGroup section */ 326 | 327 | /* Begin PBXHeadersBuildPhase section */ 328 | 2301889424D95CA100EFE1BC /* Headers */ = { 329 | isa = PBXHeadersBuildPhase; 330 | buildActionMask = 2147483647; 331 | files = ( 332 | ); 333 | runOnlyForDeploymentPostprocessing = 0; 334 | }; 335 | 3B18BAF11E289899009C125A /* Headers */ = { 336 | isa = PBXHeadersBuildPhase; 337 | buildActionMask = 2147483647; 338 | files = ( 339 | 3B18BAF81E289899009C125A /* SwiftyGif.h in Headers */, 340 | ); 341 | runOnlyForDeploymentPostprocessing = 0; 342 | }; 343 | /* End PBXHeadersBuildPhase section */ 344 | 345 | /* Begin PBXNativeTarget section */ 346 | 2301889724D95CA100EFE1BC /* SwiftyGifMac */ = { 347 | isa = PBXNativeTarget; 348 | buildConfigurationList = 230188A024D95CA100EFE1BC /* Build configuration list for PBXNativeTarget "SwiftyGifMac" */; 349 | buildPhases = ( 350 | 2301889424D95CA100EFE1BC /* Headers */, 351 | 2301889524D95CA100EFE1BC /* Sources */, 352 | 2301889624D95CA100EFE1BC /* Frameworks */, 353 | ); 354 | buildRules = ( 355 | ); 356 | dependencies = ( 357 | ); 358 | name = SwiftyGifMac; 359 | productName = SwiftyGifMac; 360 | productReference = 2301889824D95CA100EFE1BC /* libSwiftyGifMac.a */; 361 | productType = "com.apple.product-type.library.static"; 362 | }; 363 | 230188BD24D964FE00EFE1BC /* SwiftyGifMacExample */ = { 364 | isa = PBXNativeTarget; 365 | buildConfigurationList = 230188CB24D964FF00EFE1BC /* Build configuration list for PBXNativeTarget "SwiftyGifMacExample" */; 366 | buildPhases = ( 367 | 230188BA24D964FE00EFE1BC /* Sources */, 368 | 230188BB24D964FE00EFE1BC /* Frameworks */, 369 | 230188BC24D964FE00EFE1BC /* Resources */, 370 | ); 371 | buildRules = ( 372 | ); 373 | dependencies = ( 374 | 230188CF24D965DE00EFE1BC /* PBXTargetDependency */, 375 | ); 376 | name = SwiftyGifMacExample; 377 | productName = SwiftyGifMacExample; 378 | productReference = 230188BE24D964FE00EFE1BC /* SwiftyGifMacExample.app */; 379 | productType = "com.apple.product-type.application"; 380 | }; 381 | 3B18BAF31E289899009C125A /* SwiftyGif */ = { 382 | isa = PBXNativeTarget; 383 | buildConfigurationList = 3B18BAFD1E289899009C125A /* Build configuration list for PBXNativeTarget "SwiftyGif" */; 384 | buildPhases = ( 385 | 3B18BAEF1E289899009C125A /* Sources */, 386 | 3B18BAF01E289899009C125A /* Frameworks */, 387 | 3B18BAF11E289899009C125A /* Headers */, 388 | 3B18BAF21E289899009C125A /* Resources */, 389 | ); 390 | buildRules = ( 391 | ); 392 | dependencies = ( 393 | ); 394 | name = SwiftyGif; 395 | productName = SwiftyGif; 396 | productReference = 3B18BAF41E289899009C125A /* SwiftyGif.framework */; 397 | productType = "com.apple.product-type.framework"; 398 | }; 399 | EF34CB4922A51591002A6C92 /* SwiftyGifTests */ = { 400 | isa = PBXNativeTarget; 401 | buildConfigurationList = EF34CB5222A51591002A6C92 /* Build configuration list for PBXNativeTarget "SwiftyGifTests" */; 402 | buildPhases = ( 403 | EF34CB4622A51591002A6C92 /* Sources */, 404 | EF34CB4722A51591002A6C92 /* Frameworks */, 405 | EF34CB4822A51591002A6C92 /* Resources */, 406 | EF34CB6022A52914002A6C92 /* CopyFiles */, 407 | ); 408 | buildRules = ( 409 | ); 410 | dependencies = ( 411 | EF34CB5122A51591002A6C92 /* PBXTargetDependency */, 412 | ); 413 | name = SwiftyGifTests; 414 | productName = SwiftyGifTests; 415 | productReference = EF34CB4A22A51591002A6C92 /* SwiftyGifTests.xctest */; 416 | productType = "com.apple.product-type.bundle.unit-test"; 417 | }; 418 | FA29E9311CA9340E00E579D5 /* SwiftyGifExample */ = { 419 | isa = PBXNativeTarget; 420 | buildConfigurationList = FA29E94F1CA9340F00E579D5 /* Build configuration list for PBXNativeTarget "SwiftyGifExample" */; 421 | buildPhases = ( 422 | FA29E92E1CA9340E00E579D5 /* Sources */, 423 | FA29E92F1CA9340E00E579D5 /* Frameworks */, 424 | FA29E9301CA9340E00E579D5 /* Resources */, 425 | 3B18BB001E289899009C125A /* Embed Frameworks */, 426 | ); 427 | buildRules = ( 428 | ); 429 | dependencies = ( 430 | 3B18BAFA1E289899009C125A /* PBXTargetDependency */, 431 | ); 432 | name = SwiftyGifExample; 433 | productName = SwiftyGif; 434 | productReference = FA29E9321CA9340E00E579D5 /* SwiftyGifExample.app */; 435 | productType = "com.apple.product-type.application"; 436 | }; 437 | /* End PBXNativeTarget section */ 438 | 439 | /* Begin PBXProject section */ 440 | FA29E92A1CA9340E00E579D5 /* Project object */ = { 441 | isa = PBXProject; 442 | attributes = { 443 | LastSwiftUpdateCheck = 1150; 444 | LastUpgradeCheck = 1410; 445 | ORGANIZATIONNAME = alexiscreuzot; 446 | TargetAttributes = { 447 | 2301889724D95CA100EFE1BC = { 448 | CreatedOnToolsVersion = 11.5; 449 | DevelopmentTeam = 57W7KG4A63; 450 | ProvisioningStyle = Automatic; 451 | }; 452 | 230188BD24D964FE00EFE1BC = { 453 | CreatedOnToolsVersion = 11.5; 454 | DevelopmentTeam = 57W7KG4A63; 455 | ProvisioningStyle = Automatic; 456 | }; 457 | 3B18BAF31E289899009C125A = { 458 | CreatedOnToolsVersion = 8.2.1; 459 | LastSwiftMigration = 1020; 460 | ProvisioningStyle = Automatic; 461 | }; 462 | EF34CB4922A51591002A6C92 = { 463 | CreatedOnToolsVersion = 10.2.1; 464 | ProvisioningStyle = Automatic; 465 | }; 466 | FA29E9311CA9340E00E579D5 = { 467 | CreatedOnToolsVersion = 7.3; 468 | LastSwiftMigration = 1020; 469 | }; 470 | }; 471 | }; 472 | buildConfigurationList = FA29E92D1CA9340E00E579D5 /* Build configuration list for PBXProject "SwiftyGif" */; 473 | compatibilityVersion = "Xcode 3.2"; 474 | developmentRegion = en; 475 | hasScannedForEncodings = 0; 476 | knownRegions = ( 477 | en, 478 | Base, 479 | ); 480 | mainGroup = FA29E9291CA9340E00E579D5; 481 | productRefGroup = FA29E9331CA9340E00E579D5 /* Products */; 482 | projectDirPath = ""; 483 | projectRoot = ""; 484 | targets = ( 485 | FA29E9311CA9340E00E579D5 /* SwiftyGifExample */, 486 | 3B18BAF31E289899009C125A /* SwiftyGif */, 487 | EF34CB4922A51591002A6C92 /* SwiftyGifTests */, 488 | 2301889724D95CA100EFE1BC /* SwiftyGifMac */, 489 | 230188BD24D964FE00EFE1BC /* SwiftyGifMacExample */, 490 | ); 491 | }; 492 | /* End PBXProject section */ 493 | 494 | /* Begin PBXResourcesBuildPhase section */ 495 | 230188BC24D964FE00EFE1BC /* Resources */ = { 496 | isa = PBXResourcesBuildPhase; 497 | buildActionMask = 2147483647; 498 | files = ( 499 | 230188D224D9684100EFE1BC /* 3.gif in Resources */, 500 | 230188D324D9684100EFE1BC /* 2.gif in Resources */, 501 | 230188D524D9684100EFE1BC /* 5.gif in Resources */, 502 | 230188D424D9684100EFE1BC /* 4.gif in Resources */, 503 | 230188C524D964FF00EFE1BC /* Assets.xcassets in Resources */, 504 | 230188C824D964FF00EFE1BC /* Main.storyboard in Resources */, 505 | ); 506 | runOnlyForDeploymentPostprocessing = 0; 507 | }; 508 | 3B18BAF21E289899009C125A /* Resources */ = { 509 | isa = PBXResourcesBuildPhase; 510 | buildActionMask = 2147483647; 511 | files = ( 512 | 7A6E2EDC2B2278AE00A3ABF1 /* PrivacyInfo.xcprivacy in Resources */, 513 | ); 514 | runOnlyForDeploymentPostprocessing = 0; 515 | }; 516 | EF34CB4822A51591002A6C92 /* Resources */ = { 517 | isa = PBXResourcesBuildPhase; 518 | buildActionMask = 2147483647; 519 | files = ( 520 | EF34CB5522A524F6002A6C92 /* single_frame_Zt2012.gif in Resources */, 521 | EF34CB5622A524FA002A6C92 /* 20000x20000.gif in Resources */, 522 | EF34CB5722A524FE002A6C92 /* no_property_dictionary.gif in Resources */, 523 | EF34CB6322A54271002A6C92 /* 15MB_Einstein_rings_zoom.gif in Resources */, 524 | ); 525 | runOnlyForDeploymentPostprocessing = 0; 526 | }; 527 | FA29E9301CA9340E00E579D5 /* Resources */ = { 528 | isa = PBXResourcesBuildPhase; 529 | buildActionMask = 2147483647; 530 | files = ( 531 | EF34CB6522A54AC0002A6C92 /* single_frame_Zt2012.gif in Resources */, 532 | FA57CC9B1CB3EDAA000F3476 /* Main.storyboard in Resources */, 533 | FA57CC991CB3EDAA000F3476 /* Assets.xcassets in Resources */, 534 | FAC7163C1CCA26AE0018A0CF /* 2.gif in Resources */, 535 | FAF19DE11CC90F9200454324 /* 3.gif in Resources */, 536 | EF34CB6722A54ACE002A6C92 /* 20000x20000.gif in Resources */, 537 | FA57CCA31CB3EDAA000F3476 /* 5.gif in Resources */, 538 | FAC7163D1CCA26AE0018A0CF /* 4.gif in Resources */, 539 | FA57CC9A1CB3EDAA000F3476 /* LaunchScreen.storyboard in Resources */, 540 | EF34CB6422A54ABC002A6C92 /* no_property_dictionary.gif in Resources */, 541 | ); 542 | runOnlyForDeploymentPostprocessing = 0; 543 | }; 544 | /* End PBXResourcesBuildPhase section */ 545 | 546 | /* Begin PBXSourcesBuildPhase section */ 547 | 2301889524D95CA100EFE1BC /* Sources */ = { 548 | isa = PBXSourcesBuildPhase; 549 | buildActionMask = 2147483647; 550 | files = ( 551 | 230188A124D95CB800EFE1BC /* SwiftyGifManager.swift in Sources */, 552 | 230188A524D961D800EFE1BC /* NSImage+SwiftyGif.swift in Sources */, 553 | AD938876276BBDBD00013AB1 /* ObjcAssociatedWeakObject.swift in Sources */, 554 | 230188A324D9615700EFE1BC /* NSImageView+SwiftyGif.swift in Sources */, 555 | 6F2FA2D02C5417BA00971497 /* SwiftyGifError.swift in Sources */, 556 | ); 557 | runOnlyForDeploymentPostprocessing = 0; 558 | }; 559 | 230188BA24D964FE00EFE1BC /* Sources */ = { 560 | isa = PBXSourcesBuildPhase; 561 | buildActionMask = 2147483647; 562 | files = ( 563 | 230188C324D964FE00EFE1BC /* ViewController.swift in Sources */, 564 | 230188C124D964FE00EFE1BC /* AppDelegate.swift in Sources */, 565 | ); 566 | runOnlyForDeploymentPostprocessing = 0; 567 | }; 568 | 3B18BAEF1E289899009C125A /* Sources */ = { 569 | isa = PBXSourcesBuildPhase; 570 | buildActionMask = 2147483647; 571 | files = ( 572 | 3B18BB021E2898A5009C125A /* UIImage+SwiftyGif.swift in Sources */, 573 | 3B18BB031E2898A9009C125A /* UIImageView+SwiftyGif.swift in Sources */, 574 | AD938875276BBDBD00013AB1 /* ObjcAssociatedWeakObject.swift in Sources */, 575 | 3B18BB011E2898A1009C125A /* SwiftyGifManager.swift in Sources */, 576 | 6F2FA2CF2C5417B600971497 /* SwiftyGifError.swift in Sources */, 577 | ); 578 | runOnlyForDeploymentPostprocessing = 0; 579 | }; 580 | EF34CB4622A51591002A6C92 /* Sources */ = { 581 | isa = PBXSourcesBuildPhase; 582 | buildActionMask = 2147483647; 583 | files = ( 584 | EF34CB4D22A51591002A6C92 /* SwiftyGifTests.swift in Sources */, 585 | ); 586 | runOnlyForDeploymentPostprocessing = 0; 587 | }; 588 | FA29E92E1CA9340E00E579D5 /* Sources */ = { 589 | isa = PBXSourcesBuildPhase; 590 | buildActionMask = 2147483647; 591 | files = ( 592 | FA57CC981CB3EDAA000F3476 /* AppDelegate.swift in Sources */, 593 | FA57CC9C1CB3EDAA000F3476 /* Cell.swift in Sources */, 594 | FA57CCA51CB3EDAA000F3476 /* ViewController.swift in Sources */, 595 | FA57CC9D1CB3EDAA000F3476 /* DetailController.swift in Sources */, 596 | ); 597 | runOnlyForDeploymentPostprocessing = 0; 598 | }; 599 | /* End PBXSourcesBuildPhase section */ 600 | 601 | /* Begin PBXTargetDependency section */ 602 | 230188CF24D965DE00EFE1BC /* PBXTargetDependency */ = { 603 | isa = PBXTargetDependency; 604 | target = 2301889724D95CA100EFE1BC /* SwiftyGifMac */; 605 | targetProxy = 230188CE24D965DE00EFE1BC /* PBXContainerItemProxy */; 606 | }; 607 | 3B18BAFA1E289899009C125A /* PBXTargetDependency */ = { 608 | isa = PBXTargetDependency; 609 | target = 3B18BAF31E289899009C125A /* SwiftyGif */; 610 | targetProxy = 3B18BAF91E289899009C125A /* PBXContainerItemProxy */; 611 | }; 612 | EF34CB5122A51591002A6C92 /* PBXTargetDependency */ = { 613 | isa = PBXTargetDependency; 614 | target = 3B18BAF31E289899009C125A /* SwiftyGif */; 615 | targetProxy = EF34CB5022A51591002A6C92 /* PBXContainerItemProxy */; 616 | }; 617 | /* End PBXTargetDependency section */ 618 | 619 | /* Begin PBXVariantGroup section */ 620 | 230188C624D964FF00EFE1BC /* Main.storyboard */ = { 621 | isa = PBXVariantGroup; 622 | children = ( 623 | 230188C724D964FF00EFE1BC /* Base */, 624 | ); 625 | name = Main.storyboard; 626 | sourceTree = ""; 627 | }; 628 | FA57CC8A1CB3EDAA000F3476 /* LaunchScreen.storyboard */ = { 629 | isa = PBXVariantGroup; 630 | children = ( 631 | FA57CC8B1CB3EDAA000F3476 /* Base */, 632 | ); 633 | name = LaunchScreen.storyboard; 634 | sourceTree = ""; 635 | }; 636 | FA57CC8C1CB3EDAA000F3476 /* Main.storyboard */ = { 637 | isa = PBXVariantGroup; 638 | children = ( 639 | FA57CC8D1CB3EDAA000F3476 /* Base */, 640 | ); 641 | name = Main.storyboard; 642 | sourceTree = ""; 643 | }; 644 | /* End PBXVariantGroup section */ 645 | 646 | /* Begin XCBuildConfiguration section */ 647 | 2301889E24D95CA100EFE1BC /* Debug */ = { 648 | isa = XCBuildConfiguration; 649 | buildSettings = { 650 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 651 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 652 | CLANG_ENABLE_OBJC_WEAK = YES; 653 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 654 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 655 | CODE_SIGN_STYLE = Automatic; 656 | DEAD_CODE_STRIPPING = YES; 657 | DEVELOPMENT_TEAM = 57W7KG4A63; 658 | EXECUTABLE_PREFIX = lib; 659 | GCC_C_LANGUAGE_STANDARD = gnu11; 660 | MACOSX_DEPLOYMENT_TARGET = 10.15; 661 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 662 | MTL_FAST_MATH = YES; 663 | PRODUCT_MODULE_NAME = SwiftyGif; 664 | PRODUCT_NAME = "$(TARGET_NAME)"; 665 | SDKROOT = macosx; 666 | SKIP_INSTALL = YES; 667 | }; 668 | name = Debug; 669 | }; 670 | 2301889F24D95CA100EFE1BC /* Release */ = { 671 | isa = XCBuildConfiguration; 672 | buildSettings = { 673 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 674 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 675 | CLANG_ENABLE_OBJC_WEAK = YES; 676 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 677 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 678 | CODE_SIGN_STYLE = Automatic; 679 | DEAD_CODE_STRIPPING = YES; 680 | DEVELOPMENT_TEAM = 57W7KG4A63; 681 | EXECUTABLE_PREFIX = lib; 682 | GCC_C_LANGUAGE_STANDARD = gnu11; 683 | MACOSX_DEPLOYMENT_TARGET = 10.15; 684 | MTL_FAST_MATH = YES; 685 | PRODUCT_MODULE_NAME = SwiftyGif; 686 | PRODUCT_NAME = "$(TARGET_NAME)"; 687 | SDKROOT = macosx; 688 | SKIP_INSTALL = YES; 689 | }; 690 | name = Release; 691 | }; 692 | 230188CC24D964FF00EFE1BC /* Debug */ = { 693 | isa = XCBuildConfiguration; 694 | buildSettings = { 695 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 696 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 697 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 698 | CLANG_ENABLE_OBJC_WEAK = YES; 699 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 700 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 701 | CODE_SIGN_ENTITLEMENTS = SwiftyGifMacExample/SwiftyGifMacExample.entitlements; 702 | CODE_SIGN_IDENTITY = "-"; 703 | CODE_SIGN_STYLE = Automatic; 704 | COMBINE_HIDPI_IMAGES = YES; 705 | DEAD_CODE_STRIPPING = YES; 706 | DEVELOPMENT_TEAM = 57W7KG4A63; 707 | ENABLE_HARDENED_RUNTIME = YES; 708 | GCC_C_LANGUAGE_STANDARD = gnu11; 709 | INFOPLIST_FILE = SwiftyGifMacExample/Info.plist; 710 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 711 | MACOSX_DEPLOYMENT_TARGET = 10.15; 712 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 713 | MTL_FAST_MATH = YES; 714 | PRODUCT_BUNDLE_IDENTIFIER = me.carlorapisarda.SwiftyGifMacExample; 715 | PRODUCT_NAME = "$(TARGET_NAME)"; 716 | SDKROOT = macosx; 717 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 718 | SWIFT_VERSION = 5.0; 719 | }; 720 | name = Debug; 721 | }; 722 | 230188CD24D964FF00EFE1BC /* Release */ = { 723 | isa = XCBuildConfiguration; 724 | buildSettings = { 725 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 726 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 727 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 728 | CLANG_ENABLE_OBJC_WEAK = YES; 729 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 730 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 731 | CODE_SIGN_ENTITLEMENTS = SwiftyGifMacExample/SwiftyGifMacExample.entitlements; 732 | CODE_SIGN_IDENTITY = "-"; 733 | CODE_SIGN_STYLE = Automatic; 734 | COMBINE_HIDPI_IMAGES = YES; 735 | DEAD_CODE_STRIPPING = YES; 736 | DEVELOPMENT_TEAM = 57W7KG4A63; 737 | ENABLE_HARDENED_RUNTIME = YES; 738 | GCC_C_LANGUAGE_STANDARD = gnu11; 739 | INFOPLIST_FILE = SwiftyGifMacExample/Info.plist; 740 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 741 | MACOSX_DEPLOYMENT_TARGET = 10.15; 742 | MTL_FAST_MATH = YES; 743 | PRODUCT_BUNDLE_IDENTIFIER = me.carlorapisarda.SwiftyGifMacExample; 744 | PRODUCT_NAME = "$(TARGET_NAME)"; 745 | SDKROOT = macosx; 746 | SWIFT_VERSION = 5.0; 747 | }; 748 | name = Release; 749 | }; 750 | 3B18BAFE1E289899009C125A /* Debug */ = { 751 | isa = XCBuildConfiguration; 752 | buildSettings = { 753 | BUILD_LIBRARY_FOR_DISTRIBUTION = YES; 754 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 755 | CODE_SIGN_IDENTITY = ""; 756 | CURRENT_PROJECT_VERSION = 1; 757 | DEFINES_MODULE = YES; 758 | DYLIB_COMPATIBILITY_VERSION = 1; 759 | DYLIB_CURRENT_VERSION = 1; 760 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 761 | INFOPLIST_FILE = SwiftyGif/Info.plist; 762 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 763 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 764 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 765 | PRODUCT_BUNDLE_IDENTIFIER = com.alexiscreuzot.SwiftyGif.SwiftyGif; 766 | PRODUCT_NAME = "$(TARGET_NAME)"; 767 | SKIP_INSTALL = YES; 768 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 769 | SWIFT_VERSION = 5.0; 770 | VERSIONING_SYSTEM = "apple-generic"; 771 | VERSION_INFO_PREFIX = ""; 772 | }; 773 | name = Debug; 774 | }; 775 | 3B18BAFF1E289899009C125A /* Release */ = { 776 | isa = XCBuildConfiguration; 777 | buildSettings = { 778 | BUILD_LIBRARY_FOR_DISTRIBUTION = YES; 779 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 780 | CODE_SIGN_IDENTITY = ""; 781 | CURRENT_PROJECT_VERSION = 1; 782 | DEFINES_MODULE = YES; 783 | DYLIB_COMPATIBILITY_VERSION = 1; 784 | DYLIB_CURRENT_VERSION = 1; 785 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 786 | INFOPLIST_FILE = SwiftyGif/Info.plist; 787 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 788 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 789 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 790 | PRODUCT_BUNDLE_IDENTIFIER = com.alexiscreuzot.SwiftyGif.SwiftyGif; 791 | PRODUCT_NAME = "$(TARGET_NAME)"; 792 | SKIP_INSTALL = YES; 793 | SWIFT_VERSION = 5.0; 794 | VERSIONING_SYSTEM = "apple-generic"; 795 | VERSION_INFO_PREFIX = ""; 796 | }; 797 | name = Release; 798 | }; 799 | EF34CB5322A51591002A6C92 /* Debug */ = { 800 | isa = XCBuildConfiguration; 801 | buildSettings = { 802 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 803 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 804 | CLANG_ENABLE_OBJC_WEAK = YES; 805 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 806 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 807 | CODE_SIGN_IDENTITY = "iPhone Developer"; 808 | CODE_SIGN_STYLE = Automatic; 809 | FRAMEWORK_SEARCH_PATHS = ( 810 | "$(inherited)", 811 | "$(PROJECT_DIR)/Carthage/Build/iOS", 812 | ); 813 | GCC_C_LANGUAGE_STANDARD = gnu11; 814 | INFOPLIST_FILE = SwiftyGifTests/Info.plist; 815 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 816 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 817 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 818 | MTL_FAST_MATH = YES; 819 | PRODUCT_BUNDLE_IDENTIFIER = BillChan.SwiftyGifTests; 820 | PRODUCT_NAME = "$(TARGET_NAME)"; 821 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 822 | SWIFT_VERSION = 5.0; 823 | TARGETED_DEVICE_FAMILY = "1,2"; 824 | }; 825 | name = Debug; 826 | }; 827 | EF34CB5422A51591002A6C92 /* Release */ = { 828 | isa = XCBuildConfiguration; 829 | buildSettings = { 830 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 831 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 832 | CLANG_ENABLE_OBJC_WEAK = YES; 833 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 834 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 835 | CODE_SIGN_IDENTITY = "iPhone Developer"; 836 | CODE_SIGN_STYLE = Automatic; 837 | FRAMEWORK_SEARCH_PATHS = ( 838 | "$(inherited)", 839 | "$(PROJECT_DIR)/Carthage/Build/iOS", 840 | ); 841 | GCC_C_LANGUAGE_STANDARD = gnu11; 842 | INFOPLIST_FILE = SwiftyGifTests/Info.plist; 843 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 844 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 845 | MTL_FAST_MATH = YES; 846 | PRODUCT_BUNDLE_IDENTIFIER = BillChan.SwiftyGifTests; 847 | PRODUCT_NAME = "$(TARGET_NAME)"; 848 | SWIFT_VERSION = 5.0; 849 | TARGETED_DEVICE_FAMILY = "1,2"; 850 | }; 851 | name = Release; 852 | }; 853 | FA29E94D1CA9340F00E579D5 /* Debug */ = { 854 | isa = XCBuildConfiguration; 855 | buildSettings = { 856 | ALWAYS_SEARCH_USER_PATHS = NO; 857 | CLANG_ANALYZER_NONNULL = YES; 858 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 859 | CLANG_CXX_LIBRARY = "libc++"; 860 | CLANG_ENABLE_MODULES = YES; 861 | CLANG_ENABLE_OBJC_ARC = YES; 862 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 863 | CLANG_WARN_BOOL_CONVERSION = YES; 864 | CLANG_WARN_COMMA = YES; 865 | CLANG_WARN_CONSTANT_CONVERSION = YES; 866 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 867 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 868 | CLANG_WARN_EMPTY_BODY = YES; 869 | CLANG_WARN_ENUM_CONVERSION = YES; 870 | CLANG_WARN_INFINITE_RECURSION = YES; 871 | CLANG_WARN_INT_CONVERSION = YES; 872 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 873 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 874 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 875 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 876 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 877 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 878 | CLANG_WARN_STRICT_PROTOTYPES = YES; 879 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 880 | CLANG_WARN_UNREACHABLE_CODE = YES; 881 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 882 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 883 | COPY_PHASE_STRIP = NO; 884 | DEBUG_INFORMATION_FORMAT = dwarf; 885 | ENABLE_STRICT_OBJC_MSGSEND = YES; 886 | ENABLE_TESTABILITY = YES; 887 | GCC_C_LANGUAGE_STANDARD = gnu99; 888 | GCC_DYNAMIC_NO_PIC = NO; 889 | GCC_NO_COMMON_BLOCKS = YES; 890 | GCC_OPTIMIZATION_LEVEL = 0; 891 | GCC_PREPROCESSOR_DEFINITIONS = ( 892 | "DEBUG=1", 893 | "$(inherited)", 894 | ); 895 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 896 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 897 | GCC_WARN_UNDECLARED_SELECTOR = YES; 898 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 899 | GCC_WARN_UNUSED_FUNCTION = YES; 900 | GCC_WARN_UNUSED_VARIABLE = YES; 901 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 902 | MTL_ENABLE_DEBUG_INFO = YES; 903 | ONLY_ACTIVE_ARCH = YES; 904 | SDKROOT = iphoneos; 905 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 906 | SWIFT_VERSION = 5.0; 907 | TARGETED_DEVICE_FAMILY = "1,2"; 908 | }; 909 | name = Debug; 910 | }; 911 | FA29E94E1CA9340F00E579D5 /* Release */ = { 912 | isa = XCBuildConfiguration; 913 | buildSettings = { 914 | ALWAYS_SEARCH_USER_PATHS = NO; 915 | CLANG_ANALYZER_NONNULL = YES; 916 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 917 | CLANG_CXX_LIBRARY = "libc++"; 918 | CLANG_ENABLE_MODULES = YES; 919 | CLANG_ENABLE_OBJC_ARC = YES; 920 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 921 | CLANG_WARN_BOOL_CONVERSION = YES; 922 | CLANG_WARN_COMMA = YES; 923 | CLANG_WARN_CONSTANT_CONVERSION = YES; 924 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 925 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 926 | CLANG_WARN_EMPTY_BODY = YES; 927 | CLANG_WARN_ENUM_CONVERSION = YES; 928 | CLANG_WARN_INFINITE_RECURSION = YES; 929 | CLANG_WARN_INT_CONVERSION = YES; 930 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 931 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 932 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 933 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 934 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 935 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 936 | CLANG_WARN_STRICT_PROTOTYPES = YES; 937 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 938 | CLANG_WARN_UNREACHABLE_CODE = YES; 939 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 940 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 941 | COPY_PHASE_STRIP = NO; 942 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 943 | ENABLE_NS_ASSERTIONS = NO; 944 | ENABLE_STRICT_OBJC_MSGSEND = YES; 945 | GCC_C_LANGUAGE_STANDARD = gnu99; 946 | GCC_NO_COMMON_BLOCKS = YES; 947 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 948 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 949 | GCC_WARN_UNDECLARED_SELECTOR = YES; 950 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 951 | GCC_WARN_UNUSED_FUNCTION = YES; 952 | GCC_WARN_UNUSED_VARIABLE = YES; 953 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 954 | MTL_ENABLE_DEBUG_INFO = NO; 955 | SDKROOT = iphoneos; 956 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 957 | SWIFT_VERSION = 5.0; 958 | TARGETED_DEVICE_FAMILY = "1,2"; 959 | VALIDATE_PRODUCT = YES; 960 | }; 961 | name = Release; 962 | }; 963 | FA29E9501CA9340F00E579D5 /* Debug */ = { 964 | isa = XCBuildConfiguration; 965 | buildSettings = { 966 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 967 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 968 | DEVELOPMENT_TEAM = ""; 969 | INFOPLIST_FILE = "$(SRCROOT)/SwiftyGifExample/Info.plist"; 970 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 971 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 972 | PRODUCT_BUNDLE_IDENTIFIER = com.alexiscreuzot.SwiftyGif; 973 | PRODUCT_NAME = "$(TARGET_NAME)"; 974 | SWIFT_VERSION = 5.0; 975 | }; 976 | name = Debug; 977 | }; 978 | FA29E9511CA9340F00E579D5 /* Release */ = { 979 | isa = XCBuildConfiguration; 980 | buildSettings = { 981 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 982 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 983 | DEVELOPMENT_TEAM = ""; 984 | INFOPLIST_FILE = "$(SRCROOT)/SwiftyGifExample/Info.plist"; 985 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 986 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 987 | PRODUCT_BUNDLE_IDENTIFIER = com.alexiscreuzot.SwiftyGif; 988 | PRODUCT_NAME = "$(TARGET_NAME)"; 989 | SWIFT_VERSION = 5.0; 990 | }; 991 | name = Release; 992 | }; 993 | /* End XCBuildConfiguration section */ 994 | 995 | /* Begin XCConfigurationList section */ 996 | 230188A024D95CA100EFE1BC /* Build configuration list for PBXNativeTarget "SwiftyGifMac" */ = { 997 | isa = XCConfigurationList; 998 | buildConfigurations = ( 999 | 2301889E24D95CA100EFE1BC /* Debug */, 1000 | 2301889F24D95CA100EFE1BC /* Release */, 1001 | ); 1002 | defaultConfigurationIsVisible = 0; 1003 | defaultConfigurationName = Release; 1004 | }; 1005 | 230188CB24D964FF00EFE1BC /* Build configuration list for PBXNativeTarget "SwiftyGifMacExample" */ = { 1006 | isa = XCConfigurationList; 1007 | buildConfigurations = ( 1008 | 230188CC24D964FF00EFE1BC /* Debug */, 1009 | 230188CD24D964FF00EFE1BC /* Release */, 1010 | ); 1011 | defaultConfigurationIsVisible = 0; 1012 | defaultConfigurationName = Release; 1013 | }; 1014 | 3B18BAFD1E289899009C125A /* Build configuration list for PBXNativeTarget "SwiftyGif" */ = { 1015 | isa = XCConfigurationList; 1016 | buildConfigurations = ( 1017 | 3B18BAFE1E289899009C125A /* Debug */, 1018 | 3B18BAFF1E289899009C125A /* Release */, 1019 | ); 1020 | defaultConfigurationIsVisible = 0; 1021 | defaultConfigurationName = Release; 1022 | }; 1023 | EF34CB5222A51591002A6C92 /* Build configuration list for PBXNativeTarget "SwiftyGifTests" */ = { 1024 | isa = XCConfigurationList; 1025 | buildConfigurations = ( 1026 | EF34CB5322A51591002A6C92 /* Debug */, 1027 | EF34CB5422A51591002A6C92 /* Release */, 1028 | ); 1029 | defaultConfigurationIsVisible = 0; 1030 | defaultConfigurationName = Release; 1031 | }; 1032 | FA29E92D1CA9340E00E579D5 /* Build configuration list for PBXProject "SwiftyGif" */ = { 1033 | isa = XCConfigurationList; 1034 | buildConfigurations = ( 1035 | FA29E94D1CA9340F00E579D5 /* Debug */, 1036 | FA29E94E1CA9340F00E579D5 /* Release */, 1037 | ); 1038 | defaultConfigurationIsVisible = 0; 1039 | defaultConfigurationName = Release; 1040 | }; 1041 | FA29E94F1CA9340F00E579D5 /* Build configuration list for PBXNativeTarget "SwiftyGifExample" */ = { 1042 | isa = XCConfigurationList; 1043 | buildConfigurations = ( 1044 | FA29E9501CA9340F00E579D5 /* Debug */, 1045 | FA29E9511CA9340F00E579D5 /* Release */, 1046 | ); 1047 | defaultConfigurationIsVisible = 0; 1048 | defaultConfigurationName = Release; 1049 | }; 1050 | /* End XCConfigurationList section */ 1051 | }; 1052 | rootObject = FA29E92A1CA9340E00E579D5 /* Project object */; 1053 | } 1054 | -------------------------------------------------------------------------------- /SwiftyGif.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftyGif.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftyGif.xcodeproj/xcshareddata/xcschemes/SwiftyGif.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /SwiftyGif.xcodeproj/xcshareddata/xcschemes/SwiftyGifExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /SwiftyGif.xcodeproj/xcshareddata/xcschemes/SwiftyGifMacExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 44 | 46 | 52 | 53 | 54 | 55 | 61 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /SwiftyGif.xcodeproj/xcuserdata/alex.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SwiftyGif.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | SwiftyGifExample.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | SwiftyGifMac.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 3 21 | 22 | SwiftyGifMacExample.xcscheme_^#shared#^_ 23 | 24 | orderHint 25 | 2 26 | 27 | 28 | SuppressBuildableAutocreation 29 | 30 | 3B18BAF31E289899009C125A 31 | 32 | primary 33 | 34 | 35 | FA29E9311CA9340E00E579D5 36 | 37 | primary 38 | 39 | 40 | FA29E9451CA9340F00E579D5 41 | 42 | primary 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /SwiftyGif.xcodeproj/xcuserdata/travasonig.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SuppressBuildableAutocreation 6 | 7 | FA29E9311CA9340E00E579D5 8 | 9 | primary 10 | 11 | 12 | FA29E9451CA9340F00E579D5 13 | 14 | primary 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /SwiftyGif/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /SwiftyGif/NSImage+SwiftyGif.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSImage+SwiftyGif.swift 3 | // 4 | 5 | #if os(macOS) 6 | 7 | import ImageIO 8 | import AppKit 9 | 10 | public typealias GifLevelOfIntegrity = Float 11 | 12 | extension GifLevelOfIntegrity { 13 | public static let highestNoFrameSkipping: GifLevelOfIntegrity = 1 14 | public static let `default`: GifLevelOfIntegrity = 1 15 | public static let lowForManyGifs: GifLevelOfIntegrity = 0.5 16 | public static let lowForTooManyGifs: GifLevelOfIntegrity = 0.2 17 | public static let superLowForSlideShow: GifLevelOfIntegrity = 0.1 18 | } 19 | 20 | enum GifParseError: Error { 21 | case invalidFilename 22 | case noImages 23 | case noProperties 24 | case noGifDictionary 25 | case noTimingInfo 26 | } 27 | 28 | extension GifParseError: LocalizedError { 29 | public var errorDescription: String? { 30 | switch self { 31 | case .invalidFilename: 32 | return "Invalid file name" 33 | case .noImages,.noProperties, .noGifDictionary,.noTimingInfo: 34 | return "Invalid gif file " 35 | } 36 | } 37 | } 38 | 39 | public extension NSImage { 40 | /// Convenience initializer. Creates a gif with its backing data. 41 | /// 42 | /// - Parameter imageData: The actual image data, can be GIF or some other format 43 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 44 | convenience init?(imageData:Data, levelOfIntegrity: GifLevelOfIntegrity = .default) throws { 45 | do { 46 | try self.init(gifData: imageData, levelOfIntegrity: levelOfIntegrity) 47 | } catch { 48 | self.init(data: imageData) 49 | } 50 | } 51 | 52 | /// Convenience initializer. Creates a image with its backing data. 53 | /// 54 | /// - Parameter imageName: Filename 55 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 56 | convenience init?(imageName: String, levelOfIntegrity: GifLevelOfIntegrity = .default, bundle: Bundle = Bundle.main) throws { 57 | self.init() 58 | 59 | do { 60 | try setGif(imageName, levelOfIntegrity: levelOfIntegrity, bundle: bundle) 61 | } catch { 62 | self.init(named: imageName) 63 | } 64 | } 65 | } 66 | 67 | // MARK: - Inits 68 | 69 | public extension NSImage { 70 | 71 | /// Convenience initializer. Creates a gif with its backing data. 72 | /// 73 | /// - Parameter gifData: The actual gif data 74 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 75 | convenience init(gifData:Data, levelOfIntegrity: GifLevelOfIntegrity = .default) throws { 76 | self.init() 77 | try setGifFromData(gifData, levelOfIntegrity: levelOfIntegrity) 78 | } 79 | 80 | /// Convenience initializer. Creates a gif with its backing data. 81 | /// 82 | /// - Parameter gifName: Filename 83 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 84 | convenience init(gifName: String, levelOfIntegrity: GifLevelOfIntegrity = .default) throws { 85 | self.init() 86 | try setGif(gifName, levelOfIntegrity: levelOfIntegrity) 87 | } 88 | 89 | /// Set backing data for this gif. Overwrites any existing data. 90 | /// 91 | /// - Parameter data: The actual gif data 92 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 93 | func setGifFromData(_ data: Data, levelOfIntegrity: GifLevelOfIntegrity) throws { 94 | guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else { return } 95 | self.imageSource = imageSource 96 | imageData = data 97 | 98 | calculateFrameDelay(try delayTimes(imageSource), levelOfIntegrity: levelOfIntegrity) 99 | calculateFrameSize() 100 | } 101 | 102 | /// Set backing data for this gif. Overwrites any existing data. 103 | /// 104 | /// - Parameter name: Filename 105 | func setGif(_ name: String) throws { 106 | try setGif(name, levelOfIntegrity: .default) 107 | } 108 | 109 | /// Check the number of frame for this gif 110 | /// 111 | /// - Return number of frames 112 | func framesCount() -> Int { 113 | return displayOrder?.count ?? 0 114 | } 115 | 116 | private func giflog(_ msg: String) { 117 | print("SwiftyGIF: \(msg)") 118 | } 119 | 120 | /// Set backing data for this gif. Overwrites any existing data. 121 | /// 122 | /// - Parameter name: Filename 123 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 124 | func setGif(_ name: String, levelOfIntegrity: GifLevelOfIntegrity, bundle: Bundle = Bundle.main) throws { 125 | 126 | if let url = bundle.url(forResource: name, 127 | withExtension: name.pathExtension() == "gif" ? "" : "gif") { 128 | if let data = try? Data(contentsOf: url) { 129 | try setGifFromData(data, levelOfIntegrity: levelOfIntegrity) 130 | } 131 | } else { 132 | throw GifParseError.invalidFilename 133 | } 134 | } 135 | 136 | func clear() { 137 | imageData = nil 138 | imageSource = nil 139 | displayOrder = nil 140 | imageCount = nil 141 | imageSize = nil 142 | displayRefreshFactor = nil 143 | } 144 | 145 | // MARK: Logic 146 | 147 | private func convertToDelay(_ pointer:UnsafeRawPointer?) -> Float? { 148 | if pointer == nil { 149 | return nil 150 | } 151 | 152 | return unsafeBitCast(pointer, to:AnyObject.self).floatValue 153 | } 154 | 155 | /// Get delay times for each frames 156 | /// 157 | /// - Parameter imageSource: reference to the gif image source 158 | /// - Returns array of delays 159 | private func delayTimes(_ imageSource:CGImageSource) throws -> [Float] { 160 | let imageCount = CGImageSourceGetCount(imageSource) 161 | 162 | guard imageCount > 0 else { 163 | throw GifParseError.noImages 164 | } 165 | 166 | var imageProperties = [CFDictionary]() 167 | 168 | for i in 0.. CFDictionary in 177 | let key = Unmanaged.passUnretained(kCGImagePropertyGIFDictionary).toOpaque() 178 | let value = CFDictionaryGetValue(dict, key) 179 | 180 | if value == nil { 181 | throw GifParseError.noGifDictionary 182 | } 183 | 184 | return unsafeBitCast(value, to: CFDictionary.self) 185 | } 186 | 187 | let EPS:Float = 1e-6 188 | 189 | let frameDelays:[Float] = try frameProperties.map() { 190 | let unclampedKey = Unmanaged.passUnretained(kCGImagePropertyGIFUnclampedDelayTime).toOpaque() 191 | let unclampedPointer:UnsafeRawPointer? = CFDictionaryGetValue($0, unclampedKey) 192 | 193 | if let value = convertToDelay(unclampedPointer), value >= EPS { 194 | return value 195 | } 196 | 197 | let clampedKey = Unmanaged.passUnretained(kCGImagePropertyGIFDelayTime).toOpaque() 198 | let clampedPointer:UnsafeRawPointer? = CFDictionaryGetValue($0, clampedKey) 199 | 200 | if let value = convertToDelay(clampedPointer) { 201 | return value 202 | } 203 | 204 | throw GifParseError.noTimingInfo 205 | } 206 | 207 | return frameDelays 208 | } 209 | 210 | /// Compute backing data for this gif 211 | /// 212 | /// - Parameter delaysArray: decoded delay times for this gif 213 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 214 | private func calculateFrameDelay(_ delaysArray: [Float], levelOfIntegrity: GifLevelOfIntegrity) { 215 | let levelOfIntegrity = max(0, min(1, levelOfIntegrity)) 216 | var delays = delaysArray 217 | 218 | // Factors send to CADisplayLink.frameInterval 219 | let displayRefreshFactors = [60, 30, 20, 15, 12, 10, 6, 5, 4, 3, 2, 1] 220 | 221 | // maxFramePerSecond,default is 60 222 | let maxFramePerSecond = displayRefreshFactors[0] 223 | 224 | // frame numbers per second 225 | let displayRefreshRates = displayRefreshFactors.map { maxFramePerSecond / $0 } 226 | 227 | // time interval per frame 228 | let displayRefreshDelayTime = displayRefreshRates.map { 1 / Float($0) } 229 | 230 | // calculate the time when each frame should be displayed at(start at 0) 231 | for i in delays.indices.dropFirst() { 232 | delays[i] += delays[i - 1] 233 | } 234 | 235 | //find the appropriate Factors then BREAK 236 | for (i, delayTime) in displayRefreshDelayTime.enumerated() { 237 | let displayPosition = delays.map { Int($0 / delayTime) } 238 | var frameLoseCount: Float = 0 239 | 240 | for j in displayPosition.indices.dropFirst() where displayPosition[j] == displayPosition[j - 1] { 241 | frameLoseCount += 1 242 | } 243 | 244 | if displayPosition.first == 0 { 245 | frameLoseCount += 1 246 | } 247 | 248 | if frameLoseCount <= Float(displayPosition.count) * (1 - levelOfIntegrity) || i == displayRefreshDelayTime.count - 1 { 249 | imageCount = displayPosition.last 250 | displayRefreshFactor = displayRefreshFactors[i] 251 | displayOrder = [] 252 | var oldIndex = 0 253 | var newIndex = 1 254 | let imageCount = self.imageCount ?? 0 255 | 256 | while newIndex <= imageCount && oldIndex < displayPosition.count { 257 | if newIndex <= displayPosition[oldIndex] { 258 | displayOrder?.append(oldIndex) 259 | newIndex += 1 260 | } else { 261 | oldIndex += 1 262 | } 263 | } 264 | 265 | break 266 | } 267 | } 268 | } 269 | 270 | /// Compute frame size for this gif 271 | private func calculateFrameSize(){ 272 | guard let imageSource = imageSource, 273 | let imageCount = imageCount, 274 | let cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) else { 275 | return 276 | } 277 | 278 | 279 | let image = NSImage(cgImage: cgImage, size: .zero) 280 | imageSize = Int(image.size.height * image.size.width * 4) * imageCount / 1_000_000 281 | } 282 | } 283 | 284 | // MARK: - Properties 285 | 286 | private let _imageSourceKey = malloc(4) 287 | private let _displayRefreshFactorKey = malloc(4) 288 | private let _imageSizeKey = malloc(4) 289 | private let _imageCountKey = malloc(4) 290 | private let _displayOrderKey = malloc(4) 291 | private let _imageDataKey = malloc(4) 292 | 293 | public extension NSImage { 294 | 295 | var imageSource: CGImageSource? { 296 | get { 297 | let result = objc_getAssociatedObject(self, _imageSourceKey!) 298 | return result == nil ? nil : (result as! CGImageSource) 299 | } 300 | set { 301 | objc_setAssociatedObject(self, _imageSourceKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 302 | } 303 | } 304 | 305 | var displayRefreshFactor: Int?{ 306 | get { return objc_getAssociatedObject(self, _displayRefreshFactorKey!) as? Int } 307 | set { objc_setAssociatedObject(self, _displayRefreshFactorKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 308 | } 309 | 310 | var imageSize: Int?{ 311 | get { return objc_getAssociatedObject(self, _imageSizeKey!) as? Int } 312 | set { objc_setAssociatedObject(self, _imageSizeKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 313 | } 314 | 315 | var imageCount: Int?{ 316 | get { return objc_getAssociatedObject(self, _imageCountKey!) as? Int } 317 | set { objc_setAssociatedObject(self, _imageCountKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 318 | } 319 | 320 | var displayOrder: [Int]?{ 321 | get { return objc_getAssociatedObject(self, _displayOrderKey!) as? [Int] } 322 | set { objc_setAssociatedObject(self, _displayOrderKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 323 | } 324 | 325 | var imageData:Data? { 326 | get { 327 | let result = objc_getAssociatedObject(self, _imageDataKey!) 328 | return result == nil ? nil : (result as? Data) 329 | } 330 | set { 331 | objc_setAssociatedObject(self, _imageDataKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 332 | } 333 | } 334 | } 335 | 336 | extension String { 337 | fileprivate func pathExtension() -> String { 338 | return (self as NSString).pathExtension 339 | } 340 | } 341 | 342 | #endif 343 | -------------------------------------------------------------------------------- /SwiftyGif/NSImageView+SwiftyGif.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSImageView+SwiftyGif.swift 3 | // 4 | 5 | #if os(macOS) 6 | 7 | import ImageIO 8 | import AppKit 9 | 10 | @objc public protocol SwiftyGifDelegate { 11 | @objc optional func gifDidStart(sender: NSImageView) 12 | @objc optional func gifDidLoop(sender: NSImageView) 13 | @objc optional func gifDidStop(sender: NSImageView) 14 | @objc optional func gifURLDidFinish(sender: NSImageView) 15 | @objc optional func gifURLDidFail(sender: NSImageView, url: URL, error: Error?) 16 | } 17 | 18 | public extension NSImageView { 19 | /// Set an image and a manager to an existing NSImageView. If the image is not an GIF image, set it in normal way and remove self form SwiftyGifManager 20 | /// 21 | /// WARNING : this overwrite any previous gif. 22 | /// - Parameter gifImage: The NSImage containing the gif backing data 23 | /// - Parameter manager: The manager to handle the gif display 24 | /// - Parameter loopCount: The number of loops we want for this gif. -1 means infinite. 25 | func setImage(_ image: NSImage, manager: SwiftyGifManager = .defaultManager, loopCount: Int = -1) { 26 | if let _ = image.imageData { 27 | setGifImage(image, manager: manager, loopCount: loopCount) 28 | } else { 29 | manager.deleteImageView(self) 30 | self.image = image 31 | } 32 | } 33 | } 34 | 35 | public extension NSImageView { 36 | 37 | // MARK: - Inits 38 | 39 | /// Convenience initializer. Creates a gif holder (defaulted to infinite loop). 40 | /// 41 | /// - Parameter gifImage: The NSImage containing the gif backing data 42 | /// - Parameter manager: The manager to handle the gif display 43 | convenience init(gifImage: NSImage, manager: SwiftyGifManager = .defaultManager, loopCount: Int = -1) { 44 | self.init() 45 | setGifImage(gifImage,manager: manager, loopCount: loopCount) 46 | } 47 | 48 | /// Convenience initializer. Creates a gif holder (defaulted to infinite loop). 49 | /// 50 | /// - Parameter gifImage: The NSImage containing the gif backing data 51 | /// - Parameter manager: The manager to handle the gif display 52 | convenience init( 53 | gifURL: URL, 54 | manager: SwiftyGifManager = .defaultManager, 55 | loopCount: Int = -1, 56 | callback: @escaping (Result) -> Void = { _ in } 57 | ) { 58 | self.init() 59 | setGifFromURL(gifURL, manager: manager, loopCount: loopCount, callback: callback) 60 | } 61 | 62 | /// Set a gif image and a manager to an existing NSImageView. 63 | /// 64 | /// WARNING : this overwrite any previous gif. 65 | /// - Parameter gifImage: The NSImage containing the gif backing data 66 | /// - Parameter manager: The manager to handle the gif display 67 | /// - Parameter loopCount: The number of loops we want for this gif. -1 means infinite. 68 | func setGifImage(_ gifImage: NSImage, manager: SwiftyGifManager = .defaultManager, loopCount: Int = -1) { 69 | if let imageData = gifImage.imageData, (gifImage.imageCount ?? 0) < 1 { 70 | image = NSImage(data: imageData) 71 | return 72 | } 73 | 74 | self.loopCount = loopCount 75 | self.gifImage = gifImage 76 | animationManager = manager 77 | syncFactor = 0 78 | displayOrderIndex = 0 79 | cache = NSCache() 80 | haveCache = false 81 | 82 | if let source = gifImage.imageSource, let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) { 83 | currentImage = NSImage(cgImage: cgImage, size: .zero) 84 | 85 | if manager.addImageView(self) { 86 | startDisplay() 87 | startAnimatingGif() 88 | } 89 | } 90 | } 91 | } 92 | 93 | // MARK: - Download gif 94 | 95 | public extension NSImageView { 96 | 97 | /// Download gif image and sets it. 98 | /// 99 | /// - Parameters: 100 | /// - url: The URL pointing to the gif data 101 | /// - manager: The manager to handle the gif display 102 | /// - loopCount: The number of loops we want for this gif. -1 means infinite. 103 | /// - showLoader: Show UIActivityIndicatorView or not 104 | /// - Returns: An URL session task. Note: You can cancel the downloading task if it needed. 105 | @discardableResult 106 | func setGifFromURL(_ url: URL, 107 | manager: SwiftyGifManager = .defaultManager, 108 | loopCount: Int = -1, 109 | levelOfIntegrity: GifLevelOfIntegrity = .default, 110 | session: URLSession = URLSession.shared, 111 | showLoader: Bool = true, 112 | customLoader: NSView? = nil, 113 | callback: @escaping (Result) -> Void = {_ in } 114 | ) -> URLSessionDataTask? { 115 | 116 | if let data = manager.remoteCache[url] { 117 | self.parseDownloadedGif(url: url, 118 | data: data, 119 | error: nil, 120 | manager: manager, 121 | loopCount: loopCount, 122 | levelOfIntegrity: levelOfIntegrity, 123 | callback: callback 124 | ) 125 | return nil 126 | } 127 | 128 | stopAnimatingGif() 129 | 130 | let loader: NSView? = showLoader ? createLoader(from: customLoader) : nil 131 | 132 | let task = session.dataTask(with: url) { [weak self] data, _, error in 133 | DispatchQueue.main.async { 134 | loader?.removeFromSuperview() 135 | self?.parseDownloadedGif(url: url, 136 | data: data, 137 | error: error, 138 | manager: manager, 139 | loopCount: loopCount, 140 | levelOfIntegrity: levelOfIntegrity, 141 | callback: callback) 142 | } 143 | } 144 | 145 | task.resume() 146 | 147 | return task 148 | } 149 | 150 | private func createLoader(from view: NSView? = nil) -> NSView { 151 | let loader = view ?? { 152 | let indicator = NSProgressIndicator() 153 | indicator.style = .spinning 154 | return indicator 155 | }() 156 | 157 | addSubview(loader) 158 | loader.translatesAutoresizingMaskIntoConstraints = false 159 | 160 | addConstraint(NSLayoutConstraint( 161 | item: loader, 162 | attribute: .centerX, 163 | relatedBy: .equal, 164 | toItem: self, 165 | attribute: .centerX, 166 | multiplier: 1, 167 | constant: 0)) 168 | 169 | addConstraint(NSLayoutConstraint( 170 | item: loader, 171 | attribute: .centerY, 172 | relatedBy: .equal, 173 | toItem: self, 174 | attribute: .centerY, 175 | multiplier: 1, 176 | constant: 0)) 177 | 178 | (loader as? NSProgressIndicator)?.startAnimation(nil) 179 | 180 | return loader 181 | } 182 | 183 | private func parseDownloadedGif(url: URL, 184 | data: Data?, 185 | error: Error?, 186 | manager: SwiftyGifManager, 187 | loopCount: Int, 188 | levelOfIntegrity: GifLevelOfIntegrity, 189 | callback: (Result) -> Void) { 190 | guard let data = data else { 191 | report(url: url, error: error) 192 | callback(.failure(error ?? SwiftyGifError.noGifData)) 193 | return 194 | } 195 | 196 | do { 197 | let image = try NSImage(gifData: data, levelOfIntegrity: levelOfIntegrity) 198 | manager.remoteCache[url] = data 199 | setGifImage(image, manager: manager, loopCount: loopCount) 200 | startAnimatingGif() 201 | delegate?.gifURLDidFinish?(sender: self) 202 | callback(.success(data)) 203 | } catch { 204 | report(url: url, error: error) 205 | callback(.failure(error)) 206 | } 207 | } 208 | 209 | private func report(url: URL, error: Error?) { 210 | delegate?.gifURLDidFail?(sender: self, url: url, error: error) 211 | } 212 | } 213 | 214 | // MARK: - Logic 215 | 216 | public extension NSImageView { 217 | 218 | /// Start displaying the gif for this NSImageView. 219 | private func startDisplay() { 220 | displaying = true 221 | updateCache() 222 | } 223 | 224 | /// Stop displaying the gif for this NSImageView. 225 | private func stopDisplay() { 226 | displaying = false 227 | updateCache() 228 | } 229 | 230 | /// Start displaying the gif for this NSImageView. 231 | func startAnimatingGif() { 232 | isPlaying = true 233 | } 234 | 235 | /// Stop displaying the gif for this NSImageView. 236 | func stopAnimatingGif() { 237 | isPlaying = false 238 | } 239 | 240 | /// Check if this imageView is currently playing a gif 241 | /// 242 | /// - Returns wether the gif is currently playing 243 | func isAnimatingGif() -> Bool{ 244 | return isPlaying 245 | } 246 | 247 | /// Show a specific frame based on a delta from current frame 248 | /// 249 | /// - Parameter delta: The delsta from current frame we want 250 | func showFrameForIndexDelta(_ delta: Int) { 251 | guard let gifImage = gifImage else { return } 252 | var nextIndex = displayOrderIndex + delta 253 | 254 | while nextIndex >= gifImage.framesCount() { 255 | nextIndex -= gifImage.framesCount() 256 | } 257 | 258 | while nextIndex < 0 { 259 | nextIndex += gifImage.framesCount() 260 | } 261 | 262 | showFrameAtIndex(nextIndex) 263 | } 264 | 265 | /// Show a specific frame 266 | /// 267 | /// - Parameter index: The index of frame to show 268 | func showFrameAtIndex(_ index: Int) { 269 | displayOrderIndex = index 270 | updateFrame() 271 | } 272 | 273 | /// Update cache for the current imageView. 274 | func updateCache() { 275 | guard let animationManager = animationManager else { return } 276 | 277 | if animationManager.hasCache(self) && !haveCache { 278 | prepareCache() 279 | haveCache = true 280 | } else if !animationManager.hasCache(self) && haveCache { 281 | cache?.removeAllObjects() 282 | haveCache = false 283 | } 284 | } 285 | 286 | /// Update current image displayed. This method is called by the manager. 287 | func updateCurrentImage() { 288 | if displaying { 289 | updateFrame() 290 | updateIndex() 291 | 292 | if loopCount == 0 || !isDisplayedInScreen(self) || !isPlaying { 293 | stopDisplay() 294 | } 295 | } else { 296 | if isDisplayedInScreen(self) && loopCount != 0 && isPlaying { 297 | startDisplay() 298 | } 299 | 300 | if isDiscarded(self) { 301 | animationManager?.deleteImageView(self) 302 | } 303 | } 304 | } 305 | 306 | /// Force update frame 307 | private func updateFrame() { 308 | if haveCache, let image = cache?.object(forKey: displayOrderIndex as AnyObject) as? NSImage { 309 | currentImage = image 310 | } else { 311 | currentImage = frameAtIndex(index: currentFrameIndex()) 312 | } 313 | } 314 | 315 | /// Get current frame index 316 | func currentFrameIndex() -> Int{ 317 | return displayOrderIndex 318 | } 319 | 320 | /// Get frame at specific index 321 | func frameAtIndex(index: Int) -> NSImage { 322 | guard let gifImage = gifImage, 323 | let imageSource = gifImage.imageSource, 324 | let displayOrder = gifImage.displayOrder, index < displayOrder.count, 325 | let cgImage = CGImageSourceCreateImageAtIndex(imageSource, displayOrder[index], nil) else { 326 | return NSImage() 327 | } 328 | 329 | return NSImage(cgImage: cgImage, size: .zero) 330 | } 331 | 332 | /// Check if the imageView has been discarded and is not in the view hierarchy anymore. 333 | /// 334 | /// - Returns : A boolean for weather the imageView was discarded 335 | func isDiscarded(_ imageView: NSView?) -> Bool { 336 | return imageView?.superview == nil 337 | } 338 | 339 | /// Check if the imageView is displayed. 340 | /// 341 | /// - Returns : A boolean for weather the imageView is displayed 342 | func isDisplayedInScreen(_ imageView: NSView?) -> Bool { 343 | guard !isHidden, window != nil, let imageView = imageView else { 344 | return false 345 | } 346 | 347 | for screen in NSScreen.screens { 348 | let screenRect = screen.visibleFrame 349 | let viewRect = imageView.convert(bounds, to: nil) 350 | let intersectionRect = viewRect.intersection(screenRect) 351 | 352 | if !intersectionRect.isEmpty && !intersectionRect.isNull { 353 | // The image view is visible on a screen 354 | return true 355 | } 356 | } 357 | 358 | return false 359 | } 360 | 361 | func clear() { 362 | if let gifImage = gifImage { 363 | gifImage.clear() 364 | } 365 | 366 | gifImage = nil 367 | currentImage = nil 368 | cache?.removeAllObjects() 369 | animationManager = nil 370 | image = nil 371 | } 372 | 373 | /// Update loop count and sync factor. 374 | private func updateIndex() { 375 | guard let gif = self.gifImage, 376 | let displayRefreshFactor = gif.displayRefreshFactor, 377 | displayRefreshFactor > 0 else { 378 | return 379 | } 380 | 381 | syncFactor = (syncFactor + 1) % displayRefreshFactor 382 | 383 | if syncFactor == 0, let imageCount = gif.imageCount, imageCount > 0 { 384 | displayOrderIndex = (displayOrderIndex+1) % imageCount 385 | 386 | if displayOrderIndex == 0 { 387 | if loopCount == -1 { 388 | delegate?.gifDidLoop?(sender: self) 389 | } else if loopCount > 1 { 390 | delegate?.gifDidLoop?(sender: self) 391 | loopCount -= 1 392 | } else { 393 | delegate?.gifDidStop?(sender: self) 394 | loopCount -= 1 395 | } 396 | } 397 | } 398 | } 399 | 400 | /// Prepare the cache by adding every images of the gif to an NSCache object. 401 | private func prepareCache() { 402 | guard let cache = self.cache else { return } 403 | 404 | cache.removeAllObjects() 405 | 406 | guard let gif = self.gifImage, 407 | let displayOrder = gif.displayOrder, 408 | let imageSource = gif.imageSource else { return } 409 | 410 | for (i, order) in displayOrder.enumerated() { 411 | guard let cgImage = CGImageSourceCreateImageAtIndex(imageSource, order, nil) else { continue } 412 | 413 | cache.setObject(NSImage(cgImage: cgImage, size: .zero), forKey: i as AnyObject) 414 | } 415 | } 416 | } 417 | 418 | // MARK: - Dynamic properties 419 | 420 | private let _gifImageKey = malloc(4) 421 | private let _cacheKey = malloc(4) 422 | private let _currentImageKey = malloc(4) 423 | private let _displayOrderIndexKey = malloc(4) 424 | private let _syncFactorKey = malloc(4) 425 | private let _haveCacheKey = malloc(4) 426 | private let _loopCountKey = malloc(4) 427 | private let _displayingKey = malloc(4) 428 | private let _isPlayingKey = malloc(4) 429 | private let _animationManagerKey = malloc(4) 430 | private let _delegateKey = malloc(4) 431 | 432 | public extension NSImageView { 433 | 434 | var gifImage: NSImage? { 435 | get { return possiblyNil(_gifImageKey) } 436 | set { objc_setAssociatedObject(self, _gifImageKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 437 | } 438 | 439 | var currentImage: NSImage? { 440 | get { return possiblyNil(_currentImageKey) } 441 | set { objc_setAssociatedObject(self, _currentImageKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 442 | } 443 | 444 | private var displayOrderIndex: Int { 445 | get { return value(_displayOrderIndexKey, 0) } 446 | set { objc_setAssociatedObject(self, _displayOrderIndexKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 447 | } 448 | 449 | private var syncFactor: Int { 450 | get { return value(_syncFactorKey, 0) } 451 | set { objc_setAssociatedObject(self, _syncFactorKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 452 | } 453 | 454 | var loopCount: Int { 455 | get { return value(_loopCountKey, 0) } 456 | set { objc_setAssociatedObject(self, _loopCountKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 457 | } 458 | 459 | var animationManager: SwiftyGifManager? { 460 | get { return (objc_getAssociatedObject(self, _animationManagerKey!) as? SwiftyGifManager) } 461 | set { objc_setAssociatedObject(self, _animationManagerKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 462 | } 463 | 464 | var delegate: SwiftyGifDelegate? { 465 | get { return (objc_getAssociatedWeakObject(self, _delegateKey!) as? SwiftyGifDelegate) } 466 | set { objc_setAssociatedWeakObject(self, _delegateKey!, newValue) } 467 | } 468 | 469 | private var haveCache: Bool { 470 | get { return value(_haveCacheKey, false) } 471 | set { objc_setAssociatedObject(self, _haveCacheKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 472 | } 473 | 474 | var displaying: Bool { 475 | get { return value(_displayingKey, false) } 476 | set { objc_setAssociatedObject(self, _displayingKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 477 | } 478 | 479 | private var isPlaying: Bool { 480 | get { 481 | return value(_isPlayingKey, false) 482 | } 483 | set { 484 | objc_setAssociatedObject(self, _isPlayingKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 485 | 486 | if newValue { 487 | delegate?.gifDidStart?(sender: self) 488 | } else { 489 | delegate?.gifDidStop?(sender: self) 490 | } 491 | } 492 | } 493 | 494 | private var cache: NSCache? { 495 | get { return (objc_getAssociatedObject(self, _cacheKey!) as? NSCache) } 496 | set { objc_setAssociatedObject(self, _cacheKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 497 | } 498 | 499 | private func value(_ key:UnsafeMutableRawPointer?, _ defaultValue:T) -> T { 500 | return (objc_getAssociatedObject(self, key!) as? T) ?? defaultValue 501 | } 502 | 503 | private func possiblyNil(_ key:UnsafeMutableRawPointer?) -> T? { 504 | let result = objc_getAssociatedObject(self, key!) 505 | 506 | if result == nil { 507 | return nil 508 | } 509 | 510 | return (result as? T) 511 | } 512 | } 513 | 514 | #endif 515 | -------------------------------------------------------------------------------- /SwiftyGif/ObjcAssociatedWeakObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjcAssociatedWeakObject.swift 3 | // 4 | 5 | import Foundation 6 | 7 | func objc_getAssociatedWeakObject(_ object: AnyObject, _ key: UnsafeRawPointer) -> AnyObject? { 8 | let block: (() -> AnyObject?)? = objc_getAssociatedObject(object, key) as? (() -> AnyObject?) 9 | return block != nil ? block?() : nil 10 | } 11 | 12 | func objc_setAssociatedWeakObject(_ object: AnyObject, _ key: UnsafeRawPointer, _ value: AnyObject?) { 13 | weak var weakValue = value 14 | let block: (() -> AnyObject?)? = { 15 | return weakValue 16 | } 17 | objc_setAssociatedObject(object, key, block, .OBJC_ASSOCIATION_COPY) 18 | } 19 | -------------------------------------------------------------------------------- /SwiftyGif/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyCollectedDataTypes 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyAccessedAPITypes 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SwiftyGif/SwiftyGif.h: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyGif.h 3 | // SwiftyGif 4 | // 5 | // Created by Scott Hoyt on 1/12/17. 6 | // Copyright © 2017 alexiscreuzot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SwiftyGif. 12 | FOUNDATION_EXPORT double SwiftyGifVersionNumber; 13 | 14 | //! Project version string for SwiftyGif. 15 | FOUNDATION_EXPORT const unsigned char SwiftyGifVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /SwiftyGif/SwiftyGifError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyGifError.swift 3 | // SwiftyGif 4 | // 5 | // Created by Abbas Sabeti on 26.07.24. 6 | // Copyright © 2024 alexiscreuzot. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum SwiftyGifError: Error { 12 | case noGifData 13 | case corruptedGifData 14 | } 15 | -------------------------------------------------------------------------------- /SwiftyGif/SwiftyGifManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyGifManager.swift 3 | // 4 | // 5 | import ImageIO 6 | 7 | #if os(macOS) 8 | import AppKit 9 | import CoreVideo 10 | #else 11 | import UIKit 12 | #endif 13 | 14 | #if os(macOS) 15 | public typealias PlatformImageView = NSImageView 16 | #else 17 | public typealias PlatformImageView = UIImageView 18 | #endif 19 | 20 | open class SwiftyGifManager { 21 | 22 | // A convenient default manager if we only have one gif to display here and there 23 | public static var defaultManager = SwiftyGifManager(memoryLimit: 50) 24 | 25 | #if os(macOS) 26 | fileprivate var timer: CVDisplayLink? 27 | #else 28 | fileprivate var timer: CADisplayLink? 29 | #endif 30 | 31 | fileprivate var displayViews: [PlatformImageView] = [] 32 | fileprivate var totalGifSize: Int 33 | fileprivate var memoryLimit: Int 34 | open var haveCache: Bool 35 | open var remoteCache : [URL : Data] = [:] 36 | #if swift(>=4.2) 37 | public var mode: RunLoop.Mode = .common 38 | #else 39 | public var mode: RunLoopMode = RunLoopMode.commonModes 40 | #endif 41 | 42 | 43 | /// Initialize a manager 44 | /// 45 | /// - Parameter memoryLimit: The number of Mb max for this manager 46 | public init(memoryLimit: Int) { 47 | self.memoryLimit = memoryLimit 48 | totalGifSize = 0 49 | haveCache = true 50 | } 51 | 52 | deinit { 53 | stopTimer() 54 | } 55 | 56 | public func startTimerIfNeeded() { 57 | guard timer == nil else { 58 | return 59 | } 60 | 61 | #if os(macOS) 62 | 63 | func displayLinkOutputCallback(displayLink: CVDisplayLink, 64 | _ inNow: UnsafePointer, 65 | _ inOutputTime: UnsafePointer, 66 | _ flagsIn: CVOptionFlags, 67 | _ flagsOut: UnsafeMutablePointer, 68 | _ displayLinkContext: UnsafeMutableRawPointer?) -> CVReturn { 69 | unsafeBitCast(displayLinkContext!, to: SwiftyGifManager.self).updateImageView() 70 | return kCVReturnSuccess 71 | } 72 | 73 | CVDisplayLinkCreateWithActiveCGDisplays(&timer) 74 | CVDisplayLinkSetOutputCallback(timer!, displayLinkOutputCallback, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())) 75 | CVDisplayLinkStart(timer!) 76 | 77 | #else 78 | 79 | timer = CADisplayLink(target: self, selector: #selector(updateImageView)) 80 | 81 | timer?.add(to: .main, forMode: mode) 82 | 83 | #endif 84 | } 85 | 86 | public func stopTimer() { 87 | #if os(macOS) 88 | CVDisplayLinkStop(timer!) 89 | #else 90 | timer?.invalidate() 91 | #endif 92 | 93 | timer = nil 94 | } 95 | 96 | /// Add a new imageView to this manager if it doesn't exist 97 | /// - Parameter imageView: The image view we're adding to this manager 98 | open func addImageView(_ imageView: PlatformImageView) -> Bool { 99 | if containsImageView(imageView) { 100 | startTimerIfNeeded() 101 | return false 102 | } 103 | 104 | updateCacheSize(for: imageView, add: true) 105 | displayViews.append(imageView) 106 | startTimerIfNeeded() 107 | 108 | return true 109 | } 110 | 111 | /// Delete an imageView from this manager if it exists 112 | /// - Parameter imageView: The image view we want to delete 113 | open func deleteImageView(_ imageView: PlatformImageView) { 114 | guard let index = displayViews.firstIndex(of: imageView) else { 115 | return 116 | } 117 | 118 | displayViews.remove(at: index) 119 | updateCacheSize(for: imageView, add: false) 120 | } 121 | 122 | open func updateCacheSize(for imageView: PlatformImageView, add: Bool) { 123 | totalGifSize += (add ? 1 : -1) * (imageView.gifImage?.imageSize ?? 0) 124 | haveCache = totalGifSize <= memoryLimit 125 | 126 | for imageView in displayViews { 127 | DispatchQueue.global(qos: .userInteractive).sync(execute: imageView.updateCache) 128 | } 129 | } 130 | 131 | open func clear() { 132 | displayViews.forEach { $0.clear() } 133 | displayViews = [] 134 | stopTimer() 135 | } 136 | 137 | /// Check if an imageView is already managed by this manager 138 | /// - Parameter imageView: The image view we're searching 139 | /// - Returns : a boolean for wether the imageView was found 140 | open func containsImageView(_ imageView: PlatformImageView) -> Bool{ 141 | return displayViews.contains(imageView) 142 | } 143 | 144 | /// Check if this manager has cache for an imageView 145 | /// - Parameter imageView: The image view we're searching cache for 146 | /// - Returns : a boolean for wether we have cache for the imageView 147 | open func hasCache(_ imageView: PlatformImageView) -> Bool { 148 | return imageView.displaying && (imageView.loopCount == -1 || imageView.loopCount >= 5) ? haveCache : false 149 | } 150 | 151 | /// Update imageView current image. This method is called by the main loop. 152 | /// This is what create the animation. 153 | @objc func updateImageView() { 154 | guard !displayViews.isEmpty else { 155 | stopTimer() 156 | return 157 | } 158 | 159 | #if os(macOS) 160 | let queue = DispatchQueue.main 161 | #else 162 | let queue = DispatchQueue.global(qos: .userInteractive) 163 | #endif 164 | 165 | for imageView in displayViews { 166 | queue.sync { 167 | imageView.image = imageView.currentImage 168 | } 169 | 170 | if imageView.isAnimatingGif() { 171 | queue.sync(execute: imageView.updateCurrentImage) 172 | } else if imageView.isDiscarded(imageView) { 173 | queue.sync { 174 | self.deleteImageView(imageView) 175 | } 176 | } 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /SwiftyGif/UIImage+SwiftyGif.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+SwiftyGif.swift 3 | // 4 | 5 | #if !os(macOS) 6 | 7 | import ImageIO 8 | import UIKit 9 | 10 | public typealias GifLevelOfIntegrity = Float 11 | 12 | extension GifLevelOfIntegrity { 13 | public static let highestNoFrameSkipping: GifLevelOfIntegrity = 1 14 | public static let `default`: GifLevelOfIntegrity = 0.8 15 | public static let lowForManyGifs: GifLevelOfIntegrity = 0.5 16 | public static let lowForTooManyGifs: GifLevelOfIntegrity = 0.2 17 | public static let superLowForSlideShow: GifLevelOfIntegrity = 0.1 18 | } 19 | 20 | enum GifParseError: Error { 21 | case invalidFilename 22 | case noImages 23 | case noProperties 24 | case noGifDictionary 25 | case noTimingInfo 26 | } 27 | 28 | extension GifParseError: LocalizedError { 29 | public var errorDescription: String? { 30 | switch self { 31 | case .invalidFilename: 32 | return "Invalid file name" 33 | case .noImages,.noProperties, .noGifDictionary,.noTimingInfo: 34 | return "Invalid gif file " 35 | } 36 | } 37 | } 38 | 39 | public extension UIImage { 40 | /// Convenience initializer. Creates a gif with its backing data. 41 | /// 42 | /// - Parameter imageData: The actual image data, can be GIF or some other format 43 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 44 | convenience init?(imageData:Data, levelOfIntegrity: GifLevelOfIntegrity = .default) throws { 45 | do { 46 | try self.init(gifData: imageData, levelOfIntegrity: levelOfIntegrity) 47 | } catch { 48 | self.init(data: imageData) 49 | } 50 | } 51 | 52 | /// Convenience initializer. Creates a image with its backing data. 53 | /// 54 | /// - Parameter imageName: Filename 55 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 56 | convenience init?(imageName: String, levelOfIntegrity: GifLevelOfIntegrity = .default, bundle: Bundle = Bundle.main) throws { 57 | self.init() 58 | 59 | do { 60 | try setGif(imageName, levelOfIntegrity: levelOfIntegrity, bundle: bundle) 61 | } catch { 62 | self.init(named: imageName) 63 | } 64 | } 65 | } 66 | 67 | // MARK: - Inits 68 | 69 | public extension UIImage { 70 | 71 | /// Convenience initializer. Creates a gif with its backing data. 72 | /// 73 | /// - Parameter gifData: The actual gif data 74 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 75 | @objc convenience init(gifData:Data, floatLevelOfIntegrity: Float = GifLevelOfIntegrity.highestNoFrameSkipping) throws { 76 | self.init() 77 | try setGifFromData(gifData, levelOfIntegrity: floatLevelOfIntegrity) 78 | } 79 | 80 | /// Convenience initializer. Creates a gif with its backing data. 81 | /// 82 | /// - Parameter gifData: The actual gif data 83 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 84 | @objc convenience init(gifData:Data, levelOfIntegrity: GifLevelOfIntegrity = .default) throws { 85 | self.init() 86 | try setGifFromData(gifData, levelOfIntegrity: levelOfIntegrity) 87 | } 88 | 89 | /// Convenience initializer. Creates a gif with its backing data. 90 | /// 91 | /// - Parameter gifName: Filename 92 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 93 | convenience init(gifName: String, levelOfIntegrity: GifLevelOfIntegrity = .default, bundle: Bundle = Bundle.main) throws { 94 | self.init() 95 | try setGif(gifName, levelOfIntegrity: levelOfIntegrity, bundle: bundle) 96 | } 97 | 98 | /// Set backing data for this gif. Overwrites any existing data. 99 | /// 100 | /// - Parameter data: The actual gif data 101 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 102 | func setGifFromData(_ data: Data, levelOfIntegrity: GifLevelOfIntegrity) throws { 103 | guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else { return } 104 | self.imageSource = imageSource 105 | imageData = data 106 | 107 | calculateFrameDelay(try delayTimes(imageSource), levelOfIntegrity: levelOfIntegrity) 108 | calculateFrameSize() 109 | } 110 | 111 | /// Set backing data for this gif. Overwrites any existing data. 112 | /// 113 | /// - Parameter name: Filename 114 | func setGif(_ name: String, bundle: Bundle = Bundle.main) throws { 115 | try setGif(name, levelOfIntegrity: .default, bundle: bundle) 116 | } 117 | 118 | /// Check the number of frame for this gif 119 | /// 120 | /// - Return number of frames 121 | func framesCount() -> Int { 122 | return displayOrder?.count ?? 0 123 | } 124 | 125 | /// Set backing data for this gif. Overwrites any existing data. 126 | /// 127 | /// - Parameter name: Filename 128 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 129 | func setGif(_ name: String, levelOfIntegrity: GifLevelOfIntegrity, bundle: Bundle = Bundle.main) throws { 130 | if let url = bundle.url(forResource: name, withExtension: name.pathExtension() == "gif" ? "" : "gif") { 131 | if let data = try? Data(contentsOf: url) { 132 | try setGifFromData(data, levelOfIntegrity: levelOfIntegrity) 133 | } 134 | } else { 135 | throw GifParseError.invalidFilename 136 | } 137 | } 138 | 139 | func clear() { 140 | imageData = nil 141 | imageSource = nil 142 | displayOrder = nil 143 | imageCount = nil 144 | imageSize = nil 145 | displayRefreshFactor = nil 146 | } 147 | 148 | // MARK: Logic 149 | 150 | private func convertToDelay(_ pointer:UnsafeRawPointer?) -> Float? { 151 | if pointer == nil { 152 | return nil 153 | } 154 | 155 | return unsafeBitCast(pointer, to:AnyObject.self).floatValue 156 | } 157 | 158 | /// Get delay times for each frames 159 | /// 160 | /// - Parameter imageSource: reference to the gif image source 161 | /// - Returns array of delays 162 | private func delayTimes(_ imageSource:CGImageSource) throws -> [Float] { 163 | let imageCount = CGImageSourceGetCount(imageSource) 164 | 165 | guard imageCount > 0 else { 166 | throw GifParseError.noImages 167 | } 168 | 169 | var imageProperties = [CFDictionary]() 170 | 171 | for i in 0.. CFDictionary in 180 | let key = Unmanaged.passUnretained(kCGImagePropertyGIFDictionary).toOpaque() 181 | let value = CFDictionaryGetValue(dict, key) 182 | 183 | if value == nil { 184 | throw GifParseError.noGifDictionary 185 | } 186 | 187 | return unsafeBitCast(value, to: CFDictionary.self) 188 | } 189 | 190 | let EPS:Float = 1e-6 191 | 192 | let frameDelays:[Float] = try frameProperties.map() { 193 | let unclampedKey = Unmanaged.passUnretained(kCGImagePropertyGIFUnclampedDelayTime).toOpaque() 194 | let unclampedPointer:UnsafeRawPointer? = CFDictionaryGetValue($0, unclampedKey) 195 | 196 | if let value = convertToDelay(unclampedPointer), value >= EPS { 197 | return value 198 | } 199 | 200 | let clampedKey = Unmanaged.passUnretained(kCGImagePropertyGIFDelayTime).toOpaque() 201 | let clampedPointer:UnsafeRawPointer? = CFDictionaryGetValue($0, clampedKey) 202 | 203 | if let value = convertToDelay(clampedPointer) { 204 | return value 205 | } 206 | 207 | throw GifParseError.noTimingInfo 208 | } 209 | 210 | return frameDelays 211 | } 212 | 213 | /// Compute backing data for this gif 214 | /// 215 | /// - Parameter delaysArray: decoded delay times for this gif 216 | /// - Parameter levelOfIntegrity: 0 to 1, 1 meaning no frame skipping 217 | private func calculateFrameDelay(_ delaysArray: [Float], levelOfIntegrity: GifLevelOfIntegrity) { 218 | let levelOfIntegrity = max(0, min(1, levelOfIntegrity)) 219 | var delays = delaysArray 220 | 221 | var displayRefreshFactors = [Int]() 222 | 223 | displayRefreshFactors.append(contentsOf: [60, 30, 20, 15, 12, 10, 6, 5, 4, 3, 2, 1]) 224 | 225 | let defaultMaxFramePerSecond = 60 226 | var maxFramePerSecond = defaultMaxFramePerSecond 227 | 228 | if #available(iOS 10.3, *) { 229 | // Will be 120 on devices with ProMotion display, 60 otherwise. 230 | let maximumFramesPerSecond = UIScreen.main.maximumFramesPerSecond 231 | if maximumFramesPerSecond == 120 { 232 | maxFramePerSecond = maximumFramesPerSecond 233 | displayRefreshFactors.insert(maximumFramesPerSecond, at: 0) 234 | } 235 | } 236 | 237 | let frameRateRatio = Float(maxFramePerSecond / defaultMaxFramePerSecond) 238 | 239 | // frame numbers per second 240 | let displayRefreshRates = displayRefreshFactors.map { maxFramePerSecond / $0 } 241 | 242 | // time interval per frame 243 | let displayRefreshDelayTime = displayRefreshRates.map { frameRateRatio / Float($0) } 244 | 245 | // calculate the time when each frame should be displayed at(start at 0) 246 | for i in delays.indices.dropFirst() { 247 | delays[i] += delays[i - 1] 248 | } 249 | 250 | //find the appropriate Factors then BREAK 251 | for (i, delayTime) in displayRefreshDelayTime.enumerated() { 252 | let displayPosition = delays.map { Int($0 / delayTime) } 253 | 254 | var frameLoseCount: Float = 0 255 | 256 | for j in displayPosition.indices.dropFirst() where displayPosition[j] == displayPosition[j - 1] { 257 | frameLoseCount += 1 258 | } 259 | 260 | if displayPosition.first == 0 { 261 | frameLoseCount += 1 262 | } 263 | 264 | if frameLoseCount <= Float(displayPosition.count) * (1 - levelOfIntegrity) || i == displayRefreshDelayTime.count - 1 { 265 | imageCount = displayPosition.last 266 | displayRefreshFactor = displayRefreshFactors[i] 267 | displayOrder = [] 268 | var oldIndex = 0 269 | var newIndex = 1 270 | let imageCount = self.imageCount ?? 0 271 | 272 | while newIndex <= imageCount && oldIndex < displayPosition.count { 273 | if newIndex <= displayPosition[oldIndex] { 274 | displayOrder?.append(oldIndex) 275 | newIndex += 1 276 | } else { 277 | oldIndex += 1 278 | } 279 | } 280 | break 281 | } 282 | } 283 | } 284 | 285 | /// Compute frame size for this gif 286 | private func calculateFrameSize(){ 287 | guard let imageSource = imageSource, 288 | let imageCount = imageCount, 289 | let cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) else { 290 | return 291 | } 292 | 293 | let image = UIImage(cgImage: cgImage) 294 | imageSize = Int(image.size.height * image.size.width * 4) * imageCount / 1_000_000 295 | } 296 | } 297 | 298 | // MARK: - Properties 299 | 300 | private let _imageSourceKey = malloc(4) 301 | private let _displayRefreshFactorKey = malloc(4) 302 | private let _imageSizeKey = malloc(4) 303 | private let _imageCountKey = malloc(4) 304 | private let _displayOrderKey = malloc(4) 305 | private let _imageDataKey = malloc(4) 306 | 307 | public extension UIImage { 308 | 309 | var imageSource: CGImageSource? { 310 | get { 311 | let result = objc_getAssociatedObject(self, _imageSourceKey!) 312 | return result == nil ? nil : (result as! CGImageSource) 313 | } 314 | set { 315 | objc_setAssociatedObject(self, _imageSourceKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 316 | } 317 | } 318 | 319 | var displayRefreshFactor: Int?{ 320 | get { return objc_getAssociatedObject(self, _displayRefreshFactorKey!) as? Int } 321 | set { objc_setAssociatedObject(self, _displayRefreshFactorKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 322 | } 323 | 324 | var imageSize: Int?{ 325 | get { return objc_getAssociatedObject(self, _imageSizeKey!) as? Int } 326 | set { objc_setAssociatedObject(self, _imageSizeKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 327 | } 328 | 329 | var imageCount: Int?{ 330 | get { return objc_getAssociatedObject(self, _imageCountKey!) as? Int } 331 | set { objc_setAssociatedObject(self, _imageCountKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 332 | } 333 | 334 | var displayOrder: [Int]?{ 335 | get { return objc_getAssociatedObject(self, _displayOrderKey!) as? [Int] } 336 | set { objc_setAssociatedObject(self, _displayOrderKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 337 | } 338 | 339 | var imageData:Data? { 340 | get { 341 | let result = objc_getAssociatedObject(self, _imageDataKey!) 342 | return result == nil ? nil : (result as? Data) 343 | } 344 | set { 345 | objc_setAssociatedObject(self, _imageDataKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 346 | } 347 | } 348 | } 349 | 350 | extension String { 351 | fileprivate func pathExtension() -> String { 352 | return (self as NSString).pathExtension 353 | } 354 | } 355 | 356 | #endif 357 | -------------------------------------------------------------------------------- /SwiftyGif/UIImageView+SwiftyGif.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView+SwiftyGif.swift 3 | // 4 | 5 | #if !os(macOS) 6 | 7 | import ImageIO 8 | import UIKit 9 | 10 | @objc public protocol SwiftyGifDelegate { 11 | @objc optional func gifDidStart(sender: UIImageView) 12 | @objc optional func gifDidLoop(sender: UIImageView) 13 | @objc optional func gifDidStop(sender: UIImageView) 14 | @objc optional func gifURLDidFinish(sender: UIImageView) 15 | @objc optional func gifURLDidFail(sender: UIImageView, url: URL, error: Error?) 16 | } 17 | 18 | public extension UIImageView { 19 | /// Set an image and a manager to an existing UIImageView. If the image is not an GIF image, set it in normal way and remove self form SwiftyGifManager 20 | /// 21 | /// WARNING : this overwrite any previous gif. 22 | /// - Parameter gifImage: The UIImage containing the gif backing data 23 | /// - Parameter manager: The manager to handle the gif display 24 | /// - Parameter loopCount: The number of loops we want for this gif. -1 means infinite. 25 | func setImage(_ image: UIImage, manager: SwiftyGifManager = .defaultManager, loopCount: Int = -1) { 26 | if let _ = image.imageData { 27 | setGifImage(image, manager: manager, loopCount: loopCount) 28 | } else { 29 | manager.deleteImageView(self) 30 | self.image = image 31 | } 32 | } 33 | } 34 | 35 | public extension UIImageView { 36 | 37 | // MARK: - Inits 38 | 39 | /// Convenience initializer. Creates a gif holder (defaulted to infinite loop). 40 | /// 41 | /// - Parameter gifImage: The UIImage containing the gif backing data 42 | /// - Parameter manager: The manager to handle the gif display 43 | convenience init(gifImage: UIImage, manager: SwiftyGifManager = .defaultManager, loopCount: Int = -1) { 44 | self.init() 45 | setGifImage(gifImage,manager: manager, loopCount: loopCount) 46 | } 47 | 48 | /// Convenience initializer. Creates a gif holder (defaulted to infinite loop). 49 | /// 50 | /// - Parameter gifImage: The UIImage containing the gif backing data 51 | /// - Parameter manager: The manager to handle the gif display 52 | convenience init(gifURL: URL, manager: SwiftyGifManager = .defaultManager, loopCount: Int = -1) { 53 | self.init() 54 | setGifFromURL(gifURL, manager: manager, loopCount: loopCount) 55 | } 56 | 57 | /// Set a gif image and a manager to an existing UIImageView. 58 | /// 59 | /// WARNING : this overwrite any previous gif. 60 | /// - Parameter gifImage: The UIImage containing the gif backing data 61 | /// - Parameter loopCount: The number of loops we want for this gif. -1 means infinite. 62 | @objc func setGifImage(_ gifImage: UIImage, loopCount: Int = -1) { 63 | if let imageData = gifImage.imageData, (gifImage.imageCount ?? 0) < 1 { 64 | image = UIImage(data: imageData) 65 | return 66 | } 67 | 68 | let manager: SwiftyGifManager = .defaultManager 69 | 70 | self.loopCount = loopCount 71 | self.gifImage = gifImage 72 | animationManager = manager 73 | syncFactor = 0 74 | displayOrderIndex = 0 75 | cache = NSCache() 76 | haveCache = false 77 | 78 | if let source = gifImage.imageSource, let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) { 79 | currentImage = UIImage(cgImage: cgImage) 80 | 81 | if manager.addImageView(self) { 82 | startDisplay() 83 | startAnimatingGif() 84 | } 85 | } 86 | } 87 | 88 | /// Set a gif image and a manager to an existing UIImageView. 89 | /// 90 | /// WARNING : this overwrite any previous gif. 91 | /// - Parameter gifImage: The UIImage containing the gif backing data 92 | /// - Parameter manager: The manager to handle the gif display 93 | /// - Parameter loopCount: The number of loops we want for this gif. -1 means infinite. 94 | func setGifImage(_ gifImage: UIImage, manager: SwiftyGifManager = .defaultManager, loopCount: Int = -1) { 95 | if let imageData = gifImage.imageData, (gifImage.imageCount ?? 0) < 1 { 96 | image = UIImage(data: imageData) 97 | return 98 | } 99 | 100 | self.loopCount = loopCount 101 | self.gifImage = gifImage 102 | animationManager = manager 103 | syncFactor = 0 104 | displayOrderIndex = 0 105 | cache = NSCache() 106 | haveCache = false 107 | 108 | if let source = gifImage.imageSource, let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) { 109 | currentImage = UIImage(cgImage: cgImage) 110 | 111 | if manager.addImageView(self) { 112 | startDisplay() 113 | startAnimatingGif() 114 | } 115 | } 116 | } 117 | } 118 | 119 | // MARK: - Download gif 120 | 121 | public extension UIImageView { 122 | 123 | /// Download gif image and sets it. 124 | /// 125 | /// - Parameters: 126 | /// - url: The URL pointing to the gif data 127 | /// - manager: The manager to handle the gif display 128 | /// - loopCount: The number of loops we want for this gif. -1 means infinite. 129 | /// - showLoader: Show UIActivityIndicatorView or not 130 | /// - Returns: An URL session task. Note: You can cancel the downloading task if it needed. 131 | @discardableResult 132 | func setGifFromURL(_ url: URL, 133 | manager: SwiftyGifManager = .defaultManager, 134 | loopCount: Int = -1, 135 | levelOfIntegrity: GifLevelOfIntegrity = .default, 136 | session: URLSession = URLSession.shared, 137 | showLoader: Bool = true, 138 | customLoader: UIView? = nil, 139 | callback: @escaping (Result) -> Void = {_ in } 140 | ) -> URLSessionDataTask? { 141 | 142 | if let data = manager.remoteCache[url] { 143 | self.parseDownloadedGif(url: url, 144 | data: data, 145 | error: nil, 146 | manager: manager, 147 | loopCount: loopCount, 148 | levelOfIntegrity: levelOfIntegrity, 149 | callback: callback) 150 | return nil 151 | } 152 | 153 | stopAnimatingGif() 154 | 155 | let loader: UIView? = showLoader ? createLoader(from: customLoader) : nil 156 | 157 | let task = session.dataTask(with: url) { [weak self] data, _, error in 158 | DispatchQueue.main.async { 159 | loader?.removeFromSuperview() 160 | self?.parseDownloadedGif(url: url, 161 | data: data, 162 | error: error, 163 | manager: manager, 164 | loopCount: loopCount, 165 | levelOfIntegrity: levelOfIntegrity, 166 | callback: callback) 167 | } 168 | } 169 | 170 | task.resume() 171 | 172 | return task 173 | } 174 | 175 | private func createLoader(from view: UIView? = nil) -> UIView { 176 | let loader = view ?? UIActivityIndicatorView() 177 | addSubview(loader) 178 | loader.translatesAutoresizingMaskIntoConstraints = false 179 | 180 | addConstraint(NSLayoutConstraint( 181 | item: loader, 182 | attribute: .centerX, 183 | relatedBy: .equal, 184 | toItem: self, 185 | attribute: .centerX, 186 | multiplier: 1, 187 | constant: 0)) 188 | 189 | addConstraint(NSLayoutConstraint( 190 | item: loader, 191 | attribute: .centerY, 192 | relatedBy: .equal, 193 | toItem: self, 194 | attribute: .centerY, 195 | multiplier: 1, 196 | constant: 0)) 197 | 198 | (loader as? UIActivityIndicatorView)?.startAnimating() 199 | 200 | return loader 201 | } 202 | 203 | private func parseDownloadedGif(url: URL, 204 | data: Data?, 205 | error: Error?, 206 | manager: SwiftyGifManager, 207 | loopCount: Int, 208 | levelOfIntegrity: GifLevelOfIntegrity, 209 | callback: (Result) -> Void) { 210 | guard let data = data else { 211 | report(url: url, error: error) 212 | callback(.failure(error ?? SwiftyGifError.noGifData)) 213 | return 214 | } 215 | 216 | do { 217 | let image = try UIImage(gifData: data, levelOfIntegrity: levelOfIntegrity) 218 | manager.remoteCache[url] = data 219 | setGifImage(image, manager: manager, loopCount: loopCount) 220 | startAnimatingGif() 221 | delegate?.gifURLDidFinish?(sender: self) 222 | callback(.success(data)) 223 | } catch { 224 | report(url: url, error: error) 225 | callback(.failure(error)) 226 | } 227 | } 228 | 229 | private func report(url: URL, error: Error?) { 230 | delegate?.gifURLDidFail?(sender: self, url: url, error: error) 231 | } 232 | } 233 | 234 | // MARK: - Logic 235 | 236 | public extension UIImageView { 237 | 238 | /// Start displaying the gif for this UIImageView. 239 | private func startDisplay() { 240 | displaying = true 241 | updateCache() 242 | } 243 | 244 | /// Stop displaying the gif for this UIImageView. 245 | private func stopDisplay() { 246 | displaying = false 247 | updateCache() 248 | } 249 | 250 | /// Start displaying the gif for this UIImageView. 251 | @objc func startAnimatingGif() { 252 | isPlaying = true 253 | } 254 | 255 | /// Stop displaying the gif for this UIImageView. 256 | @objc func stopAnimatingGif() { 257 | isPlaying = false 258 | } 259 | 260 | /// Check if this imageView is currently playing a gif 261 | /// 262 | /// - Returns wether the gif is currently playing 263 | @objc func isAnimatingGif() -> Bool{ 264 | return isPlaying 265 | } 266 | 267 | /// Show a specific frame based on a delta from current frame 268 | /// 269 | /// - Parameter delta: The delsta from current frame we want 270 | @objc func showFrameForIndexDelta(_ delta: Int) { 271 | guard let gifImage = gifImage else { return } 272 | var nextIndex = displayOrderIndex + delta 273 | 274 | while nextIndex >= gifImage.framesCount() { 275 | nextIndex -= gifImage.framesCount() 276 | } 277 | 278 | while nextIndex < 0 { 279 | nextIndex += gifImage.framesCount() 280 | } 281 | 282 | showFrameAtIndex(nextIndex) 283 | } 284 | 285 | /// Show a specific frame 286 | /// 287 | /// - Parameter index: The index of frame to show 288 | @objc func showFrameAtIndex(_ index: Int) { 289 | displayOrderIndex = index 290 | updateFrame() 291 | } 292 | 293 | /// Update cache for the current imageView. 294 | func updateCache() { 295 | guard let animationManager = animationManager else { return } 296 | 297 | if animationManager.hasCache(self) && !haveCache { 298 | prepareCache() 299 | haveCache = true 300 | } else if !animationManager.hasCache(self) && haveCache { 301 | cache?.removeAllObjects() 302 | haveCache = false 303 | } 304 | } 305 | 306 | /// Update current image displayed. This method is called by the manager. 307 | func updateCurrentImage() { 308 | if displaying { 309 | updateFrame() 310 | updateIndex() 311 | 312 | if loopCount == 0 || !isDisplayedInScreen(self) || !isPlaying { 313 | stopDisplay() 314 | } 315 | } else { 316 | if isDisplayedInScreen(self) && loopCount != 0 && isPlaying { 317 | startDisplay() 318 | } 319 | 320 | if isDiscarded(self) { 321 | animationManager?.deleteImageView(self) 322 | } 323 | } 324 | } 325 | 326 | /// Force update frame 327 | private func updateFrame() { 328 | if haveCache, let image = cache?.object(forKey: displayOrderIndex as AnyObject) as? UIImage { 329 | currentImage = image 330 | } else { 331 | currentImage = frameAtIndex(index: currentFrameIndex()) 332 | } 333 | } 334 | 335 | /// Get current frame index 336 | func currentFrameIndex() -> Int{ 337 | return displayOrderIndex 338 | } 339 | 340 | /// Get frame at specific index 341 | func frameAtIndex(index: Int) -> UIImage { 342 | guard let gifImage = gifImage, 343 | let imageSource = gifImage.imageSource, 344 | let displayOrder = gifImage.displayOrder, index < displayOrder.count, 345 | let cgImage = CGImageSourceCreateImageAtIndex(imageSource, displayOrder[index], nil) else { 346 | return UIImage() 347 | } 348 | 349 | return UIImage(cgImage: cgImage) 350 | } 351 | 352 | /// Check if the imageView has been discarded and is not in the view hierarchy anymore. 353 | /// 354 | /// - Returns : A boolean for weather the imageView was discarded 355 | func isDiscarded(_ imageView: UIView?) -> Bool { 356 | return imageView?.superview == nil 357 | } 358 | 359 | /// Check if the imageView is displayed. 360 | /// 361 | /// - Returns : A boolean for weather the imageView is displayed 362 | func isDisplayedInScreen(_ imageView: UIView?) -> Bool { 363 | guard !isHidden, let imageView = imageView else { 364 | return false 365 | } 366 | 367 | let screenRect = UIScreen.main.bounds 368 | let viewRect = imageView.convert(bounds, to:nil) 369 | let intersectionRect = viewRect.intersection(screenRect) 370 | 371 | return window != nil && !intersectionRect.isEmpty && !intersectionRect.isNull 372 | } 373 | 374 | @objc func clear() { 375 | if let gifImage = gifImage { 376 | gifImage.clear() 377 | } 378 | 379 | gifImage = nil 380 | currentImage = nil 381 | cache?.removeAllObjects() 382 | animationManager = nil 383 | image = nil 384 | } 385 | 386 | /// Update loop count and sync factor. 387 | private func updateIndex() { 388 | guard let gif = self.gifImage, 389 | let displayRefreshFactor = gif.displayRefreshFactor, 390 | displayRefreshFactor > 0 else { 391 | return 392 | } 393 | 394 | syncFactor = (syncFactor + 1) % displayRefreshFactor 395 | 396 | if syncFactor == 0, let imageCount = gif.imageCount, imageCount > 0 { 397 | displayOrderIndex = (displayOrderIndex+1) % imageCount 398 | 399 | if displayOrderIndex == 0 { 400 | if loopCount == -1 { 401 | delegate?.gifDidLoop?(sender: self) 402 | } else if loopCount > 1 { 403 | delegate?.gifDidLoop?(sender: self) 404 | loopCount -= 1 405 | } else { 406 | delegate?.gifDidStop?(sender: self) 407 | loopCount -= 1 408 | } 409 | } 410 | } 411 | } 412 | 413 | /// Prepare the cache by adding every images of the gif to an NSCache object. 414 | private func prepareCache() { 415 | guard let cache = self.cache else { return } 416 | 417 | cache.removeAllObjects() 418 | 419 | guard let gif = self.gifImage, 420 | let displayOrder = gif.displayOrder, 421 | let imageSource = gif.imageSource else { return } 422 | 423 | for (i, order) in displayOrder.enumerated() { 424 | guard let cgImage = CGImageSourceCreateImageAtIndex(imageSource, order, nil) else { continue } 425 | 426 | cache.setObject(UIImage(cgImage: cgImage), forKey: i as AnyObject) 427 | } 428 | } 429 | } 430 | 431 | // MARK: - Dynamic properties 432 | 433 | private let _gifImageKey = malloc(4) 434 | private let _cacheKey = malloc(4) 435 | private let _currentImageKey = malloc(4) 436 | private let _displayOrderIndexKey = malloc(4) 437 | private let _syncFactorKey = malloc(4) 438 | private let _haveCacheKey = malloc(4) 439 | private let _loopCountKey = malloc(4) 440 | private let _displayingKey = malloc(4) 441 | private let _isPlayingKey = malloc(4) 442 | private let _animationManagerKey = malloc(4) 443 | private let _delegateKey = malloc(4) 444 | 445 | public extension UIImageView { 446 | 447 | var gifImage: UIImage? { 448 | get { return possiblyNil(_gifImageKey) } 449 | set { objc_setAssociatedObject(self, _gifImageKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 450 | } 451 | 452 | @objc var currentImage: UIImage? { 453 | get { return possiblyNil(_currentImageKey) } 454 | set { objc_setAssociatedObject(self, _currentImageKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 455 | } 456 | 457 | private var displayOrderIndex: Int { 458 | get { return value(_displayOrderIndexKey, 0) } 459 | set { objc_setAssociatedObject(self, _displayOrderIndexKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 460 | } 461 | 462 | private var syncFactor: Int { 463 | get { return value(_syncFactorKey, 0) } 464 | set { objc_setAssociatedObject(self, _syncFactorKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 465 | } 466 | 467 | @objc var loopCount: Int { 468 | get { return value(_loopCountKey, 0) } 469 | set { objc_setAssociatedObject(self, _loopCountKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 470 | } 471 | 472 | var animationManager: SwiftyGifManager? { 473 | get { return (objc_getAssociatedObject(self, _animationManagerKey!) as? SwiftyGifManager) } 474 | set { objc_setAssociatedObject(self, _animationManagerKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 475 | } 476 | 477 | @objc var delegate: SwiftyGifDelegate? { 478 | get { return (objc_getAssociatedWeakObject(self, _delegateKey!) as? SwiftyGifDelegate) } 479 | set { objc_setAssociatedWeakObject(self, _delegateKey!, newValue) } 480 | } 481 | 482 | private var haveCache: Bool { 483 | get { return value(_haveCacheKey, false) } 484 | set { objc_setAssociatedObject(self, _haveCacheKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 485 | } 486 | 487 | @objc var displaying: Bool { 488 | get { return value(_displayingKey, false) } 489 | set { objc_setAssociatedObject(self, _displayingKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 490 | } 491 | 492 | private var isPlaying: Bool { 493 | get { 494 | return value(_isPlayingKey, false) 495 | } 496 | set { 497 | objc_setAssociatedObject(self, _isPlayingKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 498 | 499 | if newValue { 500 | delegate?.gifDidStart?(sender: self) 501 | } else { 502 | delegate?.gifDidStop?(sender: self) 503 | } 504 | } 505 | } 506 | 507 | private var cache: NSCache? { 508 | get { return (objc_getAssociatedObject(self, _cacheKey!) as? NSCache) } 509 | set { objc_setAssociatedObject(self, _cacheKey!, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 510 | } 511 | 512 | private func value(_ key:UnsafeMutableRawPointer?, _ defaultValue:T) -> T { 513 | return (objc_getAssociatedObject(self, key!) as? T) ?? defaultValue 514 | } 515 | 516 | private func possiblyNil(_ key:UnsafeMutableRawPointer?) -> T? { 517 | let result = objc_getAssociatedObject(self, key!) 518 | 519 | if result == nil { 520 | return nil 521 | } 522 | 523 | return (result as? T) 524 | } 525 | } 526 | 527 | #endif 528 | -------------------------------------------------------------------------------- /SwiftyGifExample/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/SwiftyGifExample/2.gif -------------------------------------------------------------------------------- /SwiftyGifExample/3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/SwiftyGifExample/3.gif -------------------------------------------------------------------------------- /SwiftyGifExample/4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/SwiftyGifExample/4.gif -------------------------------------------------------------------------------- /SwiftyGifExample/5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/SwiftyGifExample/5.gif -------------------------------------------------------------------------------- /SwiftyGifExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftyGif 4 | // 5 | // Created by Alexis Creuzot on 28/03/16. 6 | // Copyright © 2016 alexiscreuzot. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /SwiftyGifExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /SwiftyGifExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SwiftyGifExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /SwiftyGifExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 124 | 138 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /SwiftyGifExample/Cell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cell.swift 3 | // SwiftyGif 4 | // 5 | // Created by Alexis Creuzot on 04/04/16. 6 | // Copyright © 2016 alexiscreuzot. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class Cell: UITableViewCell { 12 | 13 | @IBOutlet weak var gifImageView: UIImageView! 14 | 15 | override func awakeFromNib() { 16 | super.awakeFromNib() 17 | self.backgroundColor = .clear 18 | self.contentView.backgroundColor = .clear 19 | // Initialization code 20 | } 21 | 22 | override func prepareForReuse() { 23 | super.prepareForReuse() 24 | self.gifImageView.clear() 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /SwiftyGifExample/DetailController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailController.swift 3 | // SwiftyGif 4 | // 5 | // Created by Alexis Creuzot on 04/04/16. 6 | // Copyright © 2016 alexiscreuzot. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftyGif 11 | 12 | class DetailController: UIViewController { 13 | 14 | @IBOutlet private weak var imageView: UIImageView! 15 | @IBOutlet private weak var playPauseButton: UIButton! 16 | @IBOutlet private weak var forwardButton: UIButton! 17 | @IBOutlet private weak var rewindButton: UIButton! 18 | 19 | var path: String? 20 | let gifManager = SwiftyGifManager(memoryLimit: 60) 21 | var _rewindTimer: Timer? 22 | var _forwardTimer: Timer? 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | if let path = self.path { 28 | if let image = try? UIImage(imageName: path) { 29 | self.imageView.setImage(image, manager: gifManager, loopCount: -1) 30 | } else if let url = URL.init(string: path) { 31 | let loader = UIActivityIndicatorView.init(style: .white) 32 | self.imageView.setGifFromURL(url, customLoader: loader) 33 | } else { 34 | self.imageView.clear() 35 | } 36 | } 37 | 38 | // Gestures for gif control 39 | let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture)) 40 | self.imageView.addGestureRecognizer(panGesture) 41 | self.imageView.isUserInteractionEnabled = true 42 | self.imageView.delegate = self 43 | 44 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.togglePlay)) 45 | self.imageView.addGestureRecognizer(tapGesture) 46 | } 47 | 48 | // PRAGMA - Logic 49 | 50 | @objc func rewind(){ 51 | self.imageView.showFrameForIndexDelta(-1) 52 | } 53 | 54 | @objc func forward(){ 55 | self.imageView.showFrameForIndexDelta(1) 56 | } 57 | 58 | func stop(){ 59 | self.imageView.stopAnimatingGif() 60 | self.playPauseButton.setTitle("►", for: .normal) 61 | } 62 | 63 | func play(){ 64 | self.imageView.startAnimatingGif() 65 | self.playPauseButton.setTitle("❚❚", for: .normal) 66 | } 67 | 68 | // PRAGMA - Actions 69 | 70 | @IBAction func togglePlay(){ 71 | if self.imageView.isAnimatingGif() { 72 | stop() 73 | }else { 74 | play() 75 | } 76 | } 77 | 78 | @IBAction func rewindDown(){ 79 | stop() 80 | _rewindTimer = Timer.scheduledTimer(timeInterval: 1.0/30.0, target: self, selector: #selector(self.rewind), userInfo: nil, repeats: true) 81 | } 82 | 83 | @IBAction func rewindUp(){ 84 | _rewindTimer?.invalidate() 85 | _rewindTimer = nil 86 | } 87 | 88 | @IBAction func forwardDown(){ 89 | stop() 90 | _forwardTimer = Timer.scheduledTimer(timeInterval: 1.0/30.0, target: self, selector: #selector(self.forward), userInfo: nil, repeats: true) 91 | } 92 | 93 | @IBAction func forwardUp(){ 94 | _forwardTimer?.invalidate() 95 | _forwardTimer = nil 96 | } 97 | 98 | // PRAGMA - Gestures 99 | 100 | @objc func panGesture(sender:UIPanGestureRecognizer){ 101 | 102 | switch sender.state { 103 | case .began: 104 | stop() 105 | break 106 | 107 | case .changed: 108 | if sender.velocity(in: sender.view).x > 0 { 109 | forward() 110 | } else{ 111 | rewind() 112 | } 113 | break 114 | 115 | default: 116 | break 117 | } 118 | } 119 | } 120 | 121 | extension DetailController : SwiftyGifDelegate { 122 | 123 | func gifDidStart(sender: UIImageView) { 124 | print("gifDidStart") 125 | } 126 | 127 | func gifDidLoop(sender: UIImageView) { 128 | print("gifDidLoop") 129 | } 130 | 131 | func gifDidStop(sender: UIImageView) { 132 | print("gifDidStop") 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /SwiftyGifExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /SwiftyGifExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SwiftyGifManager 4 | // 5 | 6 | import UIKit 7 | import SwiftyGif 8 | 9 | class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { 10 | 11 | @IBOutlet weak var tableView: UITableView! 12 | 13 | let gifManager = SwiftyGifManager(memoryLimit:100) 14 | let images = [ 15 | "https://media.giphy.com/media/5tkEiBCurffluctzB7/giphy.gif", 16 | "2.gif", 17 | "https://media.giphy.com/media/5xtDarmOIekHPQSZEpq/giphy.gif", 18 | "3.gif", 19 | "https://media.giphy.com/media/3oEjHM2xehrp0lv6bC/giphy.gif", 20 | "5.gif", 21 | "https://media.giphy.com/media/l1J9qg0MqSZcQTuGk/giphy.gif", 22 | "4.gif", 23 | ] 24 | 25 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 26 | if let detailController = segue.destination as? DetailController { 27 | let indexPath = self.tableView.indexPathForSelectedRow 28 | detailController.path = images[indexPath!.row] 29 | } 30 | } 31 | 32 | // MARK: - TableView Datasource 33 | 34 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 35 | return images.count 36 | } 37 | 38 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 39 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath as IndexPath) as! Cell 40 | 41 | if let image = try? UIImage(imageName: images[indexPath.row]) { 42 | cell.gifImageView.setImage(image, manager: gifManager, loopCount: -1) 43 | } else if let url = URL.init(string: images[indexPath.row]) { 44 | let loader = UIActivityIndicatorView.init(style: .white) 45 | cell.gifImageView.setGifFromURL(url, customLoader: loader) 46 | } else { 47 | cell.gifImageView.clear() 48 | } 49 | 50 | return cell 51 | } 52 | 53 | // MARK: - TableView Delegate 54 | 55 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 56 | return 200 57 | } 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /SwiftyGifMacExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftyGifMacExample 4 | // 5 | // Created by Carlo Rapisarda on 2020-08-04. 6 | // Copyright © 2020 alexiscreuzot. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | 15 | 16 | func applicationDidFinishLaunching(_ aNotification: Notification) { 17 | // Insert code here to initialize your application 18 | } 19 | 20 | func applicationWillTerminate(_ aNotification: Notification) { 21 | // Insert code here to tear down your application 22 | } 23 | 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /SwiftyGifMacExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SwiftyGifMacExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftyGifMacExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2020 alexiscreuzot. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | NSSupportsAutomaticTermination 32 | 33 | NSSupportsSuddenTermination 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /SwiftyGifMacExample/SwiftyGifMacExample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /SwiftyGifMacExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SwiftyGifMacExample 4 | // 5 | // Created by Carlo Rapisarda on 2020-08-04. 6 | // Copyright © 2020 alexiscreuzot. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SwiftyGif 11 | 12 | class ViewController: NSViewController { 13 | 14 | @IBOutlet private var gifImageView: NSImageView! 15 | 16 | let gifManager = SwiftyGifManager(memoryLimit:100) 17 | let images = [ 18 | "https://media.giphy.com/media/5tkEiBCurffluctzB7/giphy.gif", 19 | "2.gif", 20 | "https://media.giphy.com/media/5xtDarmOIekHPQSZEpq/giphy.gif", 21 | "3.gif", 22 | "https://media.giphy.com/media/3oEjHM2xehrp0lv6bC/giphy.gif", 23 | "5.gif", 24 | "https://media.giphy.com/media/l1J9qg0MqSZcQTuGk/giphy.gif", 25 | "4.gif", 26 | ] 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | 31 | let imageSource = images[0] 32 | 33 | if let image = try? NSImage(imageName: imageSource) { 34 | gifImageView.setImage(image, manager: gifManager, loopCount: -1) 35 | } else if let url = URL.init(string: imageSource) { 36 | gifImageView.setGifFromURL(url, showLoader: true) 37 | } else { 38 | gifImageView.clear() 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SwiftyGifTests/Images/20000x20000.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/SwiftyGifTests/Images/20000x20000.gif -------------------------------------------------------------------------------- /SwiftyGifTests/Images/no_property_dictionary.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/SwiftyGifTests/Images/no_property_dictionary.gif -------------------------------------------------------------------------------- /SwiftyGifTests/Images/single_frame_Zt2012.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/SwiftyGifTests/Images/single_frame_Zt2012.gif -------------------------------------------------------------------------------- /SwiftyGifTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SwiftyGifTests/SwiftyGifTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyGifTests.swift 3 | // SwiftyGifTests 4 | // 5 | // Created by Bill, Chan Yiu Por on 03/06/19. 6 | // Copyright © 2019 BillChan. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import SwiftyGif 11 | import SnapshotTesting 12 | 13 | extension XCTestCase { 14 | 15 | func data(filename: String) -> Data? { 16 | let data = try? Data(contentsOf: url(for: filename)) 17 | 18 | return data 19 | } 20 | 21 | func url(for filename: String) -> URL { 22 | let bundle = Bundle(for: type(of: self)) 23 | 24 | let url = bundle.url(forResource: filename, withExtension: "") 25 | 26 | if let isFileURL = url?.isFileURL { 27 | XCTAssert(isFileURL) 28 | } else { 29 | XCTFail("\(filename) does not exist") 30 | } 31 | 32 | return url! 33 | } 34 | } 35 | 36 | final class SwiftyGifTests: XCTestCase { 37 | 38 | var sut: UIImage! 39 | let gifManager = SwiftyGifManager(memoryLimit:100) 40 | 41 | override func setUp() { 42 | // Put setup code here. This method is called before the invocation of each test method in the class. 43 | } 44 | 45 | override func tearDown() { 46 | // Put teardown code here. This method is called after the invocation of each test method in the class. 47 | sut = nil 48 | } 49 | 50 | 51 | @discardableResult 52 | private func createImage(gifName: String, file: StaticString = #file, testName: String = #function, line: UInt = #line) -> UIImage! { 53 | let data = self.data(filename: gifName)! 54 | 55 | do { 56 | sut = try UIImage(imageData: data) 57 | } catch let error { 58 | XCTFail(error.localizedDescription) 59 | return nil 60 | } 61 | 62 | return sut 63 | } 64 | 65 | private func createImageView(gifName: String, gifManager: SwiftyGifManager = .defaultManager, file: StaticString = #file, testName: String = #function, line: UInt = #line) -> UIImageView! { 66 | 67 | createImage(gifName: gifName) 68 | 69 | if sut == nil { 70 | return nil 71 | } 72 | 73 | let imageView = UIImageView() 74 | imageView.setImage(sut, manager: gifManager) 75 | 76 | return imageView 77 | } 78 | 79 | private func asset(gifName: String, file: StaticString = #file, testName: String = #function, line: UInt = #line) { 80 | let imageView = createImageView(gifName: gifName) 81 | 82 | // can not snapshot the UIImageView directly since it would produce nil image. Snapshot imageView.currentImage instead 83 | let image: UIImage 84 | if let gifImage = imageView?.currentImage { 85 | image = gifImage 86 | } else { 87 | image = imageView!.image! 88 | } 89 | assertSnapshot(matching: image, as: .image, file: file, testName: testName, line: line) 90 | } 91 | 92 | func testThatNonAnimatedGIFCanBeLoadedWithUIImage() { 93 | // GIVEN 94 | let gifName = "single_frame_Zt2012.gif" 95 | let data = self.data(filename: gifName)! 96 | 97 | // WHEN 98 | sut = UIImage(data: data) 99 | 100 | // THEN 101 | 102 | let imageView = UIImageView(image: sut) 103 | 104 | assertSnapshot(matching: imageView, as: .image) 105 | } 106 | 107 | func testThatNonAnimatedGIFCanBeLoaded() { 108 | asset(gifName: "single_frame_Zt2012.gif") 109 | } 110 | 111 | func testThatVeryBigGIFCanBeLoaded() { 112 | asset(gifName: "20000x20000.gif") 113 | } 114 | 115 | func DISABLE_testThatVeryBigGIFCanBeLoaded() { ///TODO: used 18GB of memory and the output snapshot PNG is 31 MB 116 | // GIVEN 117 | let gifName = "20000x20000.gif" 118 | let data = self.data(filename: gifName)! 119 | 120 | // WHEN 121 | sut = UIImage(data: data) 122 | 123 | // THEN 124 | 125 | let imageView = UIImageView(image: sut) 126 | 127 | assertSnapshot(matching: imageView, as: .image) 128 | } 129 | 130 | func testThatGIFWithoutkCGImagePropertyGIFDictionaryCanBeLoaded() { 131 | asset(gifName: "no_property_dictionary.gif") 132 | } 133 | 134 | func testThatNonGifImageCanBeLoaded() { 135 | asset(gifName: "sample.jpg") 136 | } 137 | 138 | func testThat15MBGIFCanBeLoaded() { 139 | asset(gifName: "15MB_Einstein_rings_zoom.gif") 140 | } 141 | 142 | func testThatImageViewCanBeRecycledForGIF() { 143 | let data = self.data(filename: "sample.jpg")! 144 | 145 | let imageView = UIImageView() 146 | 147 | let normalImage = UIImage(data: data)! 148 | imageView.setImage(normalImage, manager: gifManager) 149 | imageView.frame = CGRect(origin: .zero, size: normalImage.size) 150 | 151 | 152 | XCTAssertFalse(gifManager.containsImageView(imageView)) 153 | ///snapshot of the normal image 154 | assertSnapshot(matching: imageView, as: .image) 155 | 156 | createImage(gifName: "15MB_Einstein_rings_zoom.gif") 157 | imageView.setGifImage(sut, manager: gifManager) 158 | 159 | ///snapshot of the GIF 160 | assertSnapshot(matching: imageView.currentImage!, as: .image) 161 | 162 | ///snapshot of the normal image 163 | gifManager.deleteImageView(imageView) 164 | 165 | imageView.image = normalImage 166 | assertSnapshot(matching: imageView, as: .image) 167 | } 168 | 169 | /// GIF -> normal image recycling 170 | func testThatImageViewCanBeRecycledForNormalImage() { 171 | // GIVEN 172 | let imageView = createImageView(gifName: "15MB_Einstein_rings_zoom.gif", gifManager: gifManager)! 173 | 174 | ///snapshot of the GIF 175 | assertSnapshot(matching: imageView.currentImage!, as: .image) 176 | 177 | // WHEN 178 | let data = self.data(filename: "sample.jpg")! 179 | 180 | let updateImage = UIImage(data: data)! 181 | imageView.setImage(updateImage, manager: gifManager) 182 | imageView.frame = CGRect(origin: .zero, size: updateImage.size) 183 | 184 | // THEN 185 | XCTAssertFalse(gifManager.containsImageView(imageView)) 186 | 187 | ///snapshot of the updated JPG 188 | assertSnapshot(matching: imageView, as: .image) 189 | 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /SwiftyGifTests/__Snapshots__/SwiftyGifTests/testThat15MBGIFCanBeLoaded.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/SwiftyGifTests/__Snapshots__/SwiftyGifTests/testThat15MBGIFCanBeLoaded.1.png -------------------------------------------------------------------------------- /SwiftyGifTests/__Snapshots__/SwiftyGifTests/testThatGIFWithoutkCGImagePropertyGIFDictionaryCanBeLoaded.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/SwiftyGifTests/__Snapshots__/SwiftyGifTests/testThatGIFWithoutkCGImagePropertyGIFDictionaryCanBeLoaded.1.png -------------------------------------------------------------------------------- /SwiftyGifTests/__Snapshots__/SwiftyGifTests/testThatImageViewCanBeRecycled.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/SwiftyGifTests/__Snapshots__/SwiftyGifTests/testThatImageViewCanBeRecycled.1.png -------------------------------------------------------------------------------- /SwiftyGifTests/__Snapshots__/SwiftyGifTests/testThatImageViewCanBeRecycledForGIF.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/SwiftyGifTests/__Snapshots__/SwiftyGifTests/testThatImageViewCanBeRecycledForGIF.2.png -------------------------------------------------------------------------------- /SwiftyGifTests/__Snapshots__/SwiftyGifTests/testThatImageViewCanBeRecycledForNormalImage.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/SwiftyGifTests/__Snapshots__/SwiftyGifTests/testThatImageViewCanBeRecycledForNormalImage.1.png -------------------------------------------------------------------------------- /SwiftyGifTests/__Snapshots__/SwiftyGifTests/testThatNonAnimatedGIFCanBeLoaded.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/SwiftyGifTests/__Snapshots__/SwiftyGifTests/testThatNonAnimatedGIFCanBeLoaded.1.png -------------------------------------------------------------------------------- /SwiftyGifTests/__Snapshots__/SwiftyGifTests/testThatNonAnimatedGIFCanBeLoadedWithUIImage.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/SwiftyGifTests/__Snapshots__/SwiftyGifTests/testThatNonAnimatedGIFCanBeLoadedWithUIImage.1.png -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/example.gif -------------------------------------------------------------------------------- /projec-file-explain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexiscreuzot/SwiftyGif/c44e2b19f80baa9df496d6415c81d12bac852d46/projec-file-explain.png --------------------------------------------------------------------------------