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—then 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()
}
}