Swift - recognize a link and make it clickable - swift

I have an app that loads blog posts from a Wordpress site.
Now loading the text, images and video's work fine.
In the blog posts from Wordpress are links. Like for example: https://eenvandaag.avrotros.nl/item/ondanks-droogte-op-de-veluwe-onttrekt-kartonfabriek-parenco-er-jaarlijks-miljarden-liters-grondwater/
When the app loads the post, the link is displayed in plain text.
Is there a way to recognise the links and make these "links" clickable and guide the user to safari?
I've been searching the internet for hours but couldn't find a fitting solution.
Every post has a different link. So the goal is that the link is recognised and made clickable.
I found some examples, for example:
var body: some View {
VStack {
Text(attributedString)
Text("Check out [this link](https://stack.com)")
}
}
var attributedString: AttributedString {
var attributedText = AttributedString("Visit website")
attributedText.link = URL(string: "https://stack.com")
return attributedText
}
But then you have to set the link.
So that doesn't work for me since the link is different for every post.
Can someone point me in the right direction?
This is the code for my PostContentView
import AVKit
import CachedAsyncImage
import SwiftUI
import WPArticleView
struct PostContentView: View {
let post: Post
var body: some View {
VStack(alignment: .leading, spacing: 32) {
if let url = post.embedded.medias?.first?.url {
CachedAsyncImage(url: url, urlCache: .imageCache) { phase in
switch phase {
case.empty:
ProgressView()
.progressViewStyle(.circular)
.frame(maxWidth: .infinity)
case.success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.cornerRadius(8)
.frame(height: 200)
.padding(.bottom, 16)
.padding(.top, -10)
case.failure:
ZStack{
Image(systemName: "wifi.slash")
.foregroundColor(Color(UIColor.white))
RoundedRectangle(cornerRadius: 6)
.foregroundColor(Color(UIColor.systemGray5))
.frame(height: 200)
.frame(maxWidth: .infinity)
}
#unknown default:
EmptyView()
}
}
}
VStack(alignment: .leading, spacing: 16) {
Text(post.title.text)
.font(.title2).bold()
.padding(.horizontal)
.foregroundColor(.white)
Text(post.date.formattedDateString)
.font(.footnote)
.padding(.horizontal)
.foregroundColor(.white)
WPArticleView(htmlBody: post.content.rendered) { blocks in
ForEach(blocks.indices, id: \.self) { id in
blocks[id]
.padding(.bottom, 6)
.padding(.horizontal)
.foregroundColor(.white)
.textSelection(.enabled)
}
} text: { attributedText in
Text(attributedText)
} image: { imageURL in
CachedAsyncImage(url: imageURL, urlCache: .imageCache) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
.cornerRadius(8)
} placeholder: {
//EmptyView()
ProgressView()
.frame(maxWidth: .infinity)
}
} video: { videoURL in
VideoPlayer(player: AVPlayer(url: videoURL))
.frame(minHeight: 250)
//.frame(minHeight: 400)
.cornerRadius(8)
}
}
}
.padding(.horizontal, 20)
}
}
And my Post screen:
import Foundation
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"
}
}
EDIT:
Here is an example from a post:

Related

Thread 14 SwiftUI - Help to call an API and log in

