Hi - this came up just now during a discussion with a friend (cc @mgrebenets)
I have a UserManager
which retrieves User
objects from the network and wish to expose an interface on the UserManager
as follows:
func getUsers() -> SignalProducer<User, ErrorType>
(I'm returning a SignalProducer
as I want to flatMap
this network request with requests to other network resources).
The catch is that I only want to start
the network request wrapped up in the SignalProducer
if there's not a request currently in progress.
My intuition to solve this went like this:
class UserManager {
static let sharedManager = UserManager()
private var requestSignal: Signal<User, NSError>? = nil
func getUsers() -> SignalProducer<User, NSError> {
if let requestSignal = requestSignal {
print("There is already a request in progress so simply return a reference to its signal")
return SignalProducer(signal: requestSignal)
} else {
let requestSignalProducer = SignalProducer<User, NSError> { observer, disposable in
let url = NSURL(string:"http://jsonplaceholder.typicode.com/users/1")!
let task = NSURLSession.sharedSession()
.dataTaskWithURL(url) { (data, response, error) in
if error != nil {
observer.sendFailed(NSError(domain:"", code:5, userInfo:nil))
} else {
let json = try! NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions())
let user = User(JSON: json)
print("Completed user request at \(NSDate())")
observer.sendNext(user)
observer.sendCompleted()
}
}
print("Started user request at \(NSDate())")
task.resume()
}
requestSignalProducer.startWithSignal{ [weak self] (signal, disposable) in
guard let `self` = self else { return }
`self`.requestSignal = signal
signal.observeCompleted {
print("Completing the user request signal")
`self`.requestSignal = nil
}
}
return SignalProducer(signal: self.requestSignal!)
}
}
}
Buuuut the mutable state in the requestSignal
feels really "Un-RAC". Would appreciate any pointers you could give on a solution which is more in line with the RAC principles.
Cheers! 馃榾
Using replayLazily
would simplify things a bit鈥攖hen you could save the SignalProducer
directly instead of redirecting a signal.
Also, there's a race condition here, so you might want to use RAC's Atomic
type. (If 2 threads call getUsers
at the same time, you could could end up with 2 requests.)
I'd also recommend splitting the actual API code into a separate method or class. That will make the code a little easier to read and separate the concerns a little better.
struct API {
static func getUsers() -> SignalProducer<User, NSError> {
return SignalProducer<User, NSError> { observer, disposable in
let url = NSURL(string:"http://jsonplaceholder.typicode.com/users/1")!
let task = NSURLSession.sharedSession()
.dataTaskWithURL(url) { (data, response, error) in
if error != nil {
observer.sendFailed(NSError(domain:"", code:5, userInfo:nil))
} else {
let json = try! NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions())
let user = User(JSON: json)
print("Completed user request at \(NSDate())")
observer.sendNext(user)
observer.sendCompleted()
}
}
print("Started user request at \(NSDate())")
task.resume()
}
}
}
class UserManager {
static let sharedManager = UserManager()
private var requestProducer: Atomic<SignalProducer<User, NSError>?> = Atomic(nil)
func getUsers() -> SignalProducer<User, NSError> {
return requestProducer.modify { producer in
if let producer = producer {
print("There is already a request in progress so simply return a reference to its signal")
return producer
}
return API.getUsers()
.on(completed: {
self.requestProcuder.modifify { _ in nil }
})
.replayLazily()
}
}
}
鈿狅笍 I typed this directly into the browser and it's untested. 鈿狅笍
There still is state, but it's contained a little better.
The RAC-iest way to do it would be to add a new operator that contains the state.
extension SignalProducer {
func replayIfStarted() -> SignalProducer<Value, Error> {
let active: Atomic<SignalProducer<Value, Error>?> = nil
return active.modify { producer in
if let producer = producer {
return producer
}
return self
.on(completed: {
active.modify { _ in nil }
})
.replayLazily()
}
}
}
I'd be sure to double check that against the RAC codebase, and also write some tests around the producer lifetime, to make sure I wrote it properly. 鈽猴笍
Nice! And apologies for the code-barf on my part, I should have prefaced that block with a "this is not prod code" message 馃槃
I'll check this out when I'm in front of Xcode; appreciate all your insight.
I'm going to close this. Feel free to reopen if you have questions after looking at it!
馃憤 thanks Matt
Here's another way to go about this problem: using Property
!
final class UserManager {
/// The public API is as easy as observing this.
public let users: AnyProperty<[User]>
private let usersMutableProperty = MutableProperty<[User]>([])
init() {
self.users = AnyProperty(self.usersMutableProperty)
}
private func getUsers() -> SignalProducer<User, NSError> { }
public func requestUsers() {
/// This is not idea because multiple calls to this method would step onto one-another, just doing it like this for simplicity
self.usersMutableProperty <~ self.usersRequest()
}
}