First an information: I am actually on the way learning swiftUI, I'm a total newbie. For my first project i decided to create a small app that loads articles from a joomla website. My API will respond to a query in the following structure:
{
"source": "my AppConnector for Joomla!",
"offset": 0,
"count": 0,
"results": [
{
"id": "8",
"title": "Article 1",
...
},
{
"id": "8",
"title": "Article 2",
...
}
]
}
In the future the API will return more complex structures but actually i'm struggling already with that one. All swiftUI examples & videos i've found are just explaining how to retreive an array of items or are to old and shows depreacet code examples (with the one-dimensional examples i have already successfully created a list view of items but that's not what i want).
I've created the following structs:
struct Welcome: Codable {
let source: String
let offset, count: Int
let results: [Result]
}
// MARK: - Result
struct Result: Codable {
let id, title, alias, introtext: String
let fulltext, state, catid, created: String
let createdBy, createdByAlias, modified, modifiedBy: String
let checkedOut, checkedOutTime, publishUp, publishDown: String
let images, urls, attribs, version: String
let ordering, metakey, metadesc, access: String
let hits, metadata, featured, language: String
let xreference, note, slug, articleID: String
let introImage, fullImage: String
}
and the following fetcher:
import Foundation
import SwiftUI
import Combine
public class ArticlesFetcher: ObservableObject {
#Published var articles = [Welcome]()
init(){
load()
}
func load() {
let url = URL(string: "https://nx-productions.ch/index.php/news")! //This is a public demo url feel free to check the jsondata (SecurityToken temporary disabled)
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([Welcome].self, from: d)
DispatchQueue.main.async {
self.articles = decodedLists
}
}else {
print("No Data")
}
} catch {
print ("Error")
}
}.resume()
}
}
My view looks like this:
struct ContentView: View {
#ObservedObject var fetcher = ArticlesFetcher()
var body: some View {
VStack {
List(fetcher.articles.results) { article in
VStack (alignment: .leading) {
Text(article.title)
Text(article.articleId)
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
What i don't understand is the view part - i am not able to point into the fields, with the example above i get compiler errors like "Value of type '[Welcome]' has no member 'results'" or "Value of type 'Int' has no member 'title'"
I think i may just not understand something aboutmy structure or how to loop through it.
Thanks for any advise.
The JSON starts with a { so it's a dictionary. And the type of articles is wrong.
Replace
#Published var articles = [Welcome]()
with
#Published var articles = [Result]()
and replace
let decodedLists = try JSONDecoder().decode([Welcome].self, from: d)
DispatchQueue.main.async {
self.articles = decodedLists
}
with
let decodedLists = try JSONDecoder().decode(Welcome.self, from: d)
DispatchQueue.main.async {
self.articles = decodedLists.results
}
Finally but not related replace meaningless
print ("Error")
with
print(error)
Related
I trying to run a function with parameter from a switch statement in swiftui but kept getting the "Type '()' cannot conform to 'View'" error. I think the switch statement and the function should be correct. No matter how I play around with the case statement, I'll still get the same error message.
struct questionsData: Codable {
enum CodingKeys: CodingKey {
case question
case answers
case correctAnswerIndex
}
//var id = UUID()
var question: String
var answers = [String]()
var correctAnswerIndex: Int
}
struct ThemeView: View {
var quizzes = [questionsData]()
let themeName: String
var body: some View {
let themeselected: String = themeName
var jsonfile: String = ""
switch themeselected {
case "Money Accepted":
jsonfile = "Accounts"
return loadQuizData(jsonname: jsonfile)
case "Computers":
jsonfile = "Computers"
return loadQuizData(jsonname: jsonfile)
default:
Text("invalid")
}
}
func loadQuizData(jsonname: String){
guard let url = Bundle.main.url(forResource: jsonname, withExtension: "json")
else {
print("Json file not found")
return
}
let data = try? Data(contentsOf: url)
var quizzes = try? JSONDecoder().decode([questionsData].self, from: data!)
quizzes = quizzes!
}
}
struct ContentView: View {
#State private var selection: String?
let quizList = ["Money Accepted","Computers","Making an appointment", "Late again", "Shopping", "Renting a place", "Accounts", "Letter Writing", "Planning a business", "Business Expression 1", "Business Expression 2", "How to ask the way"]
var body: some View {
NavigationView{
List(quizList, id:\.self) { quizList in
NavigationLink(destination: ThemeView(themeName: quizList)){
Text(quizList)
}
}
.navigationTitle("Select quiz theme")
}
}
}
Please kindly assist... still new to swiftui.
Greatly appreaciated.
I don't get exactly how your ThemeView should look like, maybe you can show us a preview. But there are some mistakes in there. Firstly, try to extract that logic in a ViewModel. Basically have a separate layer to handle business logic such as json parsing and other stuff. Secondly, try not to have var in views, unless they are marked as #State, #Binding, #ObservedObject... . Not least, SwiftUI views should create views not handle logic such as ViewControllers in UIKit, your switch create no view this is why it does not conform to View.
I have a json file that includes html styled text:
I need to maintain the included text formatting: bold, italic, underline, etc.
I am using SwiftUI and Xcode 14. I read the json into a SwiftUI list, then navigate to a detail page that shows the description text. I can't find any examples of how to maintain the formatted text that is in the json file. I've googled, looked at Apple Developer documentation and more, but to no avail. Can anyone help me out with this. My application depends on properly formatted text. It seems odd that Apple wouldn't include something. This is easy in html and javascript. What am I missing? I am not a pro programmer and just starting out with SwiftUI. TIA
[
{ "imageurl": "1.png",
"levelLongDesc":"A longer description",
"id": "1.",
"name": "A name)",
"page": "Details",
"description":"<p><b>E pluribus unum</b></p><b>Instructions.</b> Latin for “Out of many one”, is a motto requested by <i>Pierre Eugene du Simitiere</i> (originally Pierre-Eugène Ducimetière) and found in 1776 on the Seal of the United States, along with Annuit cœptis and Novus ordo seclorum, and adopted by an Act of Congress in 1782.</p><p>",
"videoDemo":"myvideo"
}...]
import SwiftUI
struct LandmarkDetail: View {
var landmark: Landmark
#State var Description = AttributedString("")
var body: some View {
NavigationView{
ScrollView {
RectImage(image: landmark.image)
.padding(.top, 30)
.font(.subheadline)
.foregroundColor(.secondary)
Divider()
Text("\(landmark.name)")
Text(landmark.description)
.font(.title3)
.padding(.horizontal, 20)
.onAppear {
(addStyling(landmark.description))
}
} .padding()
}.navigationTitle(landmark.name)
.navigationBarTitleDisplayMode(.inline)
}
private func addStyling(_ htmlString: String) -> NSAttributedString {
var resultString = NSAttributedString()
var data = Data()
// Add the html data/ var in which you stored it
data.append(Data(landmark.description.utf8))
// Convert the attributed String
do {
let attributedString = try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
resultString = attributedString
} catch {
print(error)
}
return resultString
}
}
Above is what my detail view looks like. I don't know where/how to add the attributes. I get errors with everything that I try. Again, thank you to the two of you who tried to help. -R
Assuming your "description" property of your decoded object model, is some simple html text,
you could use AttributedString and NSAttributedString as shown
in this example code:
struct ContentView: View {
#State var description = AttributedString("")
var body: some View {
Text(description)
.onAppear {
let txt = """
<p><b>E pluribus unum</b></p><b>Instructions.</b> Latin for “Out of many one”, is a motto requested by <i>Pierre Eugene du Simitiere</i> (originally Pierre-Eugène Ducimetière) and found in 1776 on the Seal of the United States, along with Annuit cœptis and Novus ordo seclorum, and adopted by an Act of Congress in 1782.</p><p>
"""
description = asAttribTxt(txt)
}
}
func asAttribTxt(_ txt: String) -> AttributedString {
let data = txt.data(using: .utf16)! // <-- adjust to your needs
do {
let nsString = try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
return AttributedString(nsString)
} catch {
print(error)
}
return AttributedString(txt)
}
}
private func addStyling(_ htmlString: String) -> NSAttributedString {
var resultString = NSAttributedString()
var data = Data()
// Add the html data/ var in which you stored it
data.append(Data(htmlString.utf8))
// Convert the attributed String
do {
let attributedString = try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
resultString = attributedString
} catch {
print(error)
}
return resultString
}
I have created a class to perform a network request and parse the data using Combine. I'm not entirely certain the code is correct, but it's working as of now (still learning the basics of Swift and basic networking tasks). My Widget has the correct data and is works until the data becomes nil. Unsure how to check if the data from my first publisher in my SwiftUI View is nil, the data seems to be valid even when there's no games showing.
My SwiftUI View
struct SimpleEntry: TimelineEntry {
let date: Date
public var model: CombineData?
let configuration: ConfigurationIntent
}
struct Some_WidgetEntryView : View {
var entry: Provider.Entry
#Environment(\.widgetFamily) var widgetFamily
var body: some View {
VStack (spacing: 0){
if entry.model?.schedule?.dates.first?.games == nil {
Text("No games Scheduled")
} else {
Text("Game is scheduled")
}
}
}
}
Combine
import Foundation
import WidgetKit
import Combine
// MARK: - Combine Attempt
class CombineData {
var schedule: Schedule?
var live: Live?
private var cancellables = Set<AnyCancellable>()
func fetchSchedule(_ teamID: Int, _ completion: #escaping (Live) -> Void) {
let url = URL(string: "https://statsapi.web.nhl.com/api/v1/schedule?teamId=\(teamID)")!
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Schedule.self, decoder: JSONDecoder())
//.catch { _ in Empty<Schedule, Error>() }
//.replaceError(with: Schedule(dates: []))
let publisher2 = publisher
.flatMap {
return self.fetchLiveFeed($0.dates.first?.games.first?.link ?? "")
}
Publishers.Zip(publisher, publisher2)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {_ in
}, receiveValue: { schedule, live in
self.schedule = schedule
self.live = live
completion(self.live!)
WidgetCenter.shared.reloadTimelines(ofKind: "NHL_Widget")
}).store(in: &cancellables)
}
func fetchLiveFeed(_ link: String) -> AnyPublisher<Live, Error /*Never if .catch error */> {
let url = URL(string: "https://statsapi.web.nhl.com\(link)")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Live.self, decoder: JSONDecoder())
//.catch { _ in Empty<Live, Never>() }
.eraseToAnyPublisher()
}
}
Like I said in the comments, it's likely that the decode(type: Live.self, decoder: JSONDecoder()) returns an error because the URL that you're fetching from when link is nil doesn't return anything that can be decoded as Live.self.
So you need to handle that case somehow. For example, you can handle this by making the Live variable an optional, and returning nil when link is empty (or nil).
This is just to set you in the right direction - you'll need to work out the exact code yourself.
let publisher2 = publisher1
.flatMap {
self.fetchLiveFeed($0.dates.first?.games.first?.link ?? "")
.map { $0 as Live? } // convert to an optional
.replaceError(with: nil)
}
Then in the sink, handle the nil:
.sink(receiveCompletion: {_ in }, receiveValue:
{ schedule, live in
if let live = live {
// normal treatment
self.schedule = schedule
self.live = live
//.. etc
} else {
// set a placeholder
}
})
SwiftUI and WidgetKit work differently. I needed to fetch data in getTimeline for my IntentTimelineProvider then add a completion handler for my TimelineEntry. Heavily modified my Combine data model. All credit goes to #EmilioPelaez for pointing me in the right direction, answer here.
I would like to add leaderboards to my SwiftUI app.
I can't find any examples of using loadEntries to load leaderboard values.
I tried the following...
let leaderBoard: GKLeaderboard = GKLeaderboard()
leaderBoard.identifier = "YOUR_LEADERBOARD_ID_HERE"
leaderBoard.timeScope = .allTime
leaderBoard.loadScores { (scores, error) in ...
This results in the following warnings:
'identifier' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'timeScope' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'loadScores(completionHandler:)' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler:.
using loadEntriesForPlayerScope results in the following warning:
'loadEntriesForPlayerScope(_:timeScope:range:completionHandler:)' has
been renamed to 'loadEntries(for:timeScope:range:completionHandler:)'
Using loadEntries I don't know how to specify the leaderboard identifier.
Here is simple demo of possible approach - put everything in view model and load scores on view appear.
import GameKit
class BoardModel: ObservableObject {
private var board: GKLeaderboard?
#Published var localPlayerScore: GKLeaderboard.Entry?
#Published var topScores: [GKLeaderboard.Entry]?
func load() {
if nil == board {
GKLeaderboard.loadLeaderboards(IDs: ["YOUR_LEADERBOARD_ID_HERE"]) { [weak self] (boards, error) in
self?.board = boards?.first
self?.updateScores()
}
} else {
self.updateScores()
}
}
func updateScores() {
board?.loadEntries(for: .global, timeScope: .allTime, range: NSRange(location: 1, length: 10),
completionHandler: { [weak self] (local, entries, count, error) in
DispatchQueue.main.async {
self?.localPlayerScore = local
self?.topScores = entries
}
})
}
}
struct DemoGameboardview: View {
#StateObject var vm = BoardModel()
var body: some View {
List {
ForEach(vm.topScores ?? [], id: \.self) { item in
HStack {
Text(item.player.displayName)
Spacer()
Text(item.formattedScore)
}
}
}
.onAppear {
vm.load()
}
}
}
I might be stating the obvious but have you looked at the WWDC20 videos?
Usually when there are big changes like this they cover it during WWDC that year.
Tap into Game Center: Leaderboards, Achievements, and Multiplayer
Tap into Game Center: Dashboard, Access Point, and Profile
I haven't looked at the videos but the documentation eludes that identifier might be replaced by var baseLeaderboardID: String
Im trying to fetch data from a url once ive pressed a button and called for the function but once the function is called i keep getting a typeMismatch error.
This is my code:
struct User: Decodable {
var symbol: String
var price: Double
}
struct Response: Decodable {
var results:[User]
}
struct ContentView: View {
var body: some View {
VStack {
Text("hello")
Button(action: {
self.fetchUsers(amount: 0)
}) {
Text("Button")
}
}
}
func fetchUsers(amount: Int) {
let url:URL = URL(string: "https://api.binance.com/api/v3/ticker/price")!
URLSession.shared.dataTask(with: url) { (data, res, err) in
if let err = err { print(err) }
guard let data = data else { return }
do {
let response = try JSONDecoder().decode(Response.self, from: data)
print(response.results[0])
} catch let err {
print(err)
}
}.resume()
}
}
This is the error:
typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))
The website url where im trying to fetch data from is:
https://api.binance.com/api/v3/ticker/price
Im trying to fetch a specific price from a specific symbol for example the price of ETHBTC, which would be 0.019...
Thank you
There are two mistake in this approach. First of all, if you created a Response struct with
results = [User]
this way you expect the json to be in the form of [result: {}] but you have [{}] format without a name at the beginging. So you should replace the response struct with
typealias Response = [User]
Second of all the API you are using is returning string instead of double as a price, so you should modify your struct to this:
struct User: Decodable {
var symbol: String
var price: String
}
This way it worked for me. Tested under
swift 5
xcode 11.3.1
iOS 13.3.1 non beta