├── .gitignore ├── Tests ├── LinuxMain.swift └── PDFViewerTests │ ├── XCTestManifests.swift │ └── PDFViewerTests.swift ├── Sources └── PDFViewer │ ├── ActivityViewController.swift │ ├── ProgressView.swift │ ├── DownloadManager.swift │ ├── PDFViewer.swift │ └── PDFReaderView.swift ├── LICENSE ├── Package.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import PDFViewerTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += PDFViewerTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/PDFViewerTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(PDFViewerTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/PDFViewerTests/PDFViewerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import PDFViewer 3 | 4 | final class PDFViewerTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | //XCTAssertEqual(PDFViewer().text, "Hello, World!") 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Sources/PDFViewer/ActivityViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityViewController.swift 3 | // PDFViewer 4 | // 5 | // Created by Raju on 4/5/20. 6 | // Copyright © 2020 Raju. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | public struct ActivityViewController: UIViewControllerRepresentable { 13 | 14 | var activityItems: [Any] 15 | var applicationActivities: [UIActivity]? = nil 16 | 17 | public func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { 18 | let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities) 19 | return controller 20 | } 21 | 22 | public func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) {} 23 | 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Md. Harish-Uz-Jaman Mridha Raju 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.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "PDFViewer", 8 | platforms: [ 9 | // Only add support for iOS 13 and up. 10 | .iOS(.v13) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 14 | .library( 15 | name: "PDFViewer", 16 | targets: ["PDFViewer"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 25 | .target( 26 | name: "PDFViewer", 27 | dependencies: []), 28 | .testTarget( 29 | name: "PDFViewerTests", 30 | dependencies: ["PDFViewer"]), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /Sources/PDFViewer/ProgressView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressView.swift 3 | // PDFViewer 4 | // 5 | // Created by Raju on 4/5/20. 6 | // Copyright © 2020 Raju. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct ProgressView: View { 12 | var value: Binding 13 | var visible: Binding 14 | 15 | public init(value: Binding, visible: Binding){ 16 | self.value = value 17 | self.visible = visible 18 | } 19 | 20 | public var body: some View { 21 | ZStack { 22 | Rectangle() 23 | .fill(Color.primary.opacity(0.001)) 24 | ZStack { 25 | RoundedRectangle(cornerRadius: 5) 26 | .fill(Color.gray) 27 | Circle() 28 | .trim(from: 0, to: CGFloat(value.wrappedValue)) 29 | .stroke(Color.primary, lineWidth:5) 30 | .frame(width:50) 31 | .rotationEffect(Angle(degrees:-90)) 32 | Text(getPercentage(value.wrappedValue)) 33 | }.frame(width: 160, height: 120) 34 | } 35 | .opacity(visible.wrappedValue ? 1 : 0) 36 | } 37 | 38 | func getPercentage(_ value:Float) -> String { 39 | let intValue = Int(ceil(value * 100)) 40 | return "\(intValue) %" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/PDFViewer/DownloadManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadManager.swift 3 | // PDFViewer 4 | // 5 | // Created by Raju on 4/5/20. 6 | // Copyright © 2020 Raju. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | public protocol DownloadManagerDelegate { 13 | func downloadDidFinished(success: Bool) 14 | func downloadDidFailed(failure: Bool) 15 | func downloadInProgress(progress: Float, totalBytesWritten: Float, totalBytesExpectedToWrite:Float) 16 | } 17 | 18 | public class DownloadManager: NSObject, ObservableObject { 19 | 20 | private var fileUrl: URL? 21 | private var downloadTask: URLSessionDownloadTask! 22 | public var delegate: DownloadManagerDelegate? 23 | static let downloadManager = DownloadManager() 24 | 25 | public class func shared() -> DownloadManager{ 26 | return downloadManager 27 | } 28 | 29 | public func downloadFile(url: URL) { 30 | fileUrl = url 31 | let configuration = URLSessionConfiguration.default 32 | configuration.sessionSendsLaunchEvents = true 33 | let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) 34 | if downloadTask?.state == .running { 35 | downloadTask?.cancel() 36 | } 37 | downloadTask = session.downloadTask(with: url) 38 | downloadTask?.resume() 39 | } 40 | } 41 | 42 | extension DownloadManager: URLSessionDownloadDelegate { 43 | 44 | public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { 45 | let cachesDirectoryUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first 46 | if let url = fileUrl, let cachesDirectoryUrl = cachesDirectoryUrl { 47 | let destinationFileUrl = cachesDirectoryUrl.appendingPathComponent(url.lastPathComponent) 48 | do { 49 | try FileManager.default.moveItem(at: location, to: destinationFileUrl) 50 | print("Downloaded file path: \(destinationFileUrl.path)") 51 | self.delegate?.downloadDidFinished(success: true) 52 | } 53 | catch let error as NSError { 54 | print("Failed moving directory: \(error)") 55 | self.delegate?.downloadDidFailed(failure: true) 56 | } 57 | } 58 | } 59 | 60 | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 61 | print("Task completed: \(task), error: \(String(describing: error?.localizedDescription))") 62 | if error != nil { 63 | self.delegate?.downloadDidFailed(failure: true) 64 | } 65 | } 66 | 67 | public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { 68 | DispatchQueue.main.async { 69 | if totalBytesExpectedToWrite > 0 { 70 | let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) 71 | print("Download progress: \(progress)") 72 | self.delegate?.downloadInProgress(progress: progress, totalBytesWritten: Float(totalBytesWritten) , totalBytesExpectedToWrite: Float(totalBytesExpectedToWrite)) 73 | } 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /Sources/PDFViewer/PDFViewer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PDFViewer.swift 3 | // PDFViewer 4 | // 5 | // Created by Raju on 15/4/20. 6 | // Copyright © 2020 Raju. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import PDFKit 11 | 12 | public enum PDFType { 13 | case local 14 | case remote 15 | } 16 | 17 | public struct PDFViewer: View { 18 | var pdfName: String = "" 19 | var pdfUrlString: String = "" 20 | @State private var showingShare = false 21 | @State private var pdfType: PDFType = .local 22 | 23 | public init(pdfName: String? = nil, pdfUrlString: String? = nil) { 24 | self.pdfName = pdfName ?? "" 25 | self.pdfUrlString = pdfUrlString ?? "" 26 | } 27 | 28 | public var body: some View { 29 | GeometryReader { geometry in 30 | ZStack { 31 | PDFReaderView(pdfName: self.pdfName, pdfUrlString: self.pdfUrlString, pdfType: self.pdfName.count > 0 ? .local : .remote, viewSize: geometry.size) 32 | .navigationBarItems(trailing: 33 | Button(action: { 34 | self.showingShare = true 35 | }, label: { 36 | Image(systemName: "arrowshape.turn.up.right.circle") 37 | .resizable() 38 | }) 39 | .sheet(isPresented: self.$showingShare, onDismiss: { 40 | //dismiss 41 | }, content: { 42 | if (self.pdfLocalData() != nil) { 43 | ActivityViewController(activityItems: [self.pdfLocalData()!, self.pdfName.count > 0 ? self.pdfName : URL(fileURLWithPath: self.pdfUrlString).deletingPathExtension().lastPathComponent]) 44 | } 45 | }) 46 | ) 47 | .navigationBarTitle(self.pdfName.count > 0 ? self.pdfName : URL(fileURLWithPath: self.pdfUrlString).deletingPathExtension().lastPathComponent) 48 | .onAppear() { 49 | self.pdfType = self.pdfName.count > 0 ? .local : .remote 50 | } 51 | } 52 | } 53 | } 54 | 55 | private func pdfLocalData() -> Data? { 56 | if pdfType == .local { 57 | if let url = Bundle.main.url(forResource: pdfName, withExtension: "pdf") { 58 | do { 59 | let pdfLocalData = try Data(contentsOf: url) 60 | return pdfLocalData 61 | } catch let error { 62 | print(error.localizedDescription) 63 | } 64 | } 65 | } else { 66 | if let cachesDirectoryUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first, let lastPathComponent = URL(string: self.pdfUrlString)?.lastPathComponent { 67 | let url = cachesDirectoryUrl.appendingPathComponent(lastPathComponent) 68 | do { 69 | let pdfLocalData = try Data(contentsOf: url) 70 | return pdfLocalData 71 | } catch let error { 72 | print(error.localizedDescription) 73 | } 74 | } 75 | } 76 | return nil 77 | } 78 | } 79 | 80 | struct PDFViewer_Previews: PreviewProvider { 81 | static var previews: some View { 82 | PDFViewer() 83 | .previewLayout(.sizeThatFits) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PDFViewer 2 | 3 | ## PDFViewer powered by PDFKit and SwiftUI 4 | 5 | # Installation 6 | The preferred way of installing is via the [Swift Package Manager](https://swift.org/package-manager/). 7 | 8 | >Xcode 11 integrates with libSwiftPM to provide support for iOS, watchOS, and tvOS platforms. 9 | 10 | 1. In Xcode, open your project and navigate to **File** → **Swift Packages** → **Add Package Dependency...** 11 | 2. Paste the repository URL (`https://github.com/rajubd49/PDFViewer.git`) and click **Next**. 12 | 3. For **Rules**, select **Branch** (with branch set to `1.0.2` ). 13 | 4. Click **Finish**. 14 | 15 | ## Excample code 16 | ```Swift 17 | import SwiftUI 18 | import PDFViewer 19 | 20 | struct ContentView: View, DownloadManagerDelegate { 21 | 22 | @State private var viewLocalPDF = false 23 | @State private var viewRemotePDF = false 24 | @State private var loadingPDF: Bool = false 25 | @State private var progressValue: Float = 0.0 26 | @ObservedObject var downloadManager = DownloadManager.shared() 27 | 28 | let pdfName = "sample" 29 | let pdfUrlString = "http://www.africau.edu/images/default/sample.pdf" 30 | 31 | var body: some View { 32 | NavigationView { 33 | ZStack { 34 | VStack { 35 | NavigationLink(destination: PDFViewer(pdfName: pdfName), isActive: $viewLocalPDF) { 36 | Button("View Local PDF"){ 37 | self.viewLocalPDF = true 38 | } 39 | .padding(.bottom, 20) 40 | } 41 | Button("View Remote PDF"){ 42 | if self.fileExistsInDirectory() { 43 | self.viewRemotePDF = true 44 | } else { 45 | self.downloadPDF(pdfUrlString: self.pdfUrlString) 46 | } 47 | } 48 | if self.viewRemotePDF { 49 | NavigationLink(destination: PDFViewer(pdfUrlString: self.pdfUrlString), isActive: self.$viewRemotePDF) { 50 | EmptyView() 51 | }.hidden() 52 | } 53 | } 54 | ProgressView(value: self.$progressValue, visible: self.$loadingPDF) 55 | } 56 | .navigationBarTitle("PDFViewer", displayMode: .inline) 57 | } 58 | } 59 | 60 | private func fileExistsInDirectory() -> Bool { 61 | if let cachesDirectoryUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first, let lastPathComponent = URL(string: self.pdfUrlString)?.lastPathComponent { 62 | let url = cachesDirectoryUrl.appendingPathComponent(lastPathComponent) 63 | if FileManager.default.fileExists(atPath: url.path) { 64 | return true 65 | } 66 | } 67 | return false 68 | } 69 | 70 | private func downloadPDF(pdfUrlString: String) { 71 | guard let url = URL(string: pdfUrlString) else { return } 72 | downloadManager.delegate = self 73 | downloadManager.downloadFile(url: url) 74 | } 75 | 76 | //MARK: DownloadManagerDelegate 77 | func downloadDidFinished(success: Bool) { 78 | if success { 79 | loadingPDF = false 80 | viewRemotePDF = true 81 | } 82 | } 83 | 84 | func downloadDidFailed(failure: Bool) { 85 | if failure { 86 | loadingPDF = false 87 | print("PDFCatalogueView: Download failure") 88 | } 89 | } 90 | 91 | func downloadInProgress(progress: Float, totalBytesWritten: Float, totalBytesExpectedToWrite: Float) { 92 | loadingPDF = true 93 | progressValue = progress 94 | } 95 | } 96 | ``` 97 | 98 | ## Features 99 | 100 | * View local PDF 101 | * Download PDF from remote url and save in cache directory to view PDF 102 | * PDF thumbnail view show 103 | * PDF view page change notification 104 | * PDF view annotation hit notification 105 | * Share PDF file 106 | 107 | ## Author 108 | 109 | rajubd49@gmail.com 110 | 111 | ## License 112 | 113 | PDFViewer is released under the MIT License. [See LICENSE](https://github.com/rajubd49/PDFViewer/blob/master/LICENSE) for details. 114 | -------------------------------------------------------------------------------- /Sources/PDFViewer/PDFReaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PDFReaderView.swift 3 | // PDFViewer 4 | // 5 | // Created by Raju on 4/5/20. 6 | // Copyright © 2020 Raju. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import PDFKit 11 | 12 | struct PDFReaderView : UIViewRepresentable { 13 | var pdfName: String = "" 14 | var pdfUrlString: String = "" 15 | var pdfType: PDFType = .local 16 | var viewSize: CGSize = CGSize(width: 0.0, height: 0.0) 17 | let pdfThumbnailSize = CGSize(width: 40, height: 54) 18 | let pdfView = PDFView() 19 | let activityView = UIActivityIndicatorView(style: .large) 20 | 21 | public func makeCoordinator() -> Coordinator { 22 | Coordinator(self) 23 | } 24 | 25 | public class Coordinator: NSObject { 26 | var parent: PDFReaderView 27 | 28 | init(_ parent: PDFReaderView) { 29 | self.parent = parent 30 | } 31 | 32 | @objc func handlePageChange(notification: Notification) { 33 | parent.pageLabel(pdfView: parent.pdfView, viewSize: parent.viewSize) 34 | } 35 | 36 | @objc func handleAnnotationHit(notification: Notification) { 37 | if let annotation = notification.userInfo?["PDFAnnotationHit"] as? PDFAnnotation { 38 | parent.pdfAnnotationTapped(annotation: annotation) 39 | } 40 | } 41 | } 42 | 43 | public func makeUIView(context: Context) -> UIView { 44 | return preparePDFView(context: context) 45 | } 46 | 47 | public func updateUIView(_ uiView: UIView, context: Context) { } 48 | 49 | private func preparePDFView(context: Context) -> PDFView { 50 | pdfView.displayDirection = .horizontal 51 | pdfView.usePageViewController(true) 52 | pdfView.pageBreakMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) 53 | pdfView.autoScales = true 54 | 55 | if pdfType == .local { 56 | if let url = Bundle.main.url(forResource: pdfName, withExtension: "pdf"), 57 | let document = PDFDocument(url: url) { 58 | pdfView.document = document 59 | } 60 | } else { 61 | if let cachesDirectoryUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first, let lastPathComponent = URL(string: self.pdfUrlString)?.lastPathComponent { 62 | let url = cachesDirectoryUrl.appendingPathComponent(lastPathComponent) 63 | do { 64 | let pdfLocalData = try Data(contentsOf: url) 65 | pdfView.document = PDFDocument(data: pdfLocalData) 66 | } catch let error { 67 | print(error.localizedDescription) 68 | } 69 | } 70 | } 71 | 72 | thumbnailView(pdfView: pdfView, viewSize: viewSize) 73 | pageLabel(pdfView: pdfView, viewSize: viewSize) 74 | 75 | NotificationCenter.default.addObserver(context.coordinator, selector: #selector(context.coordinator.handlePageChange(notification:)), name: Notification.Name.PDFViewPageChanged, object: nil) 76 | NotificationCenter.default.addObserver(context.coordinator, selector: #selector(context.coordinator.handleAnnotationHit(notification:)), name: Notification.Name.PDFViewAnnotationHit, object: nil) 77 | 78 | return pdfView 79 | } 80 | 81 | private func thumbnailView(pdfView: PDFView, viewSize: CGSize) { 82 | let thumbnailView = PDFThumbnailView(frame: CGRect(x: 0, y: viewSize.height - pdfThumbnailSize.height - 8, width: viewSize.width, height: pdfThumbnailSize.height)) 83 | thumbnailView.backgroundColor = UIColor.clear 84 | thumbnailView.thumbnailSize = pdfThumbnailSize 85 | thumbnailView.layoutMode = .horizontal 86 | thumbnailView.pdfView = pdfView 87 | pdfView.addSubview(thumbnailView) 88 | } 89 | 90 | private func pageLabel(pdfView: PDFView, viewSize: CGSize) { 91 | for view in pdfView.subviews { 92 | if view.isKind(of: UILabel.self) { 93 | view.removeFromSuperview() 94 | } 95 | } 96 | let pageNumberLabel = UILabel(frame: CGRect(x: viewSize.width - 88, y: 4, width: 80, height: 20)) 97 | pageNumberLabel.font = UIFont.systemFont(ofSize: 14.0) 98 | pageNumberLabel.textAlignment = .right 99 | pageNumberLabel.text = String(format: "%@ of %d", pdfView.currentPage?.label ?? "0", pdfView.document?.pageCount ?? 0) 100 | pdfView.addSubview(pageNumberLabel) 101 | } 102 | 103 | private func pdfAnnotationTapped(annotation: PDFAnnotation) { 104 | let pdfUrl = annotation.url 105 | print(pdfUrl ?? "") 106 | } 107 | } 108 | --------------------------------------------------------------------------------