Is there any way to map user info with their tweets without too many fetching? - swift

I am now trying to create a simple Twitter client using SwiftUI, and currently at the point where I try to fetch my timeline.
Reading the official Twitter API v2 document, there is a way to fetch timeline, but it only offers tweet ID, text, created_at, and author Id.
so I tried to fetch author info using the author Id in loop, which causes too many fetching data and many of them are redundant because a single user tweet many times.
This way makes my account easily reach the api limit, but I am struggling to find better ways.
I am not an enginner and write codes just for fun, so I am not really familiar with techniques enginners should know... I would appreciate if you give me even a hint.
Here is my code below.
func getStream(client: TwitterAPIClient, id: String) async throws {
let response = await client.v2.timeline.getUserReverseChronological(.init(
id: id,
expansions: TwitterTweetExpansionsV2.all, maxResults: 20,
mediaFields: TwitterMediaFieldsV2.all,
tweetFields: TwitterTweetFieldsV2.all.subtracting([.promotedMetrics, .nonPublicMetrics, .organicMetrics]),
userFields: TwitterUserFieldsV2.all)
)
.responseDecodable(type: TwitterDataResponseV2<
[TwitterTimelineV2],
TwitterTimelineV2.Include,
TwitterTimelineV2.Meta>.self)
if let error = response.error {
print(error)
throw error
} else {
print(response.success!)
}
print(response)
let timeline = try response.map { $0.data }.result.get()
for tweet in timeline {
let response = try await getUserInfo(client: client, id: tweet.authorId).result.get()
print(response)
}
}
Note: TwitterAPIClinet is a client of a third party library.

Instead of fetching user data for each individual tweet, you could first loop the timeline array and store each user once in a dictionary. Then, you can request the user info based on that dictionary.
let timeline = try response.map { $0.data }.result.get()
var usersDictionary = [String : Bool]()
for tweet in timeline {
usersDictionary[tweet.authorId] = true
}
for authorId in usersDictionary.keys {
let response = try await getUserInfo(client: client, id: authorId).result.get()
print(response)
}

Related

How to observe multiple Firebase entries in a loop and run completion block once every observe function has finished?

I'm trying to create a feed that should include posts made by people a user follows, and I'm using following function to get posts for user feed:
func observePosts(for userID: String, completion: #escaping ([UserPost], String) -> Void) {
let databaseKey = "Posts/\(userID)/"
databaseRef.child(databaseKey).queryOrderedByKey().queryLimited(toLast: 12).observe(.value) { snapshot in
var retrievedPosts = [UserPost]()
var lastSeenPostKey = ""
let dispatchGroup = DispatchGroup()
for snapshotChild in snapshot.children.reversed() {
guard let postSnapshot = snapshotChild as? DataSnapshot,
let postDictionary = postSnapshot.value as? [String: Any]
else {
print("Couldn't cast snapshot as DataSnapshot")
return
}
dispatchGroup.enter()
lastSeenPostKey = postSnapshot.key
do {
let jsonData = try JSONSerialization.data(withJSONObject: postDictionary as Any)
let decodedPost = try JSONDecoder().decode(UserPost.self, from: jsonData)
UserService.shared.observeUser(for: decodedPost.userID) { postAuthor in
decodedPost.author = postAuthor
retrievedPosts.append(decodedPost)
dispatchGroup.leave()
}
} catch {
print("Couldn't create UserPost from postDictionary")
}
}
dispatchGroup.notify(queue: .main) {
completion(retrievedPosts, lastSeenPostKey)
}
}
}
First step I'm getting user feed posts with .observe function and after that I'm iterating through every received post calling a UserService.shared.observeUser method that observes user data for every post to set post author's profile picture and username.
The problem with my method is that I'm using DispatchGroup's .enter and .leave methods to notify once every observe function got user data so that I could call completion block in which I'm reloading tableView.
Sure it works fine first time but since it's observe function it's called every time there's a change to user so once user updates his data (profile pic, usernama etc.) it crashes on dispatchGroup.leave() line since it's not balanced with dispatchGroup.enter()
What would be a correct approach to solve this issue?
I've been trying to solve this problem for 3 days searching all the internet, and only advice I found is to fetch it one time using observeSingleValue() instead of observing, but I really need to observe user profile data to keep profile pics and other necessary data up to date.

Get sorted Firestore documents when denormalizing data