Hello fellow Swift Coders. I'm a beginner, and i'm trying to call a web API then log in to have access to a new view.
This is my code, everything seems running well (before i try to put the login + pass)
import SwiftUI
import Foundation
struct ContentView: View {
#State private var username = ""
#State private var password = ""
#State private var loginSuccessful = false
var body: some View {
NavigationView {
ZStack {
Color("primaryColor").ignoresSafeArea()
VStack {
HStack(alignment: .top) {
Text("FOLD")
.font(.system(size: 50))
.foregroundColor(Color(.white))
.padding(.bottom, 80)
}
ZStack{
Rectangle()
.frame(width: 345, height: 400)
.cornerRadius(25)
.foregroundColor(Color("secondaryColor"))
.shadow(radius: 10, x: 10, y: 3)
VStack(alignment: .leading) {
Text("Se connecter")
.font(.system(size: 20))
.foregroundColor(.white)
.bold()
.padding(.bottom,50)
VStack(alignment: .leading) {
Text("Email")
.font(.callout)
.bold()
.foregroundColor(.white)
HStack {
TextField("Enter email", text: $username)
.padding()
.font(.system(size: 14))
.foregroundColor(Color("primaryColor"))
.frame(width: 270, height: 50)
.background(.white)
.cornerRadius(10)
}
}
.frame(width: 270, height: 60)
.padding(.bottom, 30)
VStack(alignment: .leading) {
Text("Password")
.font(.callout)
.bold()
.foregroundColor(.white)
HStack {
SecureField("Password", text: $password)
.padding()
.font(.system(size: 14))
.foregroundColor(Color("primaryColor"))
.frame(width: 270, height: 50)
.background(.white)
.cornerRadius(10)
}
}
.frame(width: 270, height: 60)
.padding(.bottom, 50)
Button(action: {
logIn(username: username, password: password)
}) {
Text("Log in")
}
.font(.headline)
.foregroundColor(.white)
.frame(width: 270,height: 40)
.background(Color("buttonColor"))
.cornerRadius(10)
}
.sheet(isPresented: $loginSuccessful) {
QRscan()
}
}
}
Spacer()
}
}
}
func logIn(username: String, password: String) {
let url = URL(string: "https://foldlab.io:8081/loginScanAccount")!
let body = ["username": username, "password": password]
let jsonData = try! JSONSerialization.data(withJSONObject: body)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = jsonData
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print(error)
return
}
if let data = data {
let json = try! JSONSerialization.jsonObject(with: data) as! [String: Any]
// Check if the login was successful
if json["status"] as! String == "success" {
DispatchQueue.main.async {
self.loginSuccessful = true
}
} else {
print("Error: Invalid username or password.")
}
}
}
task.resume()
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
The thing is, when i'm trying on the simulator or the preview, its crashing. And i have an error i've never seen.
What should i do ?
Thread 14: Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=NSCocoaErrorDomain Code=3840 "JSON text did not start with array or object and option to allow fragments not set. around line 1, column 0." UserInfo={NSDebugDescription=JSON text did not start with array or object and option to allow fragments not set. around line 1, column 0., NSJSONSerializationErrorIndex=0}
Thanks a lot.
I tried to check on SOF, but i didnt really understand the answers

How to get AsyncImage url path from url with SwiftUI

My SwiftUI app displays images from an external url properly using
LazyVGrid(columns: columns, alignment: .center, spacing: 10) {
ForEach(0..<14) { i in
AsyncImage(url: url) { image in
VStack {
image
.resizable()
.scaledToFill()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(10)
.onTapGesture {
selectedItem = ImageSelection(name: url!.path)
print(selectedItem?.name as Any)
}
}
}
.sheet(item: $selectedItem) { item in
Image(item.name)
}
But the sheet that comes up from the .onTapGesture is blank. How can I properly get the url path so the image displays on the new sheet? Thanks!
EDIT
Ultimately this view is displaying images from https://picsum.photos. I'm trying to determine the actual URL of the displayed images.
as #Asperi mentioned, you could use another AsyncImage to again download the image. Try the following code, which
fixes some of the inconsistencies in your code and also loops over
the url id (as per your latest thought) to download each different ones:
struct ImageSelection: Identifiable {
let id = UUID()
var url: URL? // <-- note
}
struct ContentView: View {
let columns:[GridItem] = Array(repeating: .init(.flexible(), spacing: 5), count: 3)
#State var selectedItem: ImageSelection?
var body: some View {
ScrollView {
LazyVGrid(columns: columns, alignment: .center, spacing: 10) {
ForEach(0..<14) { i in
let url = URL(string: "https://picsum.photos/id/\(i)/200")
AsyncImage(url: url) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(10)
.onTapGesture {
selectedItem = ImageSelection(url: url)
}
}
else {
Image(systemName: "smiley")
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
}
}
}
}
.sheet(item: $selectedItem) { item in
AsyncImage(url: item.url)
}
}
}
}

I've searched and search SwiftUI Switch Case Menu Cycle?

