Wordpress REST API + Swift - ios

I'm a novice developer trying to write a simple app that presents posts from a Wordpress site as a feed. I'm using the Wordpress REST API and consuming that within swift. I'm getting stuck at parsing the JSON and presenting it in swift.
Detail below, but how do I code the dual identifier of 'title' + 'rendered' from the REST API?
So far I've got this in swift:
import SwiftUI
struct Post: Codable, Identifiable {
let id = UUID()
var title.rendered: String
var content.rendered: String
}
class Api {
func getPosts(completion: #escaping ([Post]) -> ()) {
guard let url = URL(string: "https://councillorzamprogno.info/wp-json/wp/v2/posts") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else { return }
let posts = try! JSONDecoder().decode([Post].self, from: data)
DispatchQueue.main.async {
completion(posts)
}
}
.resume()
}
but the "var title.rendered: String" isn't accepted by Xcode, I get the error "Consecutive declarations on a line must be seperated by ';'. So how should I go about getting the post tile, content etc. when it appears like this in the REST API:
{
id: 1216,
date: "2020-11-18T00:51:37",
date_gmt: "2020-11-17T13:51:37",
guid: {
rendered: "https://councillorzamprogno.info/?p=1216"
},
modified: "2020-11-18T01:31:52",
modified_gmt: "2020-11-17T14:31:52",
slug: "the-nsw-2020-state-redistribution",
status: "publish",
type: "post",
link: "https://councillorzamprogno.info/2020/11/18/the-nsw-2020-state-redistribution/",
title: {
rendered: "The NSW 2020 State Redistribution"
},
content: {
rendered: " <figure class="wp-block-embed is-type-video is-provider-youtube
(etc.)

Create another Codable type as below and update Post,
struct Rendered: Codable {
var rendered: String
}
struct Post: Codable, Identifiable {
let id = UUID()
var title: Rendered
var content: Rendered
}

Related

SwiftUI design pattern - How to make a "2nd-stage" API call from the result of the first one

This is on iOS 15.5 using the latest SwiftUI standards.
I have these two structs in my SwiftUI application:
User.swift
struct User: Codable, Identifiable, Hashable {
let id: String
let name: String
var socialID: String? // it's a var so I can modify it later
func getSocialID() async -> String {
// calls another API to get the socialID using the user's id
// code omitted
// example response:
// {
// id: "aaaa",
// name: "User1",
// social_id: "user_1_social_id",
// }
}
}
Video.swift
struct Video: Codable, Identifiable, Hashable {
let id: String
let title: String
var uploadUser: User
}
My SwiftUI application displays a list of videos, the list of videos are obtained from an API (which I have no control over), the response looks like this:
[
{
id: "AAAA",
title: "My first video. ",
uploaded_user: { id: "aaaa", name: "User1" },
},
{
id: "BBBB",
title: "My second video. ",
uploaded_user: { id: "aaaa", name: "User1" },
},
]
My video's view model looks like this:
VideoViewModel.swift
#MainActor
class VideoViewModel: ObservableObject {
#Published var videoList: [Video]
func getVideos() async {
// code simplified
let (data, _) = try await URLSession.shared.data(for: videoApiRequest)
let decoder = getVideoJSONDecoder()
let responseResult: [Video] = try decoder.decode([Video].self, from: data)
self.videoList = responseResult
}
func getSocialIDForAll() async throws -> [String: String?] {
var socialList: [String: String?] = [:]
try await withThrowingTaskGroup(of: (String, String?).self) { group in
for video in self.videoList {
group.addTask {
return (video.id, try await video.uploadedUser.getSocialId())
}
}
for try await (userId, socialId) in group {
socialList[userId] = socialId
}
}
return socialList
}
}
Now, I wish to fill in the socialID field for the User struct, which I must obtain from another API using each user's ID. the response looks like this for each user:
{
id: "aaaa",
name: "User1",
social_id: "user_1_social_id",
}
Right now the only viable way to get the information seems to be using withThrowingTaskGroup() and call getSocialID() for each user, which I am using right now, then I can return a dictionary that contains all the socialID information for each user, then the dictionary can be used in SwiftUI views.
But, is there a way for me to fill in the socialID field in the User struct without having to use a separate dictionary? It doesn't seem like I can modify the User struct in each Video inside videoList once the JSON decoder initializes the list of videos, due to the fact that VideoViewModel is a MainActor. I would prefer to have everything downloaded in one go, so that when the user enters a subview, there is no loading time.
You are correct that you can't modify the structs once they are initialized, because all of their properties are let variables; however, you can modify the videoList in VideoViewModel, allowing you to dispense with the Dictionary.
#MainActor
class VideoViewModel: ObservableObject {
#Published var videoList: [Video]
func getVideos() async {
// code simplified
let (data, _) = try await URLSession.shared.data(for: videoApiRequest)
let decoder = getVideoJSONDecoder()
let responseResult: [Video] = try decoder.decode([Video].self, from: data)
self.videoList = try await Self.getSocialIDForAll(in: responseResult)
}
private static func updatedWithSocialID(_ user: User) async throws -> User {
return User(id: user.id, name: user.name, socialID: try await user.getSocialID())
}
private static func updatedWithSocialID(_ video: Video) async throws -> Video {
return Video(id: video.id, title: video.title, uploadUser: try await updatedWithSocialID(video.uploadUser))
}
static func getSocialIDForAll(in videoList: [Video]) async throws -> [Video] {
return try await withThrowingTaskGroup(of: Video.self) { group in
videoList.forEach { video in
group.addTask {
return try await self.updatedWithSocialID(video)
}
}
var newVideos: [Video] = []
newVideos.reserveCapacity(videoList.count)
for try await video in group {
newVideos.append(video)
}
return newVideos
}
}
}
Using a view model object is not standard for SwiftUI, it's more of a UIKit design pattern but actually using built-in child view controllers was better. SwiftUI is designed around using value types to prevent the consistency errors typical for objects so if you use objects then you will still get those problems. The View struct is designed to be the primary encapsulation mechanism so you'll have more success using the View struct and its property wrappers.
So to solve your use case, you can use the #State property wrapper, which gives the View struct (which has value semantics) reference type semantics like an object would, use this to hold the data that has a lifetime matching the View on screen. For the download, you can use async/await via the task(id:) modifier. This will run the task when the view appears and cancel and restart it when the id param changes. Using these 2 features together you can do:
#State var socialID
.task(id: videoID) { newVideoID in
socialID = await Social.getSocialID(videoID: newViewID)
}
The parent View should have a task that got the video infos.

How do i call the parsed data from the GET request that have a longer nested JSON structure in Swift?

NOTE: Forgive my cluelessness, i am still new in regards to this. The full code is posted at the bottom.
ISSUE: It seems that when i have a short nest, i am able to call it for my #Published property however when i try an api request with a longer nest, like this. and type Decodable structs that follows the structure of the GET request
struct TripScheduleTest: Codable {
let TripList: InitialNest
}
struct InitialNest: Codable {
var Trip: [TravelDetail]
}
struct TravelDetail: Codable {
var Leg: [TripTest]
}
struct TripTest: Codable, Hashable {
var name: String
var type: String
}
I am not able to call it for the #Published var dataSet1 = [TripTest]()
self.dataSet1 = tripJSON.TripList.Trip.Leg
I get an error message, that says "Value of type '[TravelDetail]' has no member 'Leg'
I am not sure why, however it works when i use [TravelDetail]() instead of [TripTest]() in the #Published var and stop at Trip before Leg for the dataSet1, then it seems to at least build successfully. But now i am not able to get the name and type information from the request
Full code
import SwiftUI
struct TripScheduleTest: Codable {
let TripList: InitialNest
}
struct InitialNest: Codable {
var Trip: [TravelDetail]
}
struct TravelDetail: Codable {
var Leg: [TripTest]
}
struct TripTest: Codable, Hashable {
var name: String
var type: String
}
class TripViewModel: ObservableObject {
#Published var dataSet1 = [TripTest]()
init() {
let urlString = "http://xmlopen.rejseplanen.dk/bin/rest.exe/trip?originId=8600790&destId=6553&format=json"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, resp, err) in
guard let data = data else { return }
do {
let tripJSON = try
JSONDecoder().decode(TripScheduleTest.self, from: data)
print(data)
DispatchQueue.main.async {
self.dataSet1 = tripJSON.TripList.Trip.Leg
}
} catch {
print("JSON Decode error: ", error)
}
}.resume()
}
}
struct TripView: View {
#ObservedObject var vm = TripViewModel()
var body: some View {
List(vm.dataSet1, id: \.self) { day in
Text("Test")
.font(.system(size: 12, weight: .bold))
Text(" \(day.name)")
.font(.system(size: 12))
}
}
}
Trip is an array (note the [])
You need to get one item of the array by index for example
tripJSON.TripList.Trip.first?.Leg
To assign the value to a non-optional array write
self.dataSet1 = tripJSON.TripList.Trip.first?.Leg ?? []