I am using Firestore for my app on which users can publish Posts, stored in the posts collection:
posts
{postID}
content = ...
attachementUrl = ...
authorID = {userID}
Each User will also have a timeline in which the posts of the people they follow will appear. For that I am also keeping a user_timelines collection that gets populated via a Cloud Function:
user_timelines
{userID}
posts
{documentID}
postID = {postID}
addedDate = ...
Because the data is denormalized, if I want to iterate through a user's timeline I need to perform an additional (inner) query to get the complete Post object via its {postID}, like so:
db.collection("user_timelines").document(userID).collection("posts")
.orderBy("addedDate", "desc").limit(100).getDocuments() { (querySnap, _) in
for queryDoc in querySnap.documents {
let postID = queryDoc.data()["postID"] as! String
db.collection("posts").document("postID").getDocument() { (snap, _) in
if let postDoc = snap {
let post = Post(document: postDoc)
posts.append(post)
}
}
}
}
The problem is by doing so I am loosing the order of my collection because we are not guaranteed that all the inner queries will complete in the same order. I need to keep the order of my collection as this will match the order of the timeline.
If I had all the complete Post objects in the timeline collections there would be not issue and the .orderBy("addedDate", "desc").limit(100) would work just fine keeping the Posts sorted, but If I denormalize I cant seem to find a correct solution.
How can I iterate through a user's timeline and make sure to get all the Post objects sorted by addedDate even when denormalizing data?
I was thinking of creating a mapping dictionary postID/addedDate when reading the postIDs, and then sort the Post at the end using this dictionary, but I am thinking there must be a better solution for that?
I was expecting this to be a common issue when denormalizing data, but unfortunately I couldnot find any results. Maybe there's something I am missing here.
Thank you for your help!
What you can do is enumerate the loop where you perform the inner query, which simply numbers each iteration. From there, you could expand the Post model to include this value n and then sort the array by n when you're done.
db.collection("user_timelines").document(userID).collection("posts").orderBy("addedDate", "desc").limit(100).getDocuments() { (querySnap, _) in
for (n, queryDoc) in querySnap.documents.enumerated() {
let postID = queryDoc.data()["postID"] as! String
db.collection("posts").document("postID").getDocument() { (snap, _) in
if let postDoc = snap {
let post = Post(document: postDoc, n: n)
posts.append(post)
}
}
}
posts.sort(by: { $0.n < $1.n })
}
The example above actually won't work because the loop is asynchronous which means the array will sort before all of the downloads have completed. For that, consider using a Dispatch Group to coordinate this task.
db.collection("user_timelines").document(userID).collection("posts").orderBy("addedDate", "desc").limit(100).getDocuments() { (querySnap, _) in
let dispatch = DispatchGroup()
for (n, queryDoc) in querySnap.documents.enumerated() {
dispatch.enter() // enter on each iteration
let postID = queryDoc.data()["postID"] as! String
db.collection("posts").document("postID").getDocument() { (snap, _) in
if let postDoc = snap {
let post = Post(document: postDoc, n: n)
posts.append(post)
}
dispatch.leave() // leave no matter success or failure
}
}
dispatch.notify(queue: .main) { // completion
posts.sort(by: { $0.n < $1.n })
}
}

How to listen to Server Side events for Facebook live comments on IOS

I'm using the new SSE endpoints for Live comments on Live videos. my question is how can I achive this on IOS and what library should I use
I tried using the Event Source for IOS https://github.com/inaka/EventSource
But Even though I tried to use OnMessage and addOnEventsource it dosent seem to work.
Here is my code for listening to the live comments
var live_commentURL = URL.init(string:"https://streaming-graph.facebook.com/\(fbLiveStreamId!)/live_comments?access_token=\(accessToken ?? "")&comment_rate=one_per_two_seconds&fields=from{name,id},message")
let queryItems = [NSURLQueryItem(name: "access_token", value: accessToken!), NSURLQueryItem(name: "comment_rate", value: "one_hundred_per_second"),NSURLQueryItem(name: "fields", value: "from{name,id},message")]
let urlComps = NSURLComponents(string: "https://streaming-graph.facebook.com/\(fbLiveStreamId!)/live_comments")!
// var headders:[String:String] = [:]
// headders["access_token"] = accessToken!
// headders["comment_rate"] = "one_hundred_per_second"
// headders["fields"] = "from{name,id},message"
var eventSource = EventSource.init(url:urlComps.url!)
eventSource.connect()
eventSource.onOpen {
print("Successfully Connected to server")
}
eventSource.addEventListener("user-connected") {(id, event, data) in
print("id:", id)
print("event:" , event)
print("data: ", data)
}
eventSource.onMessage { (id, event, data) in
print("id:", id)
print("event:" , event)
print("data: ", data)
}
print(eventSource.events())
eventSource.onComplete { (code, isErro, error) in
print(code)
}
I have tried to send the access tokesn and other fields in headders too but got no luck with it.
I tried 2 methods
method 1
send accesstoken,comment_rateand fields as headders but I dont think that is the right way.
method 2
Since they are all query params I used NSURLComponents.
It was an issue with my auth tokens My bad

