Skip to content

Commit

Permalink
Use Result type (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus committed Jan 17, 2019
1 parent 6e7d7d6 commit 8a764ad
Show file tree
Hide file tree
Showing 2 changed files with 296 additions and 35 deletions.
72 changes: 39 additions & 33 deletions Gifski/Gifski.swift
Expand Up @@ -4,7 +4,7 @@ import AVFoundation
final class Gifski {
enum Error: LocalizedError {
case invalidSettings
case generateFrameFailed
case generateFrameFailed(Swift.Error)
case addFrameFailed(GifskiWrapperError)
case endAddingFramesFailed(GifskiWrapperError)
case writeFailed(GifskiWrapperError)
Expand All @@ -13,8 +13,8 @@ final class Gifski {
switch self {
case .invalidSettings:
return "Invalid settings"
case .generateFrameFailed:
return "Failed to generate frame"
case .generateFrameFailed(let error):
return "Failed to generate frame: \(error.localizedDescription)"
case .addFrameFailed(let error):
return "Failed to add frame, with underlying error: \(error.localizedDescription)"
case .endAddingFramesFailed(let error):
Expand Down Expand Up @@ -87,39 +87,45 @@ final class Gifski {
frameForTimes.append(CMTime(seconds: (1 / fps) * Double(i), preferredTimescale: .video))
}

var frameIndex = 0
generator.generateCGImagesAsynchronously(forTimePoints: frameForTimes) { _, image, _, _, error in
guard let image = image,
let data = image.dataProvider?.data,
let buffer = CFDataGetBytePtr(data)
else {
completionHandler?(.generateFrameFailed)
return
}

do {
try g.addFrameARGB(
index: UInt32(frameIndex),
width: UInt32(image.width),
bytesPerRow: UInt32(image.bytesPerRow),
height: UInt32(image.height),
pixels: buffer,
delay: UInt16(100 / fps)
)
} catch {
completionHandler?(.addFrameFailed(error as! GifskiWrapperError))
return
}
generator.generateCGImagesAsynchronously(forTimePoints: frameForTimes) { result in
switch result {
case .success(let result):
let image = result.image

guard
let data = image.dataProvider?.data,
let buffer = CFDataGetBytePtr(data)
else {
completionHandler?(.generateFrameFailed("Could not get byte pointer of image data provider"))
return
}

frameIndex += 1
do {
try g.addFrameARGB(
index: UInt32(result.completedCount),
width: UInt32(image.width),
bytesPerRow: UInt32(image.bytesPerRow),
height: UInt32(image.height),
pixels: buffer,
delay: UInt16(100 / fps)
)
} catch {
completionHandler?(.addFrameFailed(error as! GifskiWrapperError))
return
}

do {
if frameIndex == frameForTimes.count {
try g.endAddingFrames()
if result.isFinished {
do {
try g.endAddingFrames()
} catch {
completionHandler?(.endAddingFramesFailed(error as! GifskiWrapperError))
}
}
} catch {
completionHandler?(.endAddingFramesFailed(error as! GifskiWrapperError))
return
case .failure where result.isCancelled:
// TODO: Handle cancellation
print("Cancelled")
case .failure(let error):
completionHandler?(.generateFrameFailed(error))
}
}

Expand Down
259 changes: 257 additions & 2 deletions Gifski/util.swift
Expand Up @@ -2,6 +2,10 @@ import Cocoa
import AVFoundation


/// YOLO
extension String: Error {}


/**
Convenience function for initializing an object and modifying its properties
Expand Down Expand Up @@ -296,9 +300,57 @@ extension NSAlert {


extension AVAssetImageGenerator {
func generateCGImagesAsynchronously(forTimePoints timePoints: [CMTime], completionHandler: @escaping AVAssetImageGeneratorCompletionHandler) {
struct CompletionHandlerResult {
let image: CGImage
let requestedTime: CMTime
let actualTime: CMTime
let completedCount: Int
let totalCount: Int
let isCancelled: Bool
let isFinished: Bool
}

/// TODO: Remove this when using Swift 5 and use `CancellationError` in the cancellation case
enum Error: CancellableError {
case cancelled

var isCancelled: Bool {
return self == .cancelled
}
}

func generateCGImagesAsynchronously(
forTimePoints timePoints: [CMTime],
completionHandler: @escaping (CoreResult<CompletionHandlerResult, Error>) -> Void
) {
let times = timePoints.map { NSValue(time: $0) }
generateCGImagesAsynchronously(forTimes: times, completionHandler: completionHandler)
let totalCount = times.count
var completedCount = 0

generateCGImagesAsynchronously(forTimes: times) { requestedTime, image, actualTime, result, error in
switch result {
case .succeeded:
completedCount += 1

completionHandler(
.success(
CompletionHandlerResult(
image: image!,
requestedTime: requestedTime,
actualTime: actualTime,
completedCount: completedCount,
totalCount: totalCount,
isCancelled: false,
isFinished: completedCount == totalCount
)
)
)
case .failed:
completionHandler(.failure(error! as! Error))
case .cancelled:
completionHandler(.failure(.cancelled))
}
}
}
}

Expand Down Expand Up @@ -1118,3 +1170,206 @@ extension CGRect {
)
}
}


/// Polyfill for Swift 5
/// https://github.com/moiseev/swift/blob/47740c012943020aa89df93129b4fc2f33618c00/stdlib/public/core/Result.swift
/// TODO: Remove when using Swift 5
///
/// A value that represents either a success or a failure, including an
/// associated value in each case.
public enum Result<Success, Failure: Swift.Error> {
/// A success, storing a `Success` value.
case success(Success)

/// A failure, storing a `Failure` value.
case failure(Failure)

/// Returns a new result, mapping any success value using the given
/// transformation.
///
/// Use this method when you need to transform the value of a `Result`
/// instance when it represents a success. The following example transforms
/// the integer success value of a result into a string:
///
/// func getNextInteger() -> Result<Int, Error> { ... }
///
/// let integerResult = getNextInteger()
/// // integerResult == .success(5)
/// let stringResult = integerResult.map({ String($0) })
/// // stringResult == .success("5")
///
/// - Parameter transform: A closure that takes the success value of this
/// instance.
/// - Returns: A `Result` instance with the result of evaluating `transform`
/// as the new success value if this instance represents a success.
public func map<NewSuccess>(
_ transform: (Success) -> NewSuccess
) -> Result<NewSuccess, Failure> {
switch self {
case let .success(success):
return .success(transform(success))
case let .failure(failure):
return .failure(failure)
}
}

/// Returns a new result, mapping any failure value using the given
/// transformation.
///
/// Use this method when you need to transform the value of a `Result`
/// instance when it represents a failure. The following example transforms
/// the error value of a result by wrapping it in a custom `Error` type:
///
/// struct DatedError: Error {
/// var error: Error
/// var date: Date
///
/// init(_ error: Error) {
/// self.error = error
/// self.date = Date()
/// }
/// }
///
/// let result: Result<Int, Error> = ...
/// // result == .failure(<error value>)
/// let resultWithDatedError = result.mapError({ e in DatedError(e) })
/// // result == .failure(DatedError(error: <error value>, date: <date>))
///
/// - Parameter transform: A closure that takes the failure value of the
/// instance.
/// - Returns: A `Result` instance with the result of evaluating `transform`
/// as the new failure value if this instance represents a failure.
public func mapError<NewFailure>(
_ transform: (Failure) -> NewFailure
) -> Result<Success, NewFailure> {
switch self {
case let .success(success):
return .success(success)
case let .failure(failure):
return .failure(transform(failure))
}
}

/// Returns a new result, mapping any success value using the given
/// transformation and unwrapping the produced result.
///
/// - Parameter transform: A closure that takes the success value of the
/// instance.
/// - Returns: A `Result` instance with the result of evaluating `transform`
/// as the new failure value if this instance represents a failure.
public func flatMap<NewSuccess>(
_ transform: (Success) -> Result<NewSuccess, Failure>
) -> Result<NewSuccess, Failure> {
switch self {
case let .success(success):
return transform(success)
case let .failure(failure):
return .failure(failure)
}
}

/// Returns a new result, mapping any failure value using the given
/// transformation and unwrapping the produced result.
///
/// - Parameter transform: A closure that takes the failure value of the
/// instance.
/// - Returns: A `Result` instance, either from the closure or the previous
/// `.success`.
public func flatMapError<NewFailure>(
_ transform: (Failure) -> Result<Success, NewFailure>
) -> Result<Success, NewFailure> {
switch self {
case let .success(success):
return .success(success)
case let .failure(failure):
return transform(failure)
}
}

/// Returns the success value as a throwing expression.
///
/// Use this method to retrieve the value of this result if it represents a
/// success, or to catch the value if it represents a failure.
///
/// let integerResult: Result<Int, Error> = .success(5)
/// do {
/// let value = try integerResult.get()
/// print("The value is \(value).")
/// } catch error {
/// print("Error retrieving the value: \(error)")
/// }
/// // Prints "The value is 5."
///
/// - Returns: The success value, if the instance represent a success.
/// - Throws: The failure value, if the instance represents a failure.
public func get() throws -> Success {
switch self {
case let .success(success):
return success
case let .failure(failure):
throw failure
}
}
}

extension Result: Equatable where Success: Equatable, Failure: Equatable {}
extension Result: Hashable where Success: Hashable, Failure: Hashable {}

// To be able to use it in places that already have a local result
// TODO: Remove this when using Swift 5
typealias CoreResult = Result


public protocol CancellableError: Error {
/// Returns true if this Error represents a cancelled condition
var isCancelled: Bool { get }
}

public struct CancellationError: CancellableError {
public var isCancelled = true
}

extension Error {
public var isCancelled: Bool {
do {
throw self
} catch let error as CancellableError {
return error.isCancelled
} catch URLError.cancelled {
return true
} catch CocoaError.userCancelled {
return true
} catch {
#if os(macOS) || os(iOS) || os(tvOS)
let pair = { ($0.domain, $0.code) }(error as NSError)
return pair == ("SKErrorDomain", 2)
#else
return false
#endif
}
}
}

extension Result {
/**
```
switch result {
case .success(let value):
print(value)
case .failure where result.isCancelled:
print("Cancelled")
case .failure(let error):
print(error)
}
```
*/
public var isCancelled: Bool {
do {
_ = try get()
return false
} catch {
return error.isCancelled
}
}
}

0 comments on commit 8a764ad

Please sign in to comment.