How to generate fixed number of views from json API in SwiftUI?

So I am learning how to use SwiftUI with a json api. I am currently generating views through a list and a ForEach Loop. I am wondering how I can make it so that it only generates the first, say 10 elements in the posts array, instead of generating the entire list from the api. Basically I want to use RandomElement() to display 10 random posts from the entire array. I am just beginning here and learning so any help woul dbe appreicated.
Below is the code for my main view that is displaying the list
import SwiftUI
struct postList: View {
//state variable of the posts
#State var posts: [Post] = []
var array = [Post]()
var body: some View {
List {
ForEach(posts) { post in
VStack(alignment: .leading) {
Text(post.title)
.font(.headline)
Text(post.body)
.font(.callout)
}
.padding()
}
}
.onAppear() {
Api().getPosts { (posts) in
self.posts = posts
}
}
}
}
struct postList_Previews: PreviewProvider {
static var previews: some View {
postList()
}
}
Down here is the Data file I use to retrieve the json data
import SwiftUI
struct Post: Codable, Identifiable {
let id = UUID()
var title: String
var body: String
}
class Api {
func getPosts(completion: #escaping ([Post]) -> ()) {
guard let url = URL(string: "http://jsonplaceholder.typicode.com/posts") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let posts = try! JSONDecoder().decode([Post].self, from: data!)
DispatchQueue.main.async {
completion(posts)
}
}
.resume()
}
}

