From 7e5ff833e251ab7da1cc86c341b918679347878a Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Sat, 14 Jan 2017 19:42:25 +0900 Subject: [PATCH] Fix synchronization --- Package.swift | 2 +- Sources/CodeRenderer.swift | 88 ++++++++++++++++-------- Sources/Gist.swift | 2 +- Sources/Speaker.swift | 22 ++---- Sources/Twitter.swift | 4 +- Tests/TweetupKitTests/SpeakerTests.swift | 6 +- 6 files changed, 75 insertions(+), 49 deletions(-) diff --git a/Package.swift b/Package.swift index cd77375..ca8695b 100644 --- a/Package.swift +++ b/Package.swift @@ -3,6 +3,6 @@ import PackageDescription let package = Package( name: "TweetupKit", dependencies: [ - .Package(url: "https://github.com/swift-tweets/OAuthSwift.git", "1.2.0-beta") + .Package(url: "https://github.com/swift-tweets/OAuthSwift.git", "2.0.0-beta") ] ) diff --git a/Sources/CodeRenderer.swift b/Sources/CodeRenderer.swift index fb1cbd3..7c52ec9 100644 --- a/Sources/CodeRenderer.swift +++ b/Sources/CodeRenderer.swift @@ -4,47 +4,65 @@ import Foundation internal class CodeRenderer: NSObject { private var webView: WebView! fileprivate var loading = true - fileprivate var _image: CGImage! - fileprivate var error: Error? + private var getImage: (() throws -> CGImage)? = nil + private var completions: [(() throws -> CGImage) -> ()] = [] + private var zelf: CodeRenderer? // not to released during the async operation fileprivate static let height: CGFloat = 736 init(url: String) { super.init() + zelf = self - webView = WebView(frame: NSRect(x: 0, y: 0, width: 414, height: CodeRenderer.height)) - webView.frameLoadDelegate = self - webView.customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1" - webView.mainFrameURL = url - - let runLoop = RunLoop.current - while loading && runLoop.run(mode: .defaultRunLoopMode, before: .distantFuture) { } + DispatchQueue.main.async { + self.webView = WebView(frame: NSRect(x: 0, y: 0, width: 414, height: CodeRenderer.height)) + self.webView.frameLoadDelegate = self + self.webView.customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1" + self.webView.mainFrameURL = url + } } - func image() throws -> CGImage { - if let error = self.error { - throw error + func image(completion: @escaping (() throws -> CGImage) -> ()) { + DispatchQueue.main.async { + if let getImage = self.getImage { + completion { + try getImage() + } + } + + self.completions.append(completion) } - - return _image } - func writeImage(to path: String) throws { - let image = try self.image() - let url = URL(fileURLWithPath: path) - guard let destination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypePNG, 1, nil) else { - throw CodeRendererError.writingFailed + func writeImage(to path: String, completion: @escaping (() throws -> ()) -> ()) { + image { getImage in + completion { + let image = try getImage() + let url = URL(fileURLWithPath: path) + guard let destination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypePNG, 1, nil) else { + throw CodeRendererError.writingFailed + } + + CGImageDestinationAddImage(destination, image, nil) + + guard CGImageDestinationFinalize(destination) else { + throw CodeRendererError.writingFailed + } + } } - - CGImageDestinationAddImage(destination, image, nil) - - guard CGImageDestinationFinalize(destination) else { - throw CodeRendererError.writingFailed + } + + fileprivate func resolve(getImage: @escaping (() throws -> CGImage)) { + for completion in completions { + completion(getImage) } + completions.removeAll() + self.getImage = getImage + self.zelf = nil } } -extension CodeRenderer: WebFrameLoadDelegate { +extension CodeRenderer: WebFrameLoadDelegate { // called on the main thread func webView(_ sender: WebView, didFinishLoadFor frame: WebFrame) { let document = frame.domDocument! let body = document.getElementsByTagName("body").item(0)! @@ -53,7 +71,7 @@ extension CodeRenderer: WebFrameLoadDelegate { let files = document.getElementsByClassName("blob-file-content")! guard files.length > 0 else { - error = CodeRendererError.illegalResponse + resolve(getImage: { throw CodeRendererError.illegalResponse } ) return } let code = files.item(0) as! DOMElement @@ -78,13 +96,27 @@ extension CodeRenderer: WebFrameLoadDelegate { context.draw(imageRep.cgImage!, in: targetRect) let provider: CGDataProvider = CGDataProvider(data: Data(bytes: pixels) as CFData)! - _image = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent) + resolve(getImage: { + CGImage( + width: width, + height: height, + bitsPerComponent: 8, + bitsPerPixel: 32, + bytesPerRow: width * 4, + space: colorSpace, + bitmapInfo: bitmapInfo, + provider: provider, + decode: nil, + shouldInterpolate: false, + intent: .defaultIntent + )! + }) loading = false } func webView(_ sender: WebView, didFailLoadWithError error: Error, for frame: WebFrame) { - self.error = error + resolve(getImage: { throw error }) loading = false } } diff --git a/Sources/Gist.swift b/Sources/Gist.swift index cdf6ad3..04c197c 100644 --- a/Sources/Gist.swift +++ b/Sources/Gist.swift @@ -2,7 +2,7 @@ import Foundation internal struct Gist { static func createGist(description: String, code: Code, accessToken: String, callback: @escaping (() throws -> String) -> ()) { - let session = URLSession(configuration: .ephemeral) + let session = URLSession(configuration: .ephemeral, delegate: nil, delegateQueue: .current) var request = URLRequest(url: URL(string: "https://api.github.com/gists")!) request.httpMethod = "POST" request.addValue("token \(accessToken)", forHTTPHeaderField: "Authorization") diff --git a/Sources/Speaker.swift b/Sources/Speaker.swift index c103cf5..68159c6 100644 --- a/Sources/Speaker.swift +++ b/Sources/Speaker.swift @@ -46,7 +46,6 @@ public struct Speaker { return } - // TODO let resolve = flatten(flatten(resolveCode, resolveGist), resolveImage) resolve(tweet) { getTweet in do { @@ -167,20 +166,13 @@ public struct Speaker { return } - DispatchQueue.main.async { - do { - let url = "https://gist.github.com/\(id)" - let imagePath = outputDirectoryPath.appendingPathComponent("\(id).png") - let codeRenderer = CodeRenderer(url: url) - try codeRenderer.writeImage(to: Speaker.imagePath(imagePath, from: self.baseDirectoryPath)) - - callback { - return try Tweet(body: "\(tweet.body)", attachment: .image(Image(alternativeText: image.alternativeText, source: .local(imagePath)))) - } - } catch let error { - callback { - throw error - } + let url = "https://gist.github.com/\(id)" + let imagePath = outputDirectoryPath.appendingPathComponent("\(id).png") + let codeRenderer = CodeRenderer(url: url) + codeRenderer.writeImage(to: Speaker.imagePath(imagePath, from: self.baseDirectoryPath)) { getVoid in + callback { + try getVoid() + return try Tweet(body: "\(tweet.body)", attachment: .image(Image(alternativeText: image.alternativeText, source: .local(imagePath)))) } } } diff --git a/Sources/Twitter.swift b/Sources/Twitter.swift index f702909..56e2e4b 100644 --- a/Sources/Twitter.swift +++ b/Sources/Twitter.swift @@ -4,7 +4,8 @@ import Foundation internal struct Twitter { static func update(status: String, mediaId: String? = nil, credential: OAuthCredential, callback: @escaping (() throws -> (String, String)) -> ()) { let client = OAuthSwiftClient(credential: credential) - + client.sessionFactory.queue = { .current } + var parameters = [ "status": status ] @@ -24,6 +25,7 @@ internal struct Twitter { static func upload(media: Data, credential: OAuthCredential, callback: @escaping (() throws -> String) -> ()) { let client = OAuthSwiftClient(credential: credential) + client.sessionFactory.queue = { .current } _ = client.post( "https://upload.twitter.com/1.1/media/upload.json", diff --git a/Tests/TweetupKitTests/SpeakerTests.swift b/Tests/TweetupKitTests/SpeakerTests.swift index cf455f0..4439dbf 100644 --- a/Tests/TweetupKitTests/SpeakerTests.swift +++ b/Tests/TweetupKitTests/SpeakerTests.swift @@ -34,7 +34,7 @@ class SpeakerTests: XCTestCase { let string = "Twinkle, twinkle, little star,\nHow I wonder what you are! \(start)\n\n---\n\nUp above the world so high,\nLike a diamond in the sky. \(start)\n\n```swift:hello.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\n\n---\n\nTwinkle, twinkle, little star,\nHow I wonder what you are! \(start)\n\n![](\(imagePath))" // includes `start` to avoid duplicate tweets let tweets = try! Tweet.tweets(from: string) - speaker.post(tweets: tweets, with: 30.0) { getIds in + speaker.post(tweets: tweets, with: 5.0) { getIds in defer { expectation.fulfill() } @@ -50,11 +50,11 @@ class SpeakerTests: XCTestCase { } } - waitForExpectations(timeout: 89.0, handler: nil) + waitForExpectations(timeout: 14.0, handler: nil) let end = Date.timeIntervalSinceReferenceDate - XCTAssertGreaterThan(end - start, 60.0) + XCTAssertGreaterThan(end - start, 10.0) } do { // error duraing posting tweets