diff --git a/.gitignore b/.gitignore index 5e5d5ce..90a386a 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,7 @@ playground.xcworkspace # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # -# Pods/ + **/Pods/ # Carthage # diff --git a/FileBrowser.xcodeproj/project.pbxproj b/FileBrowser.xcodeproj/project.pbxproj index 3fb59b7..f94199b 100644 --- a/FileBrowser.xcodeproj/project.pbxproj +++ b/FileBrowser.xcodeproj/project.pbxproj @@ -33,6 +33,15 @@ 349A12361C707E86005435C0 /* image@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 349A122F1C707E86005435C0 /* image@2x.png */; }; 349A12371C707E86005435C0 /* pdf@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 349A12301C707E86005435C0 /* pdf@2x.png */; }; 349A12381C707E86005435C0 /* zip@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 349A12311C707E86005435C0 /* zip@2x.png */; }; + C3308C8F1E3501BE00F4D125 /* FBResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3308C8E1E3501BE00F4D125 /* FBResult.swift */; }; + C3308C901E351F9D00F4D125 /* FileParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3439AB671C6F203A0058AF04 /* FileParser.swift */; }; + C3308C911E351FB500F4D125 /* FBFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349A12271C707B1A005435C0 /* FBFile.swift */; }; + C3308C921E351FCF00F4D125 /* FBResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3308C8E1E3501BE00F4D125 /* FBResult.swift */; }; + C3308C931E351FE900F4D125 /* FileBrowserDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BF0DFB1E13F4A80009D12C /* FileBrowserDataSource.swift */; }; + C3BF0DFC1E13F4A80009D12C /* FileBrowserDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BF0DFB1E13F4A80009D12C /* FileBrowserDataSource.swift */; }; + C3BF0E001E1534070009D12C /* LoadingViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C3BF0DFF1E1534070009D12C /* LoadingViewController.xib */; }; + C3BF0E021E153A2C0009D12C /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BF0E011E153A2C0009D12C /* LoadingViewController.swift */; }; + C3C2AC201EC25F470060AECA /* FileBrowserDownloadDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2AC1D1EC25A6F0060AECA /* FileBrowserDownloadDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -46,15 +55,15 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 3437405A1C6E7DA50090FD6A /* FileListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileListViewController.swift; sourceTree = ""; }; + 3437405A1C6E7DA50090FD6A /* FileListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FileListViewController.swift; sourceTree = ""; }; 3439AB671C6F203A0058AF04 /* FileParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileParser.swift; sourceTree = ""; }; 3439AB691C6FC6D90058AF04 /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; }; 3439AB6B1C6FD6650058AF04 /* FileListPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileListPreview.swift; sourceTree = ""; }; - 3439AB6F1C6FF68C0058AF04 /* FileBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileBrowser.swift; sourceTree = ""; }; + 3439AB6F1C6FF68C0058AF04 /* FileBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FileBrowser.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 343C44611C73CC8100D874FB /* PreviewTransitionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewTransitionViewController.swift; sourceTree = ""; }; - 343C44651C73CD3200D874FB /* PreviewManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewManager.swift; sourceTree = ""; }; + 343C44651C73CD3200D874FB /* PreviewManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = PreviewManager.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 343C44671C73CD8700D874FB /* PreviewTransitionViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PreviewTransitionViewController.xib; sourceTree = ""; }; - 343C44781C73CFC400D874FB /* WebviewPreviewViewContoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebviewPreviewViewContoller.swift; sourceTree = ""; }; + 343C44781C73CFC400D874FB /* WebviewPreviewViewContoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = WebviewPreviewViewContoller.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 343C447C1C73CFD900D874FB /* WebviewPreviewViewContoller.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = WebviewPreviewViewContoller.xib; sourceTree = ""; }; 343C447E1C73D06200D874FB /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; 344169541C67812400B93D28 /* FileBrowser.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileBrowser.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -66,8 +75,8 @@ 3441DC601C71235C005CC2FA /* 3crBXeO.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = 3crBXeO.gif; sourceTree = ""; }; 3441DC621C712370005CC2FA /* Stitch.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Stitch.jpg; sourceTree = ""; }; 349A12231C70725D005435C0 /* FileListSearch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileListSearch.swift; sourceTree = ""; }; - 349A12251C707317005435C0 /* FileListTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileListTableView.swift; sourceTree = ""; }; - 349A12271C707B1A005435C0 /* FBFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FBFile.swift; sourceTree = ""; }; + 349A12251C707317005435C0 /* FileListTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FileListTableView.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 349A12271C707B1A005435C0 /* FBFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FBFile.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 349A122B1C707E86005435C0 /* documents@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "documents@2x.png"; sourceTree = ""; }; 349A122C1C707E86005435C0 /* file@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "file@2x.png"; sourceTree = ""; }; 349A122D1C707E86005435C0 /* FileBrowser.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = FileBrowser.xib; sourceTree = ""; }; @@ -75,6 +84,11 @@ 349A122F1C707E86005435C0 /* image@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "image@2x.png"; sourceTree = ""; }; 349A12301C707E86005435C0 /* pdf@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "pdf@2x.png"; sourceTree = ""; }; 349A12311C707E86005435C0 /* zip@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "zip@2x.png"; sourceTree = ""; }; + C3308C8E1E3501BE00F4D125 /* FBResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FBResult.swift; sourceTree = ""; }; + C3BF0DFB1E13F4A80009D12C /* FileBrowserDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileBrowserDataSource.swift; sourceTree = ""; }; + C3BF0DFF1E1534070009D12C /* LoadingViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LoadingViewController.xib; sourceTree = ""; }; + C3BF0E011E153A2C0009D12C /* LoadingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = LoadingViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + C3C2AC1D1EC25A6F0060AECA /* FileBrowserDownloadDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FileBrowserDownloadDelegate.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -103,6 +117,9 @@ children = ( 3439AB671C6F203A0058AF04 /* FileParser.swift */, 349A12271C707B1A005435C0 /* FBFile.swift */, + C3BF0DFB1E13F4A80009D12C /* FileBrowserDataSource.swift */, + C3308C8E1E3501BE00F4D125 /* FBResult.swift */, + C3C2AC1D1EC25A6F0060AECA /* FileBrowserDownloadDelegate.swift */, ); name = Data; sourceTree = ""; @@ -121,6 +138,7 @@ 343C44601C73CC3C00D874FB /* Preview */ = { isa = PBXGroup; children = ( + C3BF0E011E153A2C0009D12C /* LoadingViewController.swift */, 343C44611C73CC8100D874FB /* PreviewTransitionViewController.swift */, 343C44651C73CD3200D874FB /* PreviewManager.swift */, 343C44781C73CFC400D874FB /* WebviewPreviewViewContoller.swift */, @@ -187,6 +205,7 @@ 349A122B1C707E86005435C0 /* documents@2x.png */, 349A122C1C707E86005435C0 /* file@2x.png */, 349A122D1C707E86005435C0 /* FileBrowser.xib */, + C3BF0DFF1E1534070009D12C /* LoadingViewController.xib */, 349A122E1C707E86005435C0 /* folder@2x.png */, 349A122F1C707E86005435C0 /* image@2x.png */, 349A12301C707E86005435C0 /* pdf@2x.png */, @@ -294,6 +313,7 @@ 343C44681C73CD8700D874FB /* PreviewTransitionViewController.xib in Resources */, 349A12331C707E86005435C0 /* file@2x.png in Resources */, 349A12341C707E86005435C0 /* FileBrowser.xib in Resources */, + C3BF0E001E1534070009D12C /* LoadingViewController.xib in Resources */, 343C447D1C73CFD900D874FB /* WebviewPreviewViewContoller.xib in Resources */, 349A12361C707E86005435C0 /* image@2x.png in Resources */, 349A12371C707E86005435C0 /* pdf@2x.png in Resources */, @@ -319,16 +339,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 349A12281C707B1A005435C0 /* FBFile.swift in Sources */, + C3C2AC201EC25F470060AECA /* FileBrowserDownloadDelegate.swift in Sources */, 343C447A1C73CFC400D874FB /* WebviewPreviewViewContoller.swift in Sources */, 3439AB701C6FF68C0058AF04 /* FileBrowser.swift in Sources */, 349A12261C707317005435C0 /* FileListTableView.swift in Sources */, + C3308C8F1E3501BE00F4D125 /* FBResult.swift in Sources */, 343C44661C73CD3200D874FB /* PreviewManager.swift in Sources */, 3439AB681C6F203A0058AF04 /* FileParser.swift in Sources */, - 349A12281C707B1A005435C0 /* FBFile.swift in Sources */, 343C44631C73CC8100D874FB /* PreviewTransitionViewController.swift in Sources */, + C3BF0DFC1E13F4A80009D12C /* FileBrowserDataSource.swift in Sources */, 3437405B1C6E7DA50090FD6A /* FileListViewController.swift in Sources */, 349A12241C70725D005435C0 /* FileListSearch.swift in Sources */, 3439AB6C1C6FD6650058AF04 /* FileListPreview.swift in Sources */, + C3BF0E021E153A2C0009D12C /* LoadingViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -336,6 +360,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C3308C901E351F9D00F4D125 /* FileParser.swift in Sources */, + C3308C911E351FB500F4D125 /* FBFile.swift in Sources */, + C3308C921E351FCF00F4D125 /* FBResult.swift in Sources */, + C3308C931E351FE900F4D125 /* FileBrowserDataSource.swift in Sources */, 344169641C67812400B93D28 /* FileBrowserTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -370,8 +398,6 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "-"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "-"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = dwarf; @@ -421,8 +447,6 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "-"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "-"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; @@ -455,6 +479,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 6; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; INFOPLIST_FILE = FileBrowser/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -475,6 +500,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 6; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; INFOPLIST_FILE = FileBrowser/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; diff --git a/FileBrowser/FBFile.swift b/FileBrowser/FBFile.swift index 6e8c700..46247f1 100644 --- a/FileBrowser/FBFile.swift +++ b/FileBrowser/FBFile.swift @@ -7,21 +7,38 @@ // import Foundation +import UIKit /// FBFile is a class representing a file in FileBrowser -open class FBFile: NSObject { +public protocol FBFile { + var displayName: String { get } + + /// Describes the path in the current file system, e.g. /dir/file.txt + var path: URL { get } + + /// Describes where the resource can be found. May be a file:// or http[s]:// URL + var resourceUrl: URL? { get } + + var isDirectory: Bool { get } + var fileExtension: String? { get } + var type: FBFileType { get } + var isRemoteFile: Bool { get } +} + +open class BasicFBFile: NSObject, FBFile { /// Display name. String. - open let displayName: String + open var displayName: String // is Directory. Bool. open let isDirectory: Bool /// File extension. open let fileExtension: String? - /// File attributes (including size, creation date etc). - open let fileAttributes: NSDictionary? - /// NSURL file path. - open let filePath: URL + + open var resourceUrl: URL? // FBFileType - open let type: FBFileType + open var type: FBFileType + + + open let path: URL /** Initialize an FBFile object with a filePath @@ -30,26 +47,31 @@ open class FBFile: NSObject { - returns: FBFile object. */ - init(filePath: URL) { - self.filePath = filePath - let isDirectory = checkDirectory(filePath) - self.isDirectory = isDirectory + public init(path: URL) { + self.path = path + self.isDirectory = checkDirectory(path) + if self.isDirectory { - self.fileAttributes = nil self.fileExtension = nil self.type = .Directory } else { - self.fileAttributes = getFileAttributes(self.filePath) - self.fileExtension = filePath.pathExtension - if let fileExtension = fileExtension { - self.type = FBFileType(rawValue: fileExtension) ?? .Default - } - else { + if path.pathExtension != "" { + self.fileExtension = path.pathExtension + self.type = FBFileType(rawValue: fileExtension!) ?? .Default + } else { + self.fileExtension = nil self.type = .Default } } - self.displayName = filePath.lastPathComponent + self.displayName = path.lastPathComponent + } + + public var isRemoteFile: Bool { + guard let resourceUrl = resourceUrl else { + return true + } + return resourceUrl.scheme == "http" || resourceUrl.scheme == "https" } } @@ -82,7 +104,7 @@ public enum FBFileType: String { - returns: UIImage for file type */ public func image() -> UIImage? { - let bundle = Bundle(for: FileParser.self) + let bundle = Bundle(for: FileBrowser.self) var fileName = String() switch self { case .Directory: fileName = "folder@2x.png" @@ -104,6 +126,9 @@ public enum FBFileType: String { - returns: isDirectory Bool. */ func checkDirectory(_ filePath: URL) -> Bool { + if #available(iOS 9.0, *) { + return filePath.hasDirectoryPath + } var isDirectory = false do { var resourceValue: AnyObject? @@ -115,13 +140,3 @@ func checkDirectory(_ filePath: URL) -> Bool { catch { } return isDirectory } - -func getFileAttributes(_ filePath: URL) -> NSDictionary? { - let path = filePath.path - let fileManager = FileParser.sharedInstance.fileManager - do { - let attributes = try fileManager.attributesOfItem(atPath: path) as NSDictionary - return attributes - } catch {} - return nil -} diff --git a/FileBrowser/FBResult.swift b/FileBrowser/FBResult.swift new file mode 100644 index 0000000..c613d4f --- /dev/null +++ b/FileBrowser/FBResult.swift @@ -0,0 +1,14 @@ +// +// FBResult.swift +// FileBrowser +// +// Created by Carl Julius Gödecken on 01/01/2017. +// Copyright © 2017 Carl Julius Gödecken. +// + +import Foundation + +public enum FBResult { + case success(T) + case error(Error) +} diff --git a/FileBrowser/FileBrowser.swift b/FileBrowser/FileBrowser.swift index 340183f..bb57f6e 100644 --- a/FileBrowser/FileBrowser.swift +++ b/FileBrowser/FileBrowser.swift @@ -11,21 +11,21 @@ import Foundation /// File browser containing navigation controller. open class FileBrowser: UINavigationController { - let parser = FileParser.sharedInstance + open var dataSource: FileBrowserDataSource = LocalFileParser() var fileList: FileListViewController? /// File types to exclude from the file browser. open var excludesFileExtensions: [String]? { didSet { - parser.excludesFileExtensions = excludesFileExtensions + dataSource.excludesFileExtensions = excludesFileExtensions } } /// File paths to exclude from the file browser. open var excludesFilepaths: [URL]? { didSet { - parser.excludesFilepaths = excludesFilepaths + dataSource.excludesFilepaths = excludesFilepaths } } @@ -36,29 +36,45 @@ open class FileBrowser: UINavigationController { } } + open var downloadDelegate: FileBrowserDownloadDelegate? { + didSet { + fileList?.downloadDelegate = downloadDelegate + } + } + /** - Init to documents folder. - - - returns: File browser view controller. - */ + Init to local documents folder. + */ public convenience init() { - let parser = FileParser.sharedInstance - let path = parser.documentsURL() - self.init(initialPath: path as URL) + let parser = LocalFileParser() + self.init(dataSource: parser) } /** - Init to a custom directory path. + Init to a custom local directory path. - parameter initialPath: NSURL filepath to containing directory. - - - returns: File browser view controller. - */ + */ public convenience init(initialPath: URL) { - let fileListViewController = FileListViewController(initialPath: initialPath) + let parser = LocalFileParser() + parser.customRootUrl = initialPath + self.init(dataSource: parser) + } + + /** + Init with a custom dataSource. Alternatively, the dataSource can be set after initialization. + + - parameter parser: The data source used by the file browser + */ + + public convenience init(dataSource: FileBrowserDataSource) { + let fileListViewController = FileListViewController(dataSource: dataSource, withDirectory: dataSource.rootDirectory) self.init(rootViewController: fileListViewController) + self.dataSource = dataSource self.view.backgroundColor = UIColor.white self.fileList = fileListViewController } + + } diff --git a/FileBrowser/FileBrowserDataSource.swift b/FileBrowser/FileBrowserDataSource.swift new file mode 100644 index 0000000..c1037b0 --- /dev/null +++ b/FileBrowser/FileBrowserDataSource.swift @@ -0,0 +1,26 @@ +// +// FileBrowserDataSource.swift +// FileBrowser +// +// Created by Carl Julius Gödecken on 28/12/2016. +// Copyright © 2016 Carl Julius Gödecken. +// + +import Foundation + +public protocol FileBrowserDataSource { + var rootDirectory: FBFile { get } + func provideContents(ofDirectory directory: FBFile, callback: @escaping (FBResult<[FBFile]>) -> ()) + + func data(forFile file: FBFile) throws -> Data + + var excludesFileExtensions: [String]? { get set } + var excludesFilepaths: [URL]? { get set } +} + +extension FileBrowserDataSource { + public func data(forFile file: FBFile) throws -> Data { + let url = file.resourceUrl! + return try Data(contentsOf: url) + } +} diff --git a/FileBrowser/FileBrowserDownloadDelegate.swift b/FileBrowser/FileBrowserDownloadDelegate.swift new file mode 100644 index 0000000..1e9bedb --- /dev/null +++ b/FileBrowser/FileBrowserDownloadDelegate.swift @@ -0,0 +1,37 @@ +// +// FileBrowserDownloadDelegate.swift +// FileBrowser +// +// Created by Carl Gödecken on 09.05.17. +// Copyright © 2017 Carl Gödecken. +// + +import Foundation + +public protocol FileBrowserDownloadDelegate { + /// option to provide custom URLs before the download starts + /// called only if the resourceUrl for a file is not set + func provideCustomDownloadUrl(for file: FBFile, completionHandler: @escaping (FBResult) -> ()) + + /// customization point e.g. for setting custom headers + func willPerformDownloadTask(for file: FBFile, using request: inout URLRequest) + + /// may be used to validate the response and throw errors before the file is displayed + func didFinishDownloading(data: Data, for file: FBFile, for task: URLSessionDownloadTask) throws +} + +extension FileBrowserDownloadDelegate { + func provideCustomDownloadUrl(for file: FBFile, completionHandler: @escaping (FBResult) -> ()) { + if let url = file.resourceUrl { + completionHandler(.success(url)) + } else { + completionHandler(.error(FileBrowserDownloadError.noFileUrlGiven)) + } + } + + func didFinishDownloading(data: Data, for file: FBFile, for task: URLSessionDownloadTask) throws {} +} + +enum FileBrowserDownloadError: Error { + case noFileUrlGiven +} diff --git a/FileBrowser/FileListPreview.swift b/FileBrowser/FileListPreview.swift index 240413d..6d32b1b 100644 --- a/FileBrowser/FileListPreview.swift +++ b/FileBrowser/FileListPreview.swift @@ -27,7 +27,7 @@ extension FileListViewController: UIViewControllerPreviewingDelegate { let selectedFile = fileForIndexPath(indexPath) previewingContext.sourceRect = tableView.rectForRow(at: indexPath) if selectedFile.isDirectory == false { - return previewManager.previewViewControllerForFile(selectedFile, fromNavigation: false) + return previewManager.previewViewControllerForFile(selectedFile, data: nil, fromNavigation: false) } } } diff --git a/FileBrowser/FileListTableView.swift b/FileBrowser/FileListTableView.swift index bb2a290..86828f3 100644 --- a/FileBrowser/FileListTableView.swift +++ b/FileBrowser/FileListTableView.swift @@ -43,7 +43,8 @@ extension FileListViewController: UITableViewDataSource, UITableViewDelegate { let selectedFile = fileForIndexPath(indexPath) searchController.isActive = false if selectedFile.isDirectory { - let fileListViewController = FileListViewController(initialPath: selectedFile.filePath) + let fileListViewController = FileListViewController(dataSource: dataSource, withDirectory: selectedFile) + fileListViewController.downloadDelegate = downloadDelegate fileListViewController.didSelectFile = didSelectFile self.navigationController?.pushViewController(fileListViewController, animated: true) } @@ -53,7 +54,7 @@ extension FileListViewController: UITableViewDataSource, UITableViewDelegate { didSelectFile(selectedFile) } else { - let filePreview = previewManager.previewViewControllerForFile(selectedFile, fromNavigation: true) + let filePreview = previewManager.previewViewControllerForFile(selectedFile, data: nil, fromNavigation: true) self.navigationController?.pushViewController(filePreview, animated: true) } } diff --git a/FileBrowser/FileListViewController.swift b/FileBrowser/FileListViewController.swift index b898c5e..cae5693 100644 --- a/FileBrowser/FileListViewController.swift +++ b/FileBrowser/FileListViewController.swift @@ -15,13 +15,19 @@ class FileListViewController: UIViewController { let collation = UILocalizedIndexedCollation.current() /// Data + var directory: FBFile! + var dataSource: FileBrowserDataSource! + var downloadDelegate: FileBrowserDownloadDelegate? { + didSet { + previewManager.downloadDelegate = downloadDelegate + } + } + var didSelectFile: ((FBFile) -> ())? var files = [FBFile]() - var initialPath: URL? - let parser = FileParser.sharedInstance let previewManager = PreviewManager() var sections: [[FBFile]] = [] - + // Search controller var filteredFiles = [FBFile]() let searchController: UISearchController = { @@ -33,15 +39,19 @@ class FileListViewController: UIViewController { }() + //MARK: Lifecycle - convenience init (initialPath: URL) { + convenience init (dataSource: FileBrowserDataSource, withDirectory directory: FBFile) { self.init(nibName: "FileBrowser", bundle: Bundle(for: FileListViewController.self)) self.edgesForExtendedLayout = UIRectEdge() - // Set initial path - self.initialPath = initialPath - self.title = initialPath.lastPathComponent + // Set implicitly unwrapped optionals + self.dataSource = dataSource + self.directory = directory + self.previewManager.dataSource = dataSource + + self.title = directory.displayName // Set search controller delegates searchController.searchResultsUpdater = self @@ -67,9 +77,21 @@ class FileListViewController: UIViewController { override func viewDidLoad() { // Prepare data - if let initialPath = initialPath { - files = parser.filesForDirectory(initialPath) - indexFiles() + dataSource.provideContents(ofDirectory: self.directory) { result in + self.didCompleteLoading() + switch result { + case .success(let files): + self.files = files + self.indexFiles() + self.tableView.reloadData() + case .error(let error): + self.replaceTableViewRowsShowing(error: error) + } + } + + // show a loading indicator if it takes more than 0.5 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.showLoadingIndicatorIfNeeded() } // Set search bar @@ -123,6 +145,43 @@ class FileListViewController: UIViewController { }) tableView.reloadData() } - + + func replaceTableViewRowsShowing(error: Error) { + let errorLabel = UILabel(frame: CGRect(x: 0, y: 0, width: tableView.bounds.size.width, height: tableView.bounds.size.height)) + errorLabel.text = error.localizedDescription + errorLabel.textColor = UIColor.black + errorLabel.textAlignment = .center + + tableView.backgroundView = errorLabel + tableView.separatorStyle = .none + } + + //MARK: loading indicator + + var loadingCompleted = false + func didCompleteLoading() { + locking(loadingCompleted) { + loadingCompleted = true + } + hideLoadingIndicator() + } + + func showLoadingIndicatorIfNeeded() { + locking(loadingCompleted) { + if !loadingCompleted { + navigationItem.prompt = "Loading..." + } + } + } + + func hideLoadingIndicator() { + navigationItem.prompt = nil + } + + func locking(_ lock: Any, closure: () -> ()) { + objc_sync_enter(lock) + closure() + objc_sync_exit(lock) + } } diff --git a/FileBrowser/FileParser.swift b/FileBrowser/FileParser.swift index c0e364f..209546e 100644 --- a/FileBrowser/FileParser.swift +++ b/FileBrowser/FileParser.swift @@ -8,57 +8,64 @@ import Foundation -class FileParser { +class LocalFileParser: FileBrowserDataSource { - static let sharedInstance = FileParser() + var excludesFileExtensions: [String]? = nil + var excludesFilepaths: [URL]? = nil + var excludesWithEmptyFilenames = false - var _excludesFileExtensions = [String]() - - /// Mapped for case insensitivity - var excludesFileExtensions: [String]? { - get { - return _excludesFileExtensions.map({$0.lowercased()}) - } - set { - if let newValue = newValue { - _excludesFileExtensions = newValue - } - } - } - - var excludesFilepaths: [URL]? let fileManager = FileManager.default - func documentsURL() -> URL { + var customRootUrl: URL? + var defaultRootUrl: URL { return fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0] as URL } - func filesForDirectory(_ directoryPath: URL) -> [FBFile] { - var files = [FBFile]() - var filePaths = [URL]() + var rootDirectory: FBFile { + let url = customRootUrl ?? defaultRootUrl + return BasicFBFile(path: url) + } + + func provideContents(ofDirectory directory: FBFile, callback: @escaping (FBResult<[FBFile]>) -> ()) { + // Get contents - do { - filePaths = try self.fileManager.contentsOfDirectory(at: directoryPath, includingPropertiesForKeys: [], options: [.skipsHiddenFiles]) - } catch { - return files - } - // Parse - for filePath in filePaths { - let file = FBFile(filePath: filePath) - if let excludesFileExtensions = excludesFileExtensions, let fileExtensions = file.fileExtension , excludesFileExtensions.contains(fileExtensions) { - continue - } - if let excludesFilepaths = excludesFilepaths , excludesFilepaths.contains(file.filePath) { - continue + do { + let filePaths = try self.fileManager.contentsOfDirectory(at: directory.path, includingPropertiesForKeys: [], options: [.skipsHiddenFiles]) + + + // Filter + var files = filePaths.map(BasicFBFile.init) + if let excludesFileExtensions = excludesFileExtensions { + let lowercased = excludesFileExtensions.map { $0.lowercased() } + files = files.filter { !lowercased.contains($0.fileExtension?.lowercased() ?? "") } } - if file.displayName.isEmpty == false { - files.append(file) + if let excludesFilepaths = excludesFilepaths { + files = files.filter { !excludesFilepaths.contains($0.path) } } + + // Sort + files = files.sorted(){$0.displayName < $1.displayName} + callback(.success(files)) + } catch let error { + callback(.error(error)) + return } - // Sort - files = files.sorted(){$0.displayName < $1.displayName} - return files } + + func attributes(ofItemWithUrl fileUrl: URL) -> NSDictionary? { + let path = fileUrl.path + do { + let attributes = try fileManager.attributesOfItem(atPath: path) as NSDictionary + return attributes + } catch { + return nil + } + } + + func dataURL(forFile file: FBFile) throws -> URL { + return file.path + } + } diff --git a/FileBrowser/LoadingViewController.swift b/FileBrowser/LoadingViewController.swift new file mode 100644 index 0000000..615c200 --- /dev/null +++ b/FileBrowser/LoadingViewController.swift @@ -0,0 +1,137 @@ +// +// LoadingViewController.swift +// FileBrowser +// +// Created by Carl Julius Gödecken on 29/12/2016. +// Copyright © 2016 Carl Julius Gödecken. +// + +import Foundation +import QuickLook + + +class LoadingViewController: UIViewController, URLSessionDownloadDelegate, URLSessionDataDelegate { + //MARK: Lifecycle + + @IBOutlet var progressView: UIProgressView! + @IBOutlet var errorLabel: UILabel! + @IBOutlet var cancelButton: UIButton! + + var downloadTask: URLSessionDownloadTask? + var session: URLSession! + + var file: FBFile! + var downloadDelegate: FileBrowserDownloadDelegate? = nil + + convenience init (file: FBFile) { + self.init(nibName: "LoadingViewController", bundle: Bundle(for: LoadingViewController.self)) + self.file = file + self.title = file.displayName + + let configuration = URLSessionConfiguration.default + session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) + } + + override func viewDidLoad() { + super.viewDidLoad() + progressView.setProgress(0, animated: false) + + if let resourceUrl = file.resourceUrl { + downloadFile(at: resourceUrl) + } else if let downloadDelegate = downloadDelegate { + downloadDelegate.provideCustomDownloadUrl(for: file) { result in + switch result { + case .success(let url): + self.downloadFile(at: url) + case .error(let error): + self.show(error: error) + } + } + } else { + print("Error: No resourceUrl and no downloadDelegate provided to download file!") + } + } + + func downloadFile(at resourceUrl: URL) { + var urlRequest = URLRequest(url: resourceUrl) + downloadDelegate?.willPerformDownloadTask(for: file, using: &urlRequest) + downloadTask = session.downloadTask(with: urlRequest) + downloadTask!.resume() + } + + override func viewWillDisappear(_ animated: Bool) { + downloadTask?.cancel() + } + + + @IBAction func cancelButtonTapped(_ sender: Any) { + downloadTask?.cancel() + navigationController?.popViewController(animated: true) + } + + func showFile(data: Data?) { + let previewManager = PreviewManager() + let controller = previewManager.previewViewControllerForFile(self.file, data: data, fromNavigation: true) + DispatchQueue.main.async { + if let nav = self.navigationController { + //nav.pushViewController(controller, animated: true) + + var viewControllers = nav.viewControllers + viewControllers.removeLast(1) + viewControllers.append(controller) + nav.setViewControllers(viewControllers, animated: true) + + } else { + self.present(controller, animated: true, completion: nil) + } + if let ql = (controller as? QLPreviewController) ?? (controller as? PreviewTransitionViewController)?.quickLookPreviewController { + // fix for dataSource magically disappearing because hey let's store it in a weak variable in QLPreviewController + ql.dataSource = previewManager + ql.reloadData() + } + } + } + + func show(error: Error) { + DispatchQueue.main.async { + self.cancelButton.isHidden = true + self.progressView.isHidden = true + self.errorLabel.text = error.localizedDescription + self.errorLabel.isHidden = false + } + } + + //MARK: URLSession + + func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { + if let error = error { + show(error: error) + } + } + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + do { + let data = try Data(contentsOf: location) + try downloadDelegate?.didFinishDownloading(data: data, for: file, for: downloadTask) + self.showFile(data: data) + } catch let error { + print(error) + show(error: error) + } + } + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + let progress = Float(totalBytesWritten / totalBytesExpectedToWrite) + DispatchQueue.main.async { + self.progressView.setProgress(progress, animated: true) + } + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + if let error = error, (error as NSError).code != NSURLErrorCancelled { + show(error: error) + } + session.finishTasksAndInvalidate() + } +} + diff --git a/FileBrowser/PreviewManager.swift b/FileBrowser/PreviewManager.swift index 672283b..04522cf 100644 --- a/FileBrowser/PreviewManager.swift +++ b/FileBrowser/PreviewManager.swift @@ -11,20 +11,31 @@ import QuickLook class PreviewManager: NSObject, QLPreviewControllerDataSource { - var filePath: URL? + var file: FBFile? + var fileData: Data? + var downloadDelegate: FileBrowserDownloadDelegate? + var dataSource: FileBrowserDataSource! - func previewViewControllerForFile(_ file: FBFile, fromNavigation: Bool) -> UIViewController { + func previewViewControllerForFile(_ file: FBFile, data: Data?, fromNavigation: Bool) -> UIViewController { + if data == nil && file.isRemoteFile { + let loadingViewController = LoadingViewController(file: file) + loadingViewController.downloadDelegate = downloadDelegate + return loadingViewController + } - if file.type == .PLIST || file.type == .JSON{ + if file.type == .PLIST || file.type == .JSON { let webviewPreviewViewContoller = WebviewPreviewViewContoller(nibName: "WebviewPreviewViewContoller", bundle: Bundle(for: WebviewPreviewViewContoller.self)) + webviewPreviewViewContoller.fileData = data webviewPreviewViewContoller.file = file return webviewPreviewViewContoller } else { let previewTransitionViewController = PreviewTransitionViewController(nibName: "PreviewTransitionViewController", bundle: Bundle(for: PreviewTransitionViewController.self)) + self.file = file + self.fileData = data + previewTransitionViewController.quickLookPreviewController.dataSource = self - self.filePath = file.filePath as URL if fromNavigation == true { return previewTransitionViewController.quickLookPreviewController } @@ -32,19 +43,40 @@ class PreviewManager: NSObject, QLPreviewControllerDataSource { } } - + // MARK: delegate methods func numberOfPreviewItems(in controller: QLPreviewController) -> Int { return 1 } func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { let item = PreviewItem() - if let filePath = filePath { - item.filePath = filePath + + if let file = file, + let fileData = fileData, + let url = copyDataToTemporaryDirectory(fileData, file: file) { + item.filePath = url + } else if let file = file, let url = file.resourceUrl, url.scheme == "file" { + item.filePath = url } + return item } + func copyDataToTemporaryDirectory(_ data: Data, file: FBFile) -> URL? + { + let tempDirectoryURL = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true) + let fileExtension = file.fileExtension ?? file.type.rawValue + let targetURL = tempDirectoryURL.appendingPathComponent("\(file.displayName).\(fileExtension)") // TODO: better file extensions + + // Copy the file. + do { + try data.write(to: targetURL) + return targetURL + } catch let error { + print("Unable to copy file: \(error)") + return nil + } + } } class PreviewItem: NSObject, QLPreviewItem { diff --git a/FileBrowser/PreviewTransitionViewController.swift b/FileBrowser/PreviewTransitionViewController.swift index a2c5e28..e7ee714 100644 --- a/FileBrowser/PreviewTransitionViewController.swift +++ b/FileBrowser/PreviewTransitionViewController.swift @@ -23,5 +23,15 @@ class PreviewTransitionViewController: UIViewController { quickLookPreviewController.view.frame = containerView.bounds quickLookPreviewController.didMove(toParentViewController: self) } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.hidesBottomBarWhenPushed = true + } + + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + self.hidesBottomBarWhenPushed = true + } } diff --git a/FileBrowser/Resources/LoadingViewController.xib b/FileBrowser/Resources/LoadingViewController.xib new file mode 100644 index 0000000..12cbb8f --- /dev/null +++ b/FileBrowser/Resources/LoadingViewController.xib @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FileBrowser/WebviewPreviewViewContoller.swift b/FileBrowser/WebviewPreviewViewContoller.swift index 0c89067..b916a2a 100644 --- a/FileBrowser/WebviewPreviewViewContoller.swift +++ b/FileBrowser/WebviewPreviewViewContoller.swift @@ -20,6 +20,8 @@ class WebviewPreviewViewContoller: UIViewController { self.processForDisplay() } } + + var fileData: Data? //MARK: Lifecycle @@ -43,7 +45,17 @@ class WebviewPreviewViewContoller: UIViewController { guard let file = file else { return } - let activityViewController = UIActivityViewController(activityItems: [file.filePath], applicationActivities: nil) + + let activityItems: [Any] + if let data = fileData { + activityItems = [data] + } else if let url = file.resourceUrl { + activityItems = [url] + } else { + return + } + + let activityViewController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) self.present(activityViewController, animated: true, completion: nil) } @@ -51,9 +63,23 @@ class WebviewPreviewViewContoller: UIViewController { //MARK: Processing func processForDisplay() { - guard let file = file, let data = try? Data(contentsOf: file.filePath as URL) else { + guard let file = file else { + print("file is not set!") return } + + let data: Data + if let fileData = fileData { + data = fileData + } else if let localFileUrl = file.resourceUrl, + localFileUrl.scheme == "file", + let fileData = try? Data(contentsOf: localFileUrl) { + data = fileData + } else { + print("Could not find data for file!") + return + } + var rawString: String? // Prepare plist for display diff --git a/FileBrowserTests/FileBrowserTests.swift b/FileBrowserTests/FileBrowserTests.swift index 286b019..00565ad 100644 --- a/FileBrowserTests/FileBrowserTests.swift +++ b/FileBrowserTests/FileBrowserTests.swift @@ -23,8 +23,8 @@ class FileBrowserTests: XCTestCase { func testGifFBFileParse() { let filePath = Bundle(for: FileBrowserTests.self).url(forResource: "3crBXeO", withExtension: "gif")! - let file = FBFile(filePath: filePath) - XCTAssertEqual(file.filePath, filePath) + let file = FBFile(path: filePath) + XCTAssertEqual(file.path, filePath) XCTAssertEqual(file.isDirectory, false) XCTAssertEqual(file.type, FBFileType.GIF) XCTAssertEqual(file.fileExtension, "gif") @@ -32,8 +32,8 @@ class FileBrowserTests: XCTestCase { func testJpgFBFileParse() { let filePath = Bundle(for: FileBrowserTests.self).url(forResource: "Stitch", withExtension: "jpg")! - let file = FBFile(filePath: filePath) - XCTAssertEqual(file.filePath, filePath) + let file = FBFile(path: filePath) + XCTAssertEqual(file.path, filePath) XCTAssertEqual(file.isDirectory, false) XCTAssertEqual(file.type, FBFileType.JPG) XCTAssertEqual(file.fileExtension, "jpg") @@ -41,30 +41,45 @@ class FileBrowserTests: XCTestCase { func testDirectoryFBFileParse() { let filePath = Bundle(for: FileBrowserTests.self).bundleURL - let file = FBFile(filePath: filePath) + let file = FBFile(path: filePath) XCTAssertEqual(file.type, FBFileType.Directory) } func testDirectoryContentsParse() { - let parser = FileParser.sharedInstance + let parser = LocalFileParser() let directoryPath = Bundle(for: FileBrowserTests.self).bundleURL - let directoryContents = parser.filesForDirectory(directoryPath) - XCTAssertTrue(directoryContents.count > 0) - let stitchFile = directoryContents.filter({$0.displayName == "Stitch.jpg"}).first - XCTAssertNotNil(stitchFile) - if let stitchFile = stitchFile { - XCTAssertEqual(stitchFile.type, FBFileType.JPG) + let directory = FBFile(path: directoryPath) + + parser.provideContents(ofDirectory: directory) { result in + switch result { + case .error(let error): + XCTFail(error.localizedDescription) + case .success(let directoryContents): + XCTAssertTrue(directoryContents.count > 0) + let stitchFile = directoryContents.filter({$0.displayName == "Stitch.jpg"}).first + XCTAssertNotNil(stitchFile) + if let stitchFile = stitchFile { + XCTAssertEqual(stitchFile.type, FBFileType.JPG) + } + } } } func testCaseSensitiveExclusion() { - let parser = FileParser.sharedInstance + let parser = LocalFileParser() parser.excludesFileExtensions = ["gIf"] let directoryPath = Bundle(for: FileBrowserTests.self).bundleURL - let directoryContents = parser.filesForDirectory(directoryPath) - for file in directoryContents { - if let fileExtension = file.fileExtension { - XCTAssertFalse(fileExtension == "gif") + let directory = FBFile(path: directoryPath) + parser.provideContents(ofDirectory: directory) { result in + switch result { + case .error(let error): + XCTFail(error.localizedDescription) + case .success(let directoryContents): + for file in directoryContents { + if let fileExtension = file.fileExtension { + XCTAssertFalse(fileExtension == "gif") + } + } } } } diff --git a/FileBrowserWorkspace.xcworkspace/contents.xcworkspacedata b/FileBrowserWorkspace.xcworkspace/contents.xcworkspacedata index 877319b..02565c0 100644 --- a/FileBrowserWorkspace.xcworkspace/contents.xcworkspacedata +++ b/FileBrowserWorkspace.xcworkspace/contents.xcworkspacedata @@ -7,4 +7,7 @@ + + diff --git a/examples/Sample/Podfile b/examples/Sample/Podfile index c42d04e..2a96cc8 100644 --- a/examples/Sample/Podfile +++ b/examples/Sample/Podfile @@ -2,6 +2,17 @@ source 'https://github.com/CocoaPods/Specs.git' platform :ios, "8.0" use_frameworks! +project 'Sample' +# workspace '../../FileBrowserWorkspace' + target 'Sample' do - pod 'FileBrowser', :path => '../..' + pod 'FileBrowser', :path => '../../FileBrowser.podspec' +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['SWIFT_VERSION'] = '3.0' + end + end end diff --git a/examples/Sample/Podfile.lock b/examples/Sample/Podfile.lock new file mode 100644 index 0000000..8200855 --- /dev/null +++ b/examples/Sample/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - FileBrowser (0.2.0) + +DEPENDENCIES: + - FileBrowser (from `../../FileBrowser.podspec`) + +EXTERNAL SOURCES: + FileBrowser: + :path: ../../FileBrowser.podspec + +SPEC CHECKSUMS: + FileBrowser: af8abc035daca66a6066351ae162c4af4c6b24dd + +PODFILE CHECKSUM: 25d6e7f3f21422400c1d0693c31a9c3655ff1c8b + +COCOAPODS: 1.2.1 diff --git a/examples/Sample/Sample.xcodeproj/project.pbxproj b/examples/Sample/Sample.xcodeproj/project.pbxproj index 2ef86d5..592629a 100644 --- a/examples/Sample/Sample.xcodeproj/project.pbxproj +++ b/examples/Sample/Sample.xcodeproj/project.pbxproj @@ -16,7 +16,11 @@ 48A199CE1D8C546000C443D3 /* BB8.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 48A199CA1D8C546000C443D3 /* BB8.jpg */; }; 48A199CF1D8C546000C443D3 /* Images.zip in Resources */ = {isa = PBXBuildFile; fileRef = 48A199CB1D8C546000C443D3 /* Images.zip */; }; 48A199D01D8C546000C443D3 /* Stitch.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 48A199CC1D8C546000C443D3 /* Stitch.jpg */; }; - 48A24C331D8C59250044C697 /* FileBrowser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 48A24C321D8C59250044C697 /* FileBrowser.framework */; }; + 760CE3C0DC5DA79C68460A63 /* Pods_Sample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2C28D7CA33CEE3E4A364447 /* Pods_Sample.framework */; }; + C3368F3D1E167B91003F4664 /* FileBrowser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3368F3C1E167B91003F4664 /* FileBrowser.framework */; }; + C3BF0DFE1E13F5340009D12C /* CustomDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BF0DFD1E13F5340009D12C /* CustomDataSource.swift */; }; + C3BF0E061E1563F10009D12C /* Podfile in Resources */ = {isa = PBXBuildFile; fileRef = C3BF0E051E1563F10009D12C /* Podfile */; }; + C3BF0E101E1591DC0009D12C /* folderContent.json in Resources */ = {isa = PBXBuildFile; fileRef = C3BF0E0F1E1591DC0009D12C /* folderContent.json */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -33,6 +37,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 28E13CD638951BF207ADFF8D /* Pods-Sample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.debug.xcconfig"; sourceTree = ""; }; 48A199AD1D8C53AC00C443D3 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 48A199B51D8C53AC00C443D3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 48A199B71D8C53AC00C443D3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -45,6 +50,14 @@ 48A199CB1D8C546000C443D3 /* Images.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = Images.zip; sourceTree = ""; }; 48A199CC1D8C546000C443D3 /* Stitch.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Stitch.jpg; sourceTree = ""; }; 48A24C321D8C59250044C697 /* FileBrowser.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FileBrowser.framework; path = "../../../../Library/Developer/Xcode/DerivedData/FileBrowserWorkspace-gvwbufuzyigeyaecovnmsyswnfyi/Build/Products/Debug-iphoneos/FileBrowser.framework"; sourceTree = ""; }; + C3368F3C1E167B91003F4664 /* FileBrowser.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FileBrowser.framework; path = "../../build/Debug-iphoneos/FileBrowser.framework"; sourceTree = ""; }; + C3BF0DFD1E13F5340009D12C /* CustomDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CustomDataSource.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + C3BF0E051E1563F10009D12C /* Podfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Podfile; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + C3BF0E071E156BE60009D12C /* FileBrowser.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FileBrowser.framework; path = "../../build/Debug-iphoneos/FileBrowser.framework"; sourceTree = ""; }; + C3BF0E0B1E15723A0009D12C /* FileBrowser.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FileBrowser.framework; path = "../../build/Debug-iphoneos/FileBrowser.framework"; sourceTree = ""; }; + C3BF0E0F1E1591DC0009D12C /* folderContent.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = folderContent.json; sourceTree = ""; }; + D2C28D7CA33CEE3E4A364447 /* Pods_Sample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Sample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FFE0589B53B1161442905147 /* Pods-Sample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.release.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -52,7 +65,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 48A24C331D8C59250044C697 /* FileBrowser.framework in Frameworks */, + C3368F3D1E167B91003F4664 /* FileBrowser.framework in Frameworks */, + 760CE3C0DC5DA79C68460A63 /* Pods_Sample.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -65,6 +79,8 @@ 48A199AF1D8C53AC00C443D3 /* Sample */, 48A199AE1D8C53AC00C443D3 /* Products */, 48A24C2F1D8C57390044C697 /* Frameworks */, + C3BF0E051E1563F10009D12C /* Podfile */, + 7941567C86F4C287F933E7D4 /* Pods */, ); sourceTree = ""; }; @@ -90,6 +106,7 @@ children = ( 48A199D11D8C546500C443D3 /* Resources */, 48A199C31D8C53DB00C443D3 /* AppDelegate.swift */, + C3BF0DFD1E13F5340009D12C /* CustomDataSource.swift */, 48A199C41D8C53DB00C443D3 /* Info.plist */, 48A199B41D8C53AC00C443D3 /* Main.storyboard */, 48A199B71D8C53AC00C443D3 /* Assets.xcassets */, @@ -105,6 +122,7 @@ 48A199CA1D8C546000C443D3 /* BB8.jpg */, 48A199CB1D8C546000C443D3 /* Images.zip */, 48A199CC1D8C546000C443D3 /* Stitch.jpg */, + C3BF0E0F1E1591DC0009D12C /* folderContent.json */, ); name = Resources; sourceTree = ""; @@ -120,11 +138,24 @@ 48A24C2F1D8C57390044C697 /* Frameworks */ = { isa = PBXGroup; children = ( + C3368F3C1E167B91003F4664 /* FileBrowser.framework */, + C3BF0E0B1E15723A0009D12C /* FileBrowser.framework */, + C3BF0E071E156BE60009D12C /* FileBrowser.framework */, 48A24C321D8C59250044C697 /* FileBrowser.framework */, + D2C28D7CA33CEE3E4A364447 /* Pods_Sample.framework */, ); name = Frameworks; sourceTree = ""; }; + 7941567C86F4C287F933E7D4 /* Pods */ = { + isa = PBXGroup; + children = ( + 28E13CD638951BF207ADFF8D /* Pods-Sample.debug.xcconfig */, + FFE0589B53B1161442905147 /* Pods-Sample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -132,10 +163,13 @@ isa = PBXNativeTarget; buildConfigurationList = 48A199BF1D8C53AC00C443D3 /* Build configuration list for PBXNativeTarget "Sample" */; buildPhases = ( + FE1FF64C54996947165EA6D1 /* [CP] Check Pods Manifest.lock */, 48A199A91D8C53AC00C443D3 /* Sources */, 48A199AA1D8C53AC00C443D3 /* Frameworks */, 48A199AB1D8C53AC00C443D3 /* Resources */, 48A199DD1D8C564100C443D3 /* Embed Frameworks */, + E5DD55652D3BD1C4AF23C734 /* [CP] Embed Pods Frameworks */, + 46314AB0569B89DDC16A2375 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -153,12 +187,11 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0800; - LastUpgradeCheck = 0800; + LastUpgradeCheck = 0830; ORGANIZATIONNAME = "Mihail Șalari"; TargetAttributes = { 48A199AC1D8C53AC00C443D3 = { CreatedOnToolsVersion = 8.0; - DevelopmentTeam = U82TG7992X; LastSwiftMigration = 0800; ProvisioningStyle = Automatic; }; @@ -189,16 +222,66 @@ files = ( 48A199BB1D8C53AC00C443D3 /* LaunchScreen.storyboard in Resources */, 48A199CF1D8C546000C443D3 /* Images.zip in Resources */, + C3BF0E061E1563F10009D12C /* Podfile in Resources */, 48A199B81D8C53AC00C443D3 /* Assets.xcassets in Resources */, 48A199B61D8C53AC00C443D3 /* Main.storyboard in Resources */, 48A199CD1D8C546000C443D3 /* Baymax.jpg in Resources */, 48A199D01D8C546000C443D3 /* Stitch.jpg in Resources */, 48A199CE1D8C546000C443D3 /* BB8.jpg in Resources */, + C3BF0E101E1591DC0009D12C /* folderContent.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 46314AB0569B89DDC16A2375 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E5DD55652D3BD1C4AF23C734 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + FE1FF64C54996947165EA6D1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 48A199A91D8C53AC00C443D3 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -206,6 +289,7 @@ files = ( 48A199C81D8C53E200C443D3 /* MainViewController.swift in Sources */, 48A199C51D8C53DB00C443D3 /* AppDelegate.swift in Sources */, + C3BF0DFE1E13F5340009D12C /* CustomDataSource.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -249,10 +333,10 @@ CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -299,10 +383,10 @@ CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; @@ -326,11 +410,13 @@ }; 48A199C01D8C53AC00C443D3 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 28E13CD638951BF207ADFF8D /* Pods-Sample.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = U82TG7992X; - FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Sample/Supporting Files/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.mihailsalari.Sample; @@ -342,11 +428,13 @@ }; 48A199C11D8C53AC00C443D3 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = FFE0589B53B1161442905147 /* Pods-Sample.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = U82TG7992X; - FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Sample/Supporting Files/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.mihailsalari.Sample; diff --git a/examples/Sample/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples/Sample/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme index e8a4bce..b61189c 100644 --- a/examples/Sample/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme +++ b/examples/Sample/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -1,6 +1,6 @@ + + + + + + diff --git a/examples/Sample/Sample/Base.lproj/Main.storyboard b/examples/Sample/Sample/Base.lproj/Main.storyboard index bb4bbb2..bde2fec 100644 --- a/examples/Sample/Sample/Base.lproj/Main.storyboard +++ b/examples/Sample/Sample/Base.lproj/Main.storyboard @@ -1,8 +1,11 @@ - - + + + + + - + @@ -19,13 +22,21 @@ + diff --git a/examples/Sample/Sample/CustomDataSource.swift b/examples/Sample/Sample/CustomDataSource.swift new file mode 100644 index 0000000..1111ac3 --- /dev/null +++ b/examples/Sample/Sample/CustomDataSource.swift @@ -0,0 +1,97 @@ +// +// CustomDataSource.swift +// Sample +// +// Created by Carl Julius Gödecken on 28/12/2016. +// Copyright © 2016 Carl Julius Gödecken. +// + +import Foundation +import FileBrowser + +open class CustomDataSource: FileBrowserDataSource { + + typealias KeyValue = [String: Any] + let json: KeyValue + + init() { + let jsonPath = Bundle.main.path(forResource: "folderContent", ofType: "json")! + let data = try! NSData(contentsOfFile: jsonPath, options: []) as Data + json = try! JSONSerialization.jsonObject(with: data, options: []) as! KeyValue + } + + public var excludesFileExtensions: [String]? = nil + public var excludesFilepaths: [URL]? = nil + + let rootUrl = URL(string: "/")! + public var rootDirectory: FBFile { + let file = BasicFBFile(path: rootUrl) + file.displayName = "Home" + return file + } + + let fileManager = FileManager.default + + + open func provideContents(ofDirectory directory: FBFile, callback: @escaping (FBResult<[FBFile]>) -> ()) { + // traverse the file tree outlined in the JSON file to find the directory + let pathComponents = Array(directory.path.pathComponents.dropFirst()) // we're already in the root directory at the root of our json document + do { + let directoryDescription = try pathComponents.reduce(json) { currentFolder, subfolderName throws -> KeyValue in + guard let content = currentFolder["content"] as? KeyValue else { + throw JSONParsingError.noDirectoryContent + } + return content[subfolderName] as! KeyValue + } + + guard let content = directoryDescription["content"] as? KeyValue else { + throw JSONParsingError.noDirectoryContent + } + let files = content.map {name, properties -> BasicFBFile in + let properties = properties as! KeyValue + let isDirectory = (properties["type"] as? String) == "directory" + let path = directory.path.appendingPathComponent(name, isDirectory: isDirectory) + let file = BasicFBFile(path: path) + if let resourceURLString = properties["location"] as? String, let resourceURL = URL(string: resourceURLString) { + file.resourceUrl = resourceURL + } + if let typeName = properties["type"] as? String, let type = FBFileType(rawValue: typeName) { + file.type = type + } + return file + } + + // simulate loading of remote content + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: { + callback(.success(files)) + }) + } catch let error { + callback(.error(error)) + } + + return + } + + public func attributes(ofItemWithUrl fileUrl: URL) -> NSDictionary? { + return nil + } + + + public func dataURL(forFile file: FBFile) throws -> URL { + return file.resourceUrl! + } + +} + +enum JSONParsingError: Error { + case noDirectoryContent +} + +extension JSONParsingError: LocalizedError { + public var errorDescription: String? { + switch self { + case .noDirectoryContent: + return "Cannot read the directory contents" + } + } +} diff --git a/examples/Sample/Sample/View Controller/MainViewController.swift b/examples/Sample/Sample/View Controller/MainViewController.swift index c5594fe..e4920e3 100644 --- a/examples/Sample/Sample/View Controller/MainViewController.swift +++ b/examples/Sample/Sample/View Controller/MainViewController.swift @@ -26,7 +26,12 @@ class MainViewController: UIViewController { @IBAction func showFileBrowser(sender: AnyObject) { let file = FileBrowser() present(file, animated: true, completion: nil) - //self.present(fileBrowser, animated: true, completion: nil) + } + + @IBAction func showCustomFileBrowser(_ sender: Any) { + let dataSource = CustomDataSource() + let browser = FileBrowser(dataSource: dataSource) + present(browser, animated: true, completion: nil) } } diff --git a/examples/Sample/Sample/folderContent.json b/examples/Sample/Sample/folderContent.json new file mode 100644 index 0000000..e5490be --- /dev/null +++ b/examples/Sample/Sample/folderContent.json @@ -0,0 +1,43 @@ +{ + "type": "directory", + "content": { + "Random image": { + "type": "jpg", + "location": "https://unsplash.it/200/300/?random" + }, + "Larger random image": { + "type": "jpg", + "location": "https://unsplash.it/1200/1800/?random" + }, + "More images": { + "type": "directory", + "content": { + "Random square image": { + "type": "jpg", + "location": "https://unsplash.it/200/?random" + }, + "Yet another random image": { + "type": "jpg", + "location": "https://unsplash.it/200/300/?random" + }, + "Very small and blurry image": { + "type": "jpg", + "location": "https://unsplash.it/16/16/?random" + } + } + }, + "Documents": { + "type": "directory", + "content": { + "users.json": { + "location": "https://jsonplaceholder.typicode.com/users" + } + } + }, + "Invalid directory": { + "type": "directory", + "notContent": {} + } + } +} +