Failing with the decode of JSON

I am trying to decode a JSON response from the youtube API in swift.
The JSON information is:
I made a Decodable structure:
// Build a model object to import the JSON data.
struct PlaylistInformation: Decodable {
struct Items: Decodable {
struct VideoNumber: Decodable {
struct Snippet: Decodable {
let title: String
}
let snippet: Snippet
}
let videoNumber: VideoNumber
}
let items: Items
}
And I get the error when trying to decode:
// We decode the JSON data get from the url according to the structure we declared above.
guard let playlistInformation = try? JSONDecoder().decode(PlaylistInformation.self, from: data!) else {
print("Error: could not decode data into struct") <-- HERE IS THE ERROR
return
}
// Comparing DB Versions.
let videoTitle = playlistInformation.items.videoNumber.snippet.title as NSString
print(videoTitle)
The error I get is:
Error: could not decode data into struct
I guess it has something to do with the "items" in the struct, as it is an array... but I have no idea about how to solve that.
Given that items is an array, you have to model it as an array and not a struct:
// Build a model object to import the JSON data.
struct PlaylistInformation: Decodable {
struct Item: Decodable {
struct Snippet: Decodable {
let title: String
}
let snippet: Snippet
}
let items: [Item]
}
And then access each item using its index, e.g.
let videoTitle = playlistInformation.items[0].snippet.title as NSString
print(videoTitle)
Yes, the error was coming from the "items" in the struct as it is an array.
The correct Decodable struct is:
struct PlaylistInformation: Decodable {
struct Items: Decodable {
struct Snippet: Decodable {
struct Thumbnails: Decodable {
struct High: Decodable {
let url: String
}
let high: High
}
struct ResourceId: Decodable {
let videoId: String
}
let publishedAt: String
let title: String
let thumbnails: Thumbnails
let resourceId: ResourceId
}
let snippet: Snippet
}
let items: [Items]
}
Thank you for your help.

Swift Wordpress Posts Fetch Page

I'm a beginner for programming and swift. I'm developing an app for my small business. It's almost finished but i'm completely stucked with blog page. I just want to fetch data from my website which is wordpress, and put my posts in to my tableview. I was searching but couldn't find a proper answer. Please at least me tell me where to begin with
Thank you
You need to use awesome new API of WordPress called WP REST API, which will give you all data you need in JSON format. Have a look at this url to get started:
http://v2.wp-api.org/
Use this official plugin in your WordPress site:
https://wordpress.org/plugins/rest-api/
Update: You do not need to install above plugin as it has been merged into WordPress core from v4.4 to onwards.
At first, you should define models for parsing the JSON response, for example, as below.
import Foundation
struct Title: Decodable {
let rendered: String
}
struct Content: Decodable {
let rendered: String
}
struct WPFeaturedMedia: Decodable {
let sourceURLString: String
var url: URL? {
.init(string: sourceURLString)
}
enum CodingKeys: String, CodingKey {
case sourceURLString = "source_url"
}
}
struct Embedded: Decodable {
let medias: [WPFeaturedMedia]?
enum CodingKeys: String, CodingKey {
case medias = "wp:featuredmedia"
}
}
struct PostHeader: Decodable, Identifiable {
let id: Int
let date: Date
let title: Title
let embedded: Embedded
enum CodingKeys: String, CodingKey {
case date = "date"
case title = "title"
case embedded = "_embedded"
case id = "id"
}
}
struct Post: Decodable, Identifiable {
let id: Int
let date: Date
let title: Title
let content: Content
let embedded: Embedded
enum CodingKeys: String, CodingKey {
case date = "date"
case title = "title"
case content = "content"
case embedded = "_embedded"
case id = "id"
}
}
You can easily fetch posts using Combine.
import Combine
import SwiftUI
final class ViewModel: ObservableObject {
#Published var postHeaders: [PostHeader] = []
#Published var error: Error?
private var cancellable: AnyCancellable?
init() {
let url = URL(string: "https://PUT_YOUR_HOST_HERE/wp-json/wp/v2/posts?_fields=id,date,title,_links,_embedded&_embed”)!
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [PostHeader].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
self.error = error
case .finished:
break
}
},
receiveValue: { posts in
self.postHeaders = posts
}
)
}
}
Full sources you can find here: https://github.com/fuzzzlove/SwiftUIWordpressClient

Resources