I've found great content, But nothing too specific to my needs being swiftui is still new.
I need to support menu cycling with switch case in the latest swift and monterey for macos, no ios development. i need strings, variables, and custom graphics to make a menu title and current in need of up to 9 menus to cycle randomly from one to a random other without an if statement looping through all of the others first:
more info here: https://pastebin.com/VCnEmdBa
Additional information on needs:
I want to have the switch case cycle my nine menus, where i can be on any given one and the menu jump to the next random selection, right now it currently rotates in order no matter where i click.
import Foundation
import SwiftUI
import CoreData
import Combine
import PDFKit
import SceneKit
import WebKit
struct Cotharticren: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct Option: Hashable {
let title: String
let imageName: String
}
struct ContentView: View {
#State var currentOption = 0
let options: [Option] = [
.init(title: "DART Meadow", imageName: "sun.max.fill"),
.init(title: "Research", imageName: "flame"),
.init(title: "Navigation", imageName: "moon.stars.fill"),
.init(title: "Shelf", imageName: "archivebox"),
.init(title: "Chest" ,imageName: "shippingbox"),
.init(title: "Crate" ,imageName: "bonjour"),
.init(title: "Manufactoring", imageName: "gear"),
.init(title: "Warehouse", imageName: "archivebox.fill"),
.init(title: "Journal", imageName: "note.text"),
]
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .top) {
ScrollView( .vertical) {
NavigationView{
/*
List(1..<9)
{
Text("\($0)")
}
.listStyle(SidebarListStyle())
}
*/
ListView(options: options, currentSelection: $currentOption)
//Text(systemName: myItem.selectedImageName) + Text(myItem.whateverText)
switch (currentOption) {
case 1:
OrbitNodeView()
case 2:
ATM26NodeView()
case 3:
NozzleNodeView()
case 4:
EmptyView()
VStack(alignment: .center) {
Text("Chest")
.font(.largeTitle)
.bold()
.colorInvert()
}
case 5:
EmptyView()
VStack(alignment: .center) {
Text("Crate")
.font(.largeTitle)
.bold()
.colorInvert()
}
case 6:
EmptyView()
VStack(alignment: .center) {
Text("Manufactoring")
.font(.largeTitle)
.bold()
.colorInvert()
}
case 7:
EmptyView()
VStack(alignment: .center) {
Text("Warehouse")
.font(.largeTitle)
.bold()
.colorInvert()
}
case 8:
VStack(alignment: .center) {
Text("Journal")
.font(.largeTitle)
.bold()
.colorInvert()
.padding(.top, 60)
Image("articrenmeadowopacity")
.shadow(radius: 3)
WebView()
}
default:
MainView()
}
}.background(Color.white)
}
}
}
Spacer()
}
}
struct MainView: View {
var body: some View{
VStack(alignment: .leading) {
HStack(alignment: .bottom) {
Image("CotharticrenMainView")
.shadow(radius: 3)
}
}
.frame(width: 900, height: 800, alignment: .center)
Spacer()
}
}
struct ListView: View {
let options: [Option]
#Binding var currentSelection: Int
var body: some View{
VStack(alignment: .leading) {
HStack(alignment: .top) {
HStack(alignment: .top) {
VStack(alignment: .trailing) {
Circle()
.stroke(Color.init(red: 0.9, green: 0.95, blue: 0.2, opacity: 1), lineWidth: 2)
.alignmentGuide(HorizontalAlignment.myAlignment)
{ d in d[.leading] }
.alignmentGuide(VerticalAlignment.myAlignment)
{ d in d[.bottom] }
.frame(width: 50, height: 50)
Circle()
.stroke(Color.init(red: 0.25, green: 0.9, blue: 0.2, opacity: 1), lineWidth: 2)
.alignmentGuide(HorizontalAlignment.myAlignment)
{ d in d[.leading] }
.alignmentGuide(VerticalAlignment.myAlignment)
{ d in d[.bottom] }
.frame(width: 25, height: 25)
VStack(alignment: .leading) {
Circle()
.stroke(Color.init(red: 0.1, green: 0.1, blue: 1, opacity: 1), lineWidth: 2)
.alignmentGuide(HorizontalAlignment.myAlignment)
{ d in d[.leading] }
.alignmentGuide(VerticalAlignment.myAlignment)
{ d in d[.bottom] }
.frame(width: 75, height: 75)
}
}
}
HStack(alignment: .top) {
Image("DARTMeadowCSMwidthArtemis2by1")
.shadow(radius: 3)
.padding(.top, 10)
}
.padding(.top, 20)
.padding(.trailing, 10)
}.padding(.top, 20).padding(.leading, 10)
HStack(alignment: .center) {
VStack(alignment: .center) {
Image("arrow300")
HStack(alignment: .center) {
Text("You've never plotted an Edge?")
}
}
}.shadow(radius: 3)
VStack(alignment: .leading) {
let current = options[currentSelection]
ForEach(options, id: \.self) {option in
HStack {
Image(systemName: option.imageName)
//.resizable()
//.aspectRatio(contentMode: .fit)
.frame(width: 20)
Text(option.title)
.foregroundColor(current == option ? Color.blue : Color.white)
}
.padding(8)
.onTapGesture {
currentSelection += 1
if currentSelection == 9 {
currentSelection = 0
}
}
}
Spacer()
}.frame(width: 300, height: 800, alignment: .leading)
}
Spacer()
}
}
struct WebView: View {
var body: some View{
VStack(alignment: .leading) {
HStack(alignment: .bottom) {
}
}
.frame(width: 900, height: 800, alignment: .center)
Spacer()
}
}
You can add an Identifier to your Option class and use this for currentSelection, if you want to set an option, just set currentSelection to option.id:
Also:
1: If you want answers, it's best to format your code, before you post it (select in Xcode and ctrl+i should do it), so it's easy to read and understand
2: A minimal, reproducible example is not just posting your entire code, create an example, that contains only as much code as necessary to show the problem you're experiencing. The code I posted would be a better example, it will work without having to change anything. Your code includes references to objects that are not on here, so a possible helper would have to remove those, before he could even test your issue
here is a guide on how to create a minimal, reproducible example:
struct Option: Hashable, Identifiable {
// Identifier for Option !! MUST be unique
let id: Int
let title: String
let imageName: String
}
struct ContentView: View {
#State var currentOption: Int = 0
let options: [Option] = [
.init(id: 1, title: "DART Meadow", imageName: "sun.max.fill"),
.init(id: 2, title: "Research", imageName: "flame"),
.init(id: 3, title: "Navigation", imageName: "moon.stars.fill"),
]
var body: some View {
GeometryReader { geo in
HStack {
ListView(options: options, currentSelection: $currentOption)
.frame(width: geo.size.width / 2, height: geo.size.height)
switch (currentOption) {
case 1: Text("OrbitNodeView")
case 2: Text("ATM26NodeView")
case 3: Text("NozzleNodeView")
default: Text("MainView")
}
}
}
}
}
struct ListView: View {
let options: [Option]
#Binding var currentSelection: Int
var body: some View{
VStack(alignment: .leading) {
ForEach(options, id: \.self) {option in
HStack {
Image(systemName: option.imageName)
.frame(width: 20)
Text(option.title)
// Don't even have to use current = options[currentSelection] anymore:
.foregroundColor(currentSelection == option.id ? .accentColor : .primary)
}
.padding(8)
.onTapGesture {
// Set the currentSelection to the ID of the option
currentSelection = option.id
}
}
}
}
}