Swift, URLSession, viewDidLoad, tableViewController

I've never really gotten the nuances of async operations so time and again, I get stymied. And I just can't figure it out.
I'm trying to do some very simple web scraping.
My local volleyball association has a page (verbose HTML, not responsive, not mobile-friendly, yaddah, yaddah, yaddah) which shows the refs assigned to each game of the season. I'm trying to write a silly little app which will scrape that page (no API, no direct access to db, etc.) and display the data in a grouped table. The first group will show today's matches (time, home team, away team). The second group will show tomorrow's matches. Third group shows the entire season's matches.
Using code I found elsewhere, my viewDidLoad loads the page, scrapes the data and parses it into an array. Once I've parsed the data, I have three arrays: today, tomorrow, and matches, all are [Match].
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: urlString)!
let request = NSMutableURLRequest(url: url)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if let error = error {
print (error)
} else {
if let unwrappedData = data {
// scrape, scrape, parse, parse
matchRow = ...
self.matches.append(matchRow)
if matchRow.date == todaysDate {
self.today.append(matchRow)
} else if matchRow.date == tomorrowsDate {
self.tomorrow.append(matchRow)
}
}
}
}
task.resume()
}
As I'm sure is no surprise to anyone who understands async operations, my table is empty. I've checked and I see the the data is there and properly parsed, etc. But I can't for the life of me figure out how get the data in my table. The way I have it now, the data is not ready when numberOfSections or numberOfRowsInSection is called.
I've found the Ray Wenderlich tutorial on URLSession and I also have a Udemy course (Rob Percival) that builds an app to get the weather using web scraping, but in both those instances, the app starts and waits for user input before going out to the web to get the data. I want my app to get the data immediately upon launch, without user interaction. But I just can't figure out what changes I need to make so that those examples work with my program.
Help, please.
You can simply reload the tableviews once the data arrays are getting populated from the URLSession completion block. Have you tried that. Sample snippet may be like the one follows.
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if let error = error {
print (error)
} else {
if let unwrappedData = data {
// scrape, scrape, parse, parse
matchRow = ...
self.matches.append(matchRow)
if matchRow.date == todaysDate {
self.today.append(matchRow)
} else if matchRow.date == tomorrowsDate {
self.tomorrow.append(matchRow)
}
}
DispatchQueue.main.async { [weak self] in
self?.todayTableView.reloadData()
self?.tomorrowTableView.reloadData()
}
}
}

Swift dealing with classes, UIButtons and tableView

This might sound like a very stupid question but I am fairly new to swift and cannot think how to go about this. As you can see in this Screenshot I have a search recipes textfield in RecipesViewController where the user enters a food item (which I use in the api call). After the user hits the button I make a call to an api and get data from that api and store that data in instance variable (searchRecipe array) in my RecipesViewController class. Now I am trying to show the data that I received from the api in a table view so I have another class called SearchRecipeTViewController. n this class I want to populate the table with the data I received from the api however when I try to access the searchRecipe array (which stores the elements received from the api) I get a blank value which I understand is due to the instance variable being initialized as "". But now how do I go about this so that I can get data from the api and display it on the table view when the user hits the button. Any suggestions would be appreciated.
Code to call and get data from api when button is clicked
#IBAction func SearchButton(sender: UIButton) {
if let recipe = RecipeSearchBar.text {
searchRecipe = recipe
}
//search recipe API call
endpoint = "http://api.yummly.com/v1/api/recipes? _app_id=apiID&_app_key=apiKey&q=\(searchRecipe)"
Alamofire.request(.GET, endpoint).responseJSON { response in
if response.result.isSuccess {
let data = response.result.value as! NSDictionary
if let matches = data["matches"] as? [[String: AnyObject]] {
for match in matches {
if let name = match["recipeName"] as? String {
self.recipeName.append(name);
}
}
}
}
else if response.result.isFailure {
print("Bad request")
}
}
}
Try using SwiftyJSON to manipulate the JSON the API returns. SwiftyJSON makes API calls that use JSON much easier. Here is the code I used that uses both Alamofire and SwiftyJSON.
//Use alamofire to connect to the web service and use GET on it
Alamofire.request(url).responseJSON { response in
if let error = response.result.error {
print("Error: \(error)")
return
}
//Value is the response the webservice gives, given as a Data Obkect
if let value = response.result.value {
//The information given from the webservice in JSON format
let users = JSON(value)
print("The user information is: \(users)")
//Get each username in the JSON output and print it
for username in users.arrayValue{
print(username["username"].stringValue)
}
}
}
Forgot to add a link to SwiftJSON: https://github.com/SwiftyJSON/SwiftyJSON