How to read formatted text of json file in swiftui 14? - swiftui

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
}

Related

SwiftUI combine nil data

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.

SwiftUI GKLeaderboard loadEntries

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

SwiftUI parse Multilayer JSON Data from URL and build list

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)

Is it possible to set a character limit on a TextField using SwiftUI?

[RESOLVED]
I am using a codable struct which stores the object values retrieved from an API call so I have amended my TextField using Cenk Belgin's example, I've also removed extra bits I've added in so if anyone else is trying to do the same thing then they won't have pieces of code from my app that aren't required.
TextField("Product Code", text: $item.ProdCode)
.onReceive(item.ProdCode.publisher.collect()) {
self.item.ProdCode = String($0.prefix(5))
}
Here is one way, not sure if it was mentioned in the other examples you gave:
#State var text = ""
var body: some View {
TextField("text", text: $text)
.onReceive(text.publisher.collect()) {
self.text = String($0.prefix(5))
}
}
The text.publisher will publish each character as it is typed. Collect them into an array and then just take the prefix.
From iOS 14 you can add onChange modifier to the TextField and use it like so :
TextField("Some Placeholder", text: self.$someValue)
.onChange(of: self.someValue, perform: { value in
if value.count > 10 {
self.someValue = String(value.prefix(10))
}
})
Works fine for me.
You can also do it in the Textfield binding directly:
TextField("Text", text: Binding(get: {item.ProCode}, set: {item.ProCode = $0.prefix(5).trimmingCharacters(in: .whitespacesAndNewlines)}))

How to create tappable url/phone number in SwiftUI

I would like to display a phone number in a SwiftUI Text (or any View), and then make it clickable so that it will open the 'Phone'.
Is there a way to do this with SwiftUI, or should I try to wrap a UITextView in SwiftUI and do it the old-fashioned way with NSAttributed string etc?
I've read the documentation for Text in SwiftUI, and couldn't find anything about how to do this. Currently trying to do this in Xcode 11 beta 5.
I've searched 'text' in the SwiftUI API in SwiftUI.h
I've also searched stackoverflow [swiftui] and google with queries like "make phone number/url tappable", "Tappable link/url swiftUI" etc..
Text("123-456-7890")
.onTapGesture {
// do something here
}
(text will be Japanese phone number)
Using iOS 14 / Xcode 12.0 beta 5
Use new link feature in SwiftUI for phone and email links.
// Link that will open Safari on iOS devices
Link("Apple", destination: URL(string: "https://www.apple.com")!)
// Clickable telphone number
Link("(800)555-1212", destination: URL(string: "tel:8005551212")!)
// Clickable Email Address
Link("apple#me.com", destination: URL(string: "mailto:apple#me.com")!)
Try this,
let strNumber = "123-456-7890"
Button(action: {
let tel = "tel://"
let formattedString = tel + strNumber
guard let url = URL(string: formattedString) else { return }
UIApplication.shared.open(url)
}) {
Text("123-456-7890")
}
Thanks to Ashish's answer, I found the necessary code I needed to solve this:
In the action inside of the Button - you need to call this method:
UIApplication.shared.open(url)
to actually make the phone call / open a link in a SwiftUI View.
Of course, I didn't understand how to format my phone number at first, which I found in these answers:
How to use openURL for making a phone call in Swift?
Don't forget to add the 'tel://' to the beginning of your string/format it as URL..
The full code of what worked is
Button(action: {
// validation of phone number not included
let dash = CharacterSet(charactersIn: "-")
let cleanString =
hotel.phoneNumber!.trimmingCharacters(in: dash)
let tel = "tel://"
var formattedString = tel + cleanString
let url: NSURL = URL(string: formattedString)! as NSURL
UIApplication.shared.open(url as URL)
}) {
Text(verbatim: hotel.phoneNumber!)
}
KISS answer:
Button("url") {UIApplication.shared.open(URL(string: "https://google.com")!)}
From iOS 14, Apple provides us a Link view by default. So, you can just use this,
Link("Anyone can learn Swift", destination: URL(string: "https://ohmyswift.com")!)
For the previous versions of iOS, like iOS 13.0, you still have to use
Button("Anyone can learn Swift") {
UIApplication.shared.open(URL(string: "https://ohmyswift.com")!)
}
iOS 15 (beta)
Take advantage of Markdown in SwiftUI, which supports links!
struct ContentView: View {
var body: some View {
Text("Call [123-456-7890](tel:1234567890)")
}
}
Result:
Make it a Button() with an action, not a Text() with a gesture.
Button {
var cleanNum = selectedItem.phoneNum
let charsToRemove: Set<Character> = [" ", "(", ")", "-"] // "+" can stay
cleanNum.removeAll(where: { charsToRemove.contains($0) })
guard let phoneURL = URL(string: "tel://\(cleanNum)") else { return }
UIApplication.shared.open(phoneURL, options: [:], completionHandler: nil)
} label: {
// ...
}
Using iOS 14 / Xcode 12.5
I add this in case you wants to use a view.
To make a call by tapping a View
Link(destination: URL(string: "tel:1234567890")!, label: {
SomeView()
})
To make a call by tapping a String
Link("1234567890", destination: URL(string: "tel:1234567890")!)
You can do this way as well if in case you need to log any event on tap of the link :
struct exampleView: View {
#SwiftUI.Environment(\.openURL) private var openURL
var body: some View {
Button {
if let url = URL(string: "https://www.google.com") {
openURL(url)
}
} label: {
Text("Link")
.foregroundColor(Color("EyrusDarkBlue"))
}
}
}