Asynchronous Image Loading Bug

I am hitting a strange bug when it comes to Asynchronous Image Loading, where when I enter a view, the shows up like it is suppose to do, then for some reason the image drops and all I see is the "Loading..." placeholder. I used this tutorial when building my loader and the following script is my Article View. I have a Global Functions file, which includes reference to Combine and Foundation for my various functions through out the app. I am just not fully understanding why the image is showing for a brief moment, then calls the placeholder. Thanks!
import SwiftUI
struct ArticleView: View {
#Environment(\.imageCache) var cache: ImageCache
var articleID: Int
#ObservedObject private var data = Result2()
let defaults = UserDefaults.standard
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
init(articleID: Int){
self.articleID = articleID
self.data.results.append(Story.init(id: 0, title: "test", image: "", story: "", published: "", author: ""))
self.loadArticle(CDNLink: "http://\(self.defaults.object(forKey: "domain") as! String)/cdn?funct=fetchArticle&articleID=\(self.articleID)")
}
var backBtn: some View {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label:{
Image(systemName: "lessthan.circle")
.resizable()
.frame(width: 30, height: 30)
.foregroundColor(Color.primaryRed)
.padding(.top, 6)
.padding(.leading, 10)
})
}
var body: some View {
VStack(alignment: .leading){
ScrollView {
ForEach(data.results, id: \.id) { result in
Group{
AsyncImage(
url: URL(string: result.image)!,
placeholder: Text("Loading..."), configuration: { $0.resizable() })
.frame(height: 250)
.aspectRatio(contentMode: .fill)
VStack(alignment: .leading) {
Text("\(result.title)")
.font(.system(size: 18))
.fontWeight(.heavy)
.foregroundColor(.primary)
.multilineTextAlignment(.leading)
HStack {
Text("\(result.author)")
.font(.system(size: 12))
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
Text("|")
.font(.system(size: 12))
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
Text("\(result.published)")
.font(.system(size: 12))
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
}.frame(width: UIScreen.main.bounds.size.width - 40, alignment: .leading)
}.frame(width: UIScreen.main.bounds.size.width - 40)
}
Text("\(result.story)")
}
}
}
.frame(maxWidth: .infinity)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backBtn)
}
func loadArticle(CDNLink: String) {
guard let url = URL(string: CDNLink) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if error != nil {
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
return
} else {
do {
let decodedResponse = try JSONDecoder().decode(Response2.self, from: data!)
print(decodedResponse)
DispatchQueue.main.async {
self.data.results.append(contentsOf: decodedResponse.results)
self.data.results.remove(at: 0)
}
} catch let err {
print("Error parsing: \(err)")
}
}
}.resume()
}
}
struct Response2: Codable {
var results: [Story]
}
struct Story: Codable, Identifiable {
var id: Int
var title: String
var image: String
var story: String
var published: String
var author: String
}
struct ArticleView_Previews: PreviewProvider {
static var previews: some View {
ArticleView(articleID: 0)
}
}
Edit: I have made the changes suggested and I am still seeing the error, you can see the error live by checking out this link.
Hi most likely it is due to the fact that you are using #ObservedObject. This object is discarded and initialized every time your view state changes and is triggering a view re-render. Try using #StateObject instead of #ObservedObject. The #StateObject will be initialized only once in the view.
A #StateObject can be also used with an ObservedObject model. It is kind of a combination of #ObservedObject and #State.
Okay so I created a new solution to patch this issue up and I am very glad that this little work around is working. So, I started looking at what I could do server side to optimize content being pulled into the app. What I did was use PHP to encode the image with base64, then the app pulls it in and decodes the base64 data and the image populates with lightning speed!
Server Side code:
$article['image'] = base64_encode(file_get_contents(PATH . "/cache/content/topstory/" . $row['app_article']));
Client Side code:
let dataDecoded:NSData = NSData(base64Encoded: result.image, options: NSData.Base64DecodingOptions(rawValue: 0))!
let decodedimage:UIImage = UIImage(data: dataDecoded as Data)!
Image(uiImage: decodedimage)
.frame(height: 250)
.aspectRatio(contentMode: .fill)

SwiftUi : for i in 0... How to solve closure containing control flow statement

I want to display some images that depend on an integer.
Example with '3':
VStack {
Text(recette.name)
HStack() {
Text("Durée 20 min")
.font(.caption)
.fontWeight(.light)
Text("Notes")
.font(.caption)
.fontWeight(.light)
HStack(spacing: -1.0) {
for 0 in 0...recette.avis{
Image(systemName: "star.fill")
.padding(.leading)
.imageScale(.small)
.foregroundColor(.yellow)
}
}
}
}
but the code doesn't compile with this error in for.
Closure containing control flow statement cannot be used with function builder 'ViewBuilder'
Can somebody help me ?
Thank you.
You want to use a ForEach so that you can create your stars.
Below is a working example.
// This is a simple struct to mock the data
struct Recette {
let name: String = "Test"
let avis: Int = 3
}
struct ContentView: View {
let recette = Recette()
var body: some View {
VStack {
Text(recette.name)
HStack() {
Text("Durée 20 min")
.font(.caption)
.fontWeight(.light)
Text("Notes")
.font(.caption)
.fontWeight(.light)
HStack(spacing: -1.0) {
ForEach(0..<recette.avis) {_ in // <- use ForEach() here
Image(systemName: "star.fill")
.padding(.leading)
.imageScale(.small)
.foregroundColor(.yellow)
}
}
}
}
}
}
This is what the above code produces: