How to make hyperlinks in SwiftUI - swiftui

In Swift, as shown here, you can use NSMutableAttributedString to embed links in text.
How can I achieve this with SwiftUI?
I implemented it as the following, but it does not look how I want it to. .
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
Text("By tapping Done, you agree to the ")
Button(action: {}) {
Text("privacy policy")
}
Text(" and ")
Button(action: {}) {
Text("terms of service")
}
Text(" .")
}
}
}

iOS 15+ (Swift 5.5 +)
SwiftUI has built-in support for rendering Markdown
To create a link, enclose the link text in brackets (e.g., [Duck Duck Go]) and then follow it immediately with the URL in parentheses (e.g., (https://duckduckgo.com)).
Text("[Privacy Policy](https://example.com)")
https://www.markdownguide.org/basic-syntax/#links
String variable
Use init(_ value: String)
Creates a localized string key from the given string value.
let link = "[Duck Duck Go](https://duckduckgo.com)"
Text(.init(link))
String interpolation
Use init(_ value: String)
Creates a localized string key from the given string value.
let url = "https://duckduckgo.com"
let link = "[Duck Duck Go](\(url))"
Text(.init(link))
Attributed text
Use init(_ attributedContent: AttributedString)
Creates a text view that displays styled attributed content.
let markdownLink = try! AttributedString(markdown: "[Duck Duck Go](https://duckduckgo.com)")
Text(markdownLink)
Similar question:
Making parts of text bold in SwiftUI
Use the Link View
A control for navigating to a URL.
https://developer.apple.com/documentation/swiftui/link
Link("Privacy Policy", destination: URL(string: "https://example.com")!)

Just as #mahan mention, this works perfectly for iOS 15.0 and above by using markdown language:
Text("[Privacy Policy](https://example.com)")
But if you're using a String variable, and put it into the Text it wouldn't work. Example:
let privacyPolicyText = "Read our [Privacy Policy](https://example.com) here."
Text(privacyPolicyText) // Will not work
Solution for using a String variable
The reason is that Text got multiple initiations. So how do we solve this? The easiest way is just to do:
let privacyPolicyText = "Read our [Privacy Policy](https://example.com) here."
Text(.init(privacyPolicyText))
Result: Read our Privacy Policy here.

it's very simple just use LocalizedStringKey for example:
let message = "Hello, www.google.com this is just testing for hyperlinks, check this out our website https://www.apple.in thank you."
Text(LocalizedStringKey(message))

Motjaba Hosseni is right so far there is nothing that resembles NSAttributedString in SwiftUI.
This should solve your problem for the time being:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("By tapping Done, you agree to the ")
HStack(spacing: 0) {
Button("privacy policy") {}
Text(" and ")
Button("terms of service") {}
Text(".")
}
}
}
}

It's always an option to wrap a UIKit view in UIViewRepresentable. Just have to go through the manual process of exposing each attribute you want to change.
struct AttributedText: UIViewRepresentable {
var attributedText: NSAttributedString
init(_ attributedText: NSAttributedString) {
self.attributedText = attributedText
}
func makeUIView(context: Context) -> UITextView {
return UITextView()
}
func updateUIView(_ label: UITextView, context: Context) {
label.attributedText = attributedText
}
}
//usage: AttributedText(NSAttributedString())

I know it's a bit late but I solved the same problem using HTML.
First I created a small helper and link model.
struct HTMLStringView: UIViewRepresentable {
let htmlContent: String
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.loadHTMLString(htmlContent, baseURL: nil)
}
}
struct TextLink {
let url: URL
let title: String
}
Next I created function that changes String to HTML and replaces first occurrence of #link to my tappable link.
var content = "My string with #link."
var link = TextLink(url: URL(string: "https://www.facebook.com")!, title: "Facebook")
var body: some View {
let bodySize = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body).pointSize
var html = "<span style=\"font: -apple-system-body; font-size:calc(\(bodySize)px + 1.0vw)\">"
if let linkRange = content.range(of: "#link") {
let startText = content[content.startIndex ..< linkRange.lowerBound]
let endText = content[linkRange.upperBound ..< content.endIndex]
html += startText
html += "\(link.title)"
html += endText
} else {
html += content
}
html += "</span>"
return HTMLStringView(htmlContent: html)
}

I tried concatenated Texts with Link in between and these are the ways for iOS 15+ and below iOS 15.
if #available(iOS 15, *) {
Text("[Seperate Link 1 ](https://www.android.com/intl/en_in/)")
.font(.caption)
.foregroundColor(Color.green)
// green color is not applied.
Text("[Seperate Link 2 ](https://www.apple.com/in/)")
.font(.caption)
.accentColor(Color.green)
// green is applied.
Text("By authorizing you agree
our ")
.font(.caption)
.foregroundColor(Color.black)
+ Text("[Terms and Conditions](https://www.android.com/intl/en_in/)")
.font(.caption)
.foregroundColor(Color.green) // default blue is applied
+ Text(" and ")
.font(.caption)
.foregroundColor(Color.black)
+ Text("[Privacy Policy](https://www.apple.com/in/)")
.font(.caption)
.foregroundColor(Color.green) // default blue
// cannot use accentColor(Color.green) here
}
else{
// lower iOS versions.
VStack{
Text("By authorizing you agree our ")
.font(.caption)
.foregroundColor(Color.black)
HStack(spacing: 4 ) {
Text("Terms and Conditions")
.font(.caption)
.foregroundColor(Color.green)
.onTapGesture {
let url = URL.init(string: "https://www.android.com/intl/en_in/")
guard let termsAndConditionURL = url, UIApplication.shared.canOpenURL(termsAndConditionURL) else { return }
UIApplication.shared.open(termsAndConditionURL)
}
Text("and")
.font(.caption)
.foregroundColor(Color.black)
Text("Privacy Policy")
.font(.caption)
.foregroundColor(Color.green)
.onTapGesture {
let url = URL.init(string: "https://www.apple.com/in/")
guard let privacyPolicyURL = url, UIApplication.shared.canOpenURL(privacyPolicyURL) else { return }
UIApplication.shared.open(privacyPolicyURL)
}
}
}
}
swift ios swiftui

To be on the safe side, if your text is not a string literal, you will probably want to use .init. In other words, if there's any string concatenation, interpolation, etc., you may want to use Text(.init(...)).
Just note that .init in this case actually refers to LocalizedStringKey.init, so localization will still be happening, just like when you're just passing a string literal.
Here are some examples and their rendered output in Xcode 14 previews.
let foo = "Foo"
let bar = "Bar"
let link = "link"
Group {
Text("Foo [Bar](link) Baz") // ✅
Text("Foo" + " [Bar](link) Baz") // ❌
Text(foo + " [Bar](link) Baz") // ❌
Text("\(foo) [Bar](link) Baz") // ✅
Text("\(foo) [Bar](\(link)) Baz") // ❌
Text("\(foo) [\(bar)](\(link)) Baz") // ❌
}
Rectangle().height(1)
Group {
Text(.init("Foo [Bar](link) Baz")) // ✅
Text(.init("Foo" + " [Bar](link) Baz")) // ✅
Text(.init(foo + " [Bar](link) Baz")) // ✅
Text(.init("\(foo) [Bar](link) Baz")) // ✅
Text(.init("\(foo) [Bar](\(link)) Baz")) // ✅
Text(.init("\(foo) [\(bar)](\(link)) Baz")) // ✅
}

iOS 15 below
import SwiftUI
import SwiftUIFlowLayout
public struct HyperlinkText: View {
private let subStrings: [StringWithLinks]
public init(html: String) {
let newString = html.replacingOccurrences(of: "<a href=\'(.+)\'>(.+)</a>",
with: "#&#$2#&#$1#&#",
options: .regularExpression,
range: nil)
self.subStrings = newString.components(separatedBy: "#&#").compactMap{ subString in
let arr = subString.components(separatedBy: "#&#")
return StringWithLinks(string: arr[0], link: arr[safe: 1])
}
}
public var body: some View {
FlowLayout(mode: .scrollable,
binding: .constant(false),
items: subStrings,
itemSpacing: 0) { subString in
if let link = subString.link, let url = URL(string: link) {
Text(subString.string)
.foregroundColor(Color(hexString: "#FF0000EE"))
.onTapGesture {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
.fixedSize(horizontal: false, vertical: true)
} else {
Text(subString.string).fixedSize(horizontal: false, vertical: true)
}
}
}
}
struct StringWithLinks: Hashable, Identifiable {
let id = UUID()
let string: String
let link: String?
static func == (lhs: StringWithLinks, rhs: StringWithLinks) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

*** iOS 15 ***
You can add emails or phonenumbers like this too:
var infoText = "For any queries reach out to [email] or call [phone]"
var phone = "+45 12345678".replacingOccurrences(of: " ", with: "")
var email = "some-email#some-host.something"
var footerText: String {
return infoText
.replacingOccurrences(
of: "[email]",
with: "[\(email)](mailto:\(email))"
)
.replacingOccurrences(
of: "[phone]",
with: "[\(phone)](tel:\(phone))"
)
}
Text(.init(footerText))
Just remember the links (phone mainly, as emails doesn't have spaces) can't have spaces.
Also remember that this won't work on the simulator. You need a real device to test it.

Use built-in function +, it looks like a charm:
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
Button(action: {
}) {
Text("By tapping Done, you agree to the ")
+ Text("privacy policy")
.foregroundColor(Color.blue)
+ Text(" and ")
+ Text("terms of service")
.foregroundColor(Color.blue)
+ Text(".")
}
.foregroundColor(Color.black)
}
}
}

Related

How to make Text tappable inside Group? Compiler problem can't type check

The idea is to create Privacy Text from where I can tap on some selected link and open Safari window inside the app.
As you can see that commented code is redirecting to Safari app to see the web page.
But the problem is with compiler that says - The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressionsThe compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
Here's the code, Please let me know how to overcome this, thanks 🙏
import SwiftUI
struct PrivacyText: View {
let text: String
// private let privacyPolicy = "[Privacy Policy](https://www.apple.com/)"
// private let termsAndConditions = "[Terms & Conditions](https://www.google.com.ua/)"
private let privacyPolicy = URL(string: "https://www.apple.com/")
private let termsAndConditions = URL(string: "https://www.google.com.ua/")
#State private var isSafariPresented = false
#State private var selectedUrl: URL?
var body: some View {
// Group {
// Text(text) +
// Text(" ") +
// Text(.init(privacyPolicy)) +
// Text(" and ") +
// Text(.init(termsAndConditions))
// }
// .accentColor(.primary)
// .foregroundColor(Color(hex: 0x727272))
// .font(.system(size: 11))
// .multilineTextAlignment(.center)
Group {
Text(text) +
Text(" ") +
Text("Privacy Policy")
.foregroundColor(.white)
.onTapGesture {
selectedUrl = privacyPolicy
isSafariPresented.toggle()
}
Text(" and ") +
Text("Terms & Conditions")
.foregroundColor(.white)
.onTapGesture {
selectedUrl = termsAndConditions
isSafariPresented.toggle()
}
}
// .accentColor(.primary)
.foregroundColor(Color(hex: 0x727272))
.font(.system(size: 11))
.multilineTextAlignment(.center)
.fullScreenCover(isPresented: $isSafariPresented) {
if let selectedUrl = selectedUrl {
SFSafariView(url: selectedUrl)
}
}
}
}
struct PrivacyText_Previews: PreviewProvider {
static var previews: some View {
PrivacyText(text: "By continuing, you agree to")
}
}
import SafariServices
struct SFSafariView: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: UIViewControllerRepresentableContext<Self>) -> SFSafariViewController {
let safariController = SFSafariViewController(url: url)
safariController.preferredBarTintColor = UIColor(.white.opacity(0.8))
safariController.preferredControlTintColor = UIColor(.white.opacity(0.8))
return safariController
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<SFSafariView>) {
return
}
}
The compiler is unable to type-check this expression in reasonable time is caused most of the time because of a typo.
Your first typo is because of
Text(text) +
Text(" ") + // Here
Text("Privacy Policy")
.foregroundColor(.white)
.onTapGesture {
And
Text(" and ") + //Here
Text("Terms & Conditions")
.foregroundColor(.white)
.onTapGesture {
It is a typo because once you add a ViewModifier(s) (.foregroundColor(.white) and . onTapGesture) the Text becomes a View + does not work between a View and a Text
The second typo is
if let selectedUrl = selectedUrl {
SFSafariView(url: selectedUrl)
}//Here
You need an else, in a very fragile manner you assume that you don't need an else by setting $isSafariPresented first.
The second is easier to fix just use .fullScreenCover(item:)
.fullScreenCover(item: $selectedURL) { selectedUrl in
SFSafariView(url: selectedUrl)
}
The first is more complex and you won't get the multiline formatting.
//switch to HStack
HStack {
Text(text) +
Text(" ") //Remove +
Text("Privacy Policy")
.foregroundColor(.white)
.onTapGesture {
selectedURL = privacyPolicy
}
Text(" and ") //Remove +
Text("Terms & Conditions")
.foregroundColor(.white)
.onTapGesture {
selectedURL = termsAndConditions
}
}.lineLimit(1).minimumScaleFactor(0.5)

How to Append to Array of Structs and have Change Persistant?

I want to be able to store a list of objects in a separate Swift file and call them in a page to have them show up. I successful did this with this code:
import Foundation
import SwiftUI
struct MatchInfo: Hashable, Codable {
let theType: String
let theWinner: String
let theTime: String
let id = UUID()
}
var matchInfo = [
MatchInfo(theType: "Capitalism", theWinner: "Julia", theTime: "3/3/2021"),
MatchInfo(theType: "Socialism", theWinner: "Julia", theTime: "3/2/2021"),
MatchInfo(theType: "Authoritarianism", theWinner: "Luke", theTime: "3/1/2021")
]
where I append to the list after a match is played on another page here:
matchInfo.insert(MatchInfo(theType: typeSelection, theWinner: winnerName, theTime: "\(datetimeWithoutYear)" + "\(year)"), at: 0)
And heres some of the code on another page where I call it into a list:
List {
ForEach(matchInfo, id: \.self) { matchData in
matchRow(matchData : matchData)
} .background(Color("invisble"))
.listRowBackground(Color("invisble"))
} .frame(height: 490)
...
struct matchRow: View {
let matchData: MatchInfo
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(matchData.theType)
.font(.system(size: 20, weight: .medium, design: .default))
Text(" Winner: " + matchData.theWinner)
}
Spacer()
Text(matchData.theTime)
.padding(.leading, 40)
.multilineTextAlignment(.trailing)
}
.foregroundColor(.white)
.accentColor(.white)
}
}
But this code doesn't save through app restarts. I've never had something save through a restart before and have been struggling to find an answer simple enough for me to understand. How can I update the list without it going away next time I open the app?
Okay so here is an example on how you save to/ load from the documents folder.
First of all make sure that you object MatchInfo conforms to this protocol.
import Foundation
protocol LocalFileStorable: Codable {
static var fileName: String { get }
}
extension LocalFileStorable {
static var localStorageURL: URL {
guard let documentDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first else {
fatalError("Can NOT access file in Documents.")
}
return documentDirectory
.appendingPathComponent(self.fileName)
.appendingPathExtension("json")
}
}
extension LocalFileStorable {
static func loadFromFile() -> [Self] {
do {
let fileWrapper = try FileWrapper(url: Self.localStorageURL, options: .immediate)
guard let data = fileWrapper.regularFileContents else {
throw NSError()
}
return try JSONDecoder().decode([Self].self, from: data)
} catch _ {
print("Could not load \(Self.self) the model uses an empty collection (NO DATA).")
return []
}
}
}
extension LocalFileStorable {
static func saveToFile(_ collection: [Self]) {
do {
let data = try JSONEncoder().encode(collection)
let jsonFileWrapper = FileWrapper(regularFileWithContents: data)
try jsonFileWrapper.write(to: self.localStorageURL, options: .atomic, originalContentsURL: nil)
} catch _ {
print("Could not save \(Self.self)s to file named: \(self.localStorageURL.description)")
}
}
}
extension Array where Element: LocalFileStorable {
///Saves an array of LocalFileStorables to a file in Documents
func saveToFile() {
Element.saveToFile(self)
}
}
Your main Content View should look like this: (I modified your object to make it a bit simpler.)
import SwiftUI
struct MatchInfo: Hashable, Codable, LocalFileStorable {
static var fileName: String {
return "MatchInfo"
}
let description: String
}
struct ContentView: View {
#State var matchInfos = [MatchInfo]()
var body: some View {
VStack {
Button("Add Match Info:") {
matchInfos.append(MatchInfo(description: "Nr." + matchInfos.count.description))
MatchInfo.saveToFile(matchInfos)
}
List(matchInfos, id: \.self) {
Text($0.description)
}
.onAppear(perform: {
matchInfos = MatchInfo.loadFromFile()
})
}
}
}

Display filename in next View too

I have a code that makes a http Request, gets an array with filenames from that, displays them each with an image and the filename below. Everything works fine.
Now I made each image a button that opens a detail page.
That works but at the top it should say the matching filename from the page before.
But I am not able to hand over the filename (name) from ContentView4 to the next page (ts).
The language is SwiftUi
Could you please help me?
Thanks
Nikias
Here is my code:
import SwiftUI
struct ContentView4: View {
#State var showingDetail = false
#State var username: String = "."
#State var password: String = "."
#State private var name = String("Nikias2")
#State private var t = String()
#State private var x = -1
#State var dateien = ["word.png"]
var body: some View {
ScrollView(.vertical) {
ZStack{
VStack {
ForEach(0 ..< dateien.count, id: \.self) {
Button(action: {
print("button pressed")
x = x + 1
t = dateien[x]
self.showingDetail.toggle()
}) {
Image("datei")
}
.scaledToFit()
.padding(0)
Text(self.dateien[$0])
Text(t)
.foregroundColor(.white)
}
}
}
.sheet(isPresented:
$showingDetail) {
ts(name: t)
}
.onAppear { //# This `onAppear` is added to `ZStack{...}`
doHttpRequest()
}
}
}
func doHttpRequest() {
let myUrl = URL(string: "http://192.168.1.180/int.php")! //# Trailing semicolon is not needed
var request = URLRequest(url: myUrl)
request.httpMethod = "POST"// Compose a query string
let postString = "Name=\($username)&Passwort=\($password)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
//# Use if-let when you want to use the unwrapped value
if let error = error {
print("error=\(error)")
return
}
//# Use guard-let when nil has no meaning and want to exit on nil
guard let response = response else {
print("Unexpected nil response")
return
}
// You can print out response object
print("response = \(response)")
//Let's convert response sent from a server side script to a NSDictionary object:
do {
//# Use guard-let when nil has no meaning and want to exit on nil
guard let data = data else {
print("Unexpected nil data")
return
}
//#1 `mutableContainer` has no meaning in Swift
//#2 Use Swift Dictionary type instead of `NSDictionary`
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
if let parseJSON = json {
// Now we can access value of First Name by its key
//# Use if-let when you want to use the unwrapped value
if let firstNameValue = parseJSON["Name"] as? String {
print("firstNameValue: \(firstNameValue)")
let dateien = firstNameValue.components(separatedBy: ",")
print(dateien)
self.dateien = dateien
}
}
} catch {
print(error)
}
}
task.resume()
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
ContentView4()
}
}
struct ts: View {
#State var hin = false
#State var um = false
#State var datname: String = ""
var name: String
var body: some View {
NavigationView {
VStack {
Text(name)
.font(.system(size: 60))
.foregroundColor(.black)
.padding(50)
Button(action: {
self.hin.toggle()
}) {
Text("+")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.yellow)
.cornerRadius(35.0)
}
.padding()
if hin {
HStack {
Text("Datei auswählen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.yellow)
.cornerRadius(20.0)
.animation(Animation.default)
Text("Datei hochladen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.yellow)
.cornerRadius(20.0)
.animation(Animation.default)
}
}
Text("Datei herunterladen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.blue)
.cornerRadius(35.0)
Button(action: {
self.um.toggle()
}) {
Text("Datei umbenennen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.green)
.cornerRadius(35.0)
}
.padding()
if um {
HStack {
TextField(name, text: $datname)
.font(.headline)
.frame(width: 150, height: 70)
.cornerRadius(20.0)
.animation(Animation.default)
Text("Datei umbenennen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.green)
.cornerRadius(20.0)
.animation(Animation.default)
}
}
Text("Datei löschen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.red)
.cornerRadius(35.0)
}
}
}
}
I believe your issue is a result of using #State variables to store all of the attributes. #State variables are not consistent and get refreshed in the background by SwiftUI depending on your views visibility.
The piece that you are missing is a view controller class stored in an #EnviornmentObject variable. This class gets Initiated in your main contentView and is used to keep track and alter of all your attributes.
Each ContentView should reference the single #EnviornmentObject and pull data from that class.
Another solution which may work would be to replace all your #State variables with #StateObject vars. #StateObject vars are basically #State vars but get initiated before the struct get loaded and the value is kept consistent regardless of the view state of the parent struct.
Here is a rough implementation of #EnvironmentObject within your project.
Basically use the #EnvironmentObject to pass values to child views
ContentView4.swift
struct ContentView4: View {
#EnvironmentObject cv4Controller: ContentView4Controller
var body: some View {
ScrollView(.vertical) {
ZStack{
VStack {
ForEach(0 ..< cv4Controller.dateien.count, id: \.self) {
Button(action: {
print("button pressed")
x = x + 1
t = cv4Controller.dateien[x]
self.showingDetail.toggle()
}) {
Image("datei")
}
.scaledToFit()
.padding(0)
Text(self.dateien[$0])
Text(cv4Controller.t)
.foregroundColor(.white)
}
}
}
.sheet(isPresented:
cv4Controller.$showingDetail) {
ts(name: cv4Controller.t)
}
.onAppear { //# This `onAppear` is added to `ZStack{...}`
cv4Controller.doHttpRequest()
}
}
}
ContentView4Controller.swift
class ContentView4Controller: ObservableObject {
#Published var showingDetail = false
#Published var username: String = "."
#Published var password: String = "."
#Published private var name = String("Nikias2")
#Published private var t = String()
#Published private var x = -1
#Published private var t = String()
#Published private var x = -1
#Published var dateien = ["word.png"]
func doHttpRequest() {
let myUrl = URL(string: "http://192.168.1.180/int.php")! //# Trailing semicolon is not needed
var request = URLRequest(url: myUrl)
request.httpMethod = "POST"// Compose a query string
let postString = "Name=\($username)&Passwort=\($password)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
//# Use if-let when you want to use the unwrapped value
if let error = error {
print("error=\(error)")
return
}
//# Use guard-let when nil has no meaning and want to exit on nil
guard let response = response else {
print("Unexpected nil response")
return
}
// You can print out response object
print("response = \(response)")
//Let's convert response sent from a server side script to a NSDictionary object:
do {
//# Use guard-let when nil has no meaning and want to exit on nil
guard let data = data else {
print("Unexpected nil data")
return
}
//#1 `mutableContainer` has no meaning in Swift
//#2 Use Swift Dictionary type instead of `NSDictionary`
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
if let parseJSON = json {
// Now we can access value of First Name by its key
//# Use if-let when you want to use the unwrapped value
if let firstNameValue = parseJSON["Name"] as? String {
print("firstNameValue: \(firstNameValue)")
let dateien = firstNameValue.components(separatedBy: ",")
print(dateien)
self.dateien = dateien
}
}
} catch {
print(error)
}
}
task.resume()
}
}
Example of main ContentView.swift
struct ContentView: View {
var cv4Controller: ContentView4Controller = ContentView4Controller()
var body: some view {
// your main page output
GeometryReader { geo in
// just a guess for what you have in your main contentView
switch(page) {
case .main:
ContentView2()
default:
ContentView4()
break
}
}.environmentObject(cv4Controller) // this will make cv4Controller available to all child view structs
}
}
Add #Binding wrapper to the "name" variable in your ts view. And pass the t variable as a binding by adding a "$". This will keep your ts name variable updated to whatever is value it has in the parent view.
Also why do you use a NavigationView in your ts View?
struct ContentView4: View {
...
#State private var t = String()
...
var body: some View {
...
ZStack{
...
}
.sheet(isPresented: $showingDetail) {
ts(name: $t)
}
...
}
func doHttpRequest() {
...
}
}
struct ts: View {
...
#Binding var name: String
var body: some View {
...
}
}
My starting code works, but It's just displaying the Filenames in a row and if I tap a random image, the name won't fit, only if I'm going down in the row and tap them. The problem is, that I don't know how to set the variable to the id, not to pass them to the next view. Has anyone got and idea how I can pass the right filename into a variable in the for loop and read it in the next view?

SwiftUI: Add ClearButton to TextField

I am trying to add a ClearButton to TextField in SwiftUI when the particular TextField is selected.
The closest I got was creating a ClearButton ViewModifier and adding it to the TextField using .modifer()
The only problem is ClearButton is permanent and does not disappear when TextField is deselected
TextField("Some Text" , text: $someBinding).modifier(ClearButton(text: $someBinding))
struct ClearButton: ViewModifier {
#Binding var text: String
public func body(content: Content) -> some View {
HStack {
content
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
.foregroundColor(.secondary)
}
}
}
}
Use ZStack to position the clear button appear inside the TextField.
TextField("Some Text" , text: $someBinding).modifier(ClearButton(text: $someBinding))
struct ClearButton: ViewModifier
{
#Binding var text: String
public func body(content: Content) -> some View
{
ZStack(alignment: .trailing)
{
content
if !text.isEmpty
{
Button(action:
{
self.text = ""
})
{
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.opaqueSeparator))
}
.padding(.trailing, 8)
}
}
}
}
Use .appearance() to activate the button
var body: some View {
UITextField.appearance().clearButtonMode = .whileEditing
return TextField(...)
}
For reuse try with this:
func TextFieldUIKit(text: Binding<String>) -> some View{
UITextField.appearance().clearButtonMode = .whileEditing
return TextField("Nombre", text: text)
}
=== solution 1(best): Introspect https://github.com/siteline/SwiftUI-Introspect
import Introspect
TextField("", text: $text)
.introspectTextField(customize: {
$0.clearButtonMode = .whileEditing
})
=== solution 2: ViewModifier
public struct ClearButton: ViewModifier {
#Binding var text: String
public init(text: Binding<String>) {
self._text = text
}
public func body(content: Content) -> some View {
HStack {
content
Spacer()
Image(systemName: "multiply.circle.fill")
.foregroundColor(.secondary)
.opacity(text == "" ? 0 : 1)
.onTapGesture { self.text = "" } // onTapGesture or plainStyle button
}
}
}
Usage:
#State private var name: String
...
Form {
Section() {
TextField("NAME", text: $name).modifier(ClearButton(text: $name))
}
}
=== solution 3: global appearance
UITextField.appearance().clearButtonMode = .whileEditing
You can add another Binding in your modifier:
#Binding var visible: Bool
then bind it to opacity of the button:
.opacity(visible ? 1 : 0)
then add another State for checking textField:
#State var showClearButton = true
And lastly update the textfield:
TextField("Some Text", text: $someBinding, onEditingChanged: { editing in
self.showClearButton = editing
}, onCommit: {
self.showClearButton = false
})
.modifier( ClearButton(text: $someBinding, visible: $showClearButton))
Not exactly what you're looking for, but this will let you show/hide the button based on the text contents:
HStack {
if !text.isEmpty {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle")
}
}
}
After initializing a new project we need to create a simple view modifier which we will apply later to our text field. The view modifier has the tasks to check for content in the text field element and display a clear button inside of it, if content is available. It also handles taps on the button and clears the content.
Let’s have a look at that view modifier:
import SwiftUI
struct TextFieldClearButton: ViewModifier {
#Binding var text: String
func body(content: Content) -> some View {
HStack {
content
if !text.isEmpty {
Button(
action: { self.text = "" },
label: {
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.opaqueSeparator))
}
)
}
}
}
}
The code itself should be self explanatory and easy to understand as there is no fancy logic included in our tasks.
We just wrap the textfield inside a HStack and add the button, if the text field is not empty. The button itself has a single action of deleting the value of the text field.
For the clear icon we use the delete.left icon from the SF Symbols 2 library by Apple, but you could also use another one or even your own custom one.
The binding of the modifier is the same as the one we apply to the text field. Without it we would not be able to check for content or clear the field itself.
Inside the ContentView.swift we now simply add a TextField element and apply our modifier to it — that’s all!
import SwiftUI
struct ContentView: View {
#State var exampleText: String = ""
var body: some View {
NavigationView {
Form {
Section {
TextField("Type in your Text here...", text: $exampleText)
.modifier(TextFieldClearButton(text: $exampleText))
.multilineTextAlignment(.leading)
}
}
.navigationTitle("Clear button example")
}
}
}
The navigation view and form inside of the ContentView are not required. You could also just add the TextField inside the body, but with a form it’s much clearer and beautiful. 🙈
And so our final result looks like this:
I found this answer from #NigelGee on "Hacking with Swift".
.onAppear {
UITextField.appearance().clearButtonMode = .whileEditing
}
It really helped me out.
Simplest solution I came up with
//
// ClearableTextField.swift
//
// Created by Fred on 21.11.22.
//
import SwiftUI
struct ClearableTextField: View {
var title: String
#Binding var text: String
init(_ title: String, text: Binding<String>) {
self.title = title
_text = text
}
var body: some View {
ZStack(alignment: .trailing) {
TextField(title, text: $text)
Image(systemName: "xmark.circle.fill")
.foregroundColor(.secondary)
.onTapGesture {
text = ""
}
}
}
}
struct ClearableTextField_Previews: PreviewProvider {
#State static var text = "some value"
static var previews: some View {
Form {
// replace TextField("Original", text: $text) with
ClearableTextField("Clear me", text: $text)
}
}
}

SwiftUI Picker onChange or equivalent?

I want to change another unrelated #State variable when a Picker gets changed, but there is no onChanged and it's not possible to put a didSet on the pickers #State. Is there another way to solve this?
Deployment target of iOS 14 or newer
Apple has provided a built in onChange extension to View, which can be used like this:
struct MyPicker: View {
#State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor, label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
.onChange(of: favoriteColor) { tag in print("Color tag: \(tag)") }
}
}
Deployment target of iOS 13 or older
struct MyPicker: View {
#State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor.onChange(colorChange), label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
}
func colorChange(_ tag: Int) {
print("Color tag: \(tag)")
}
}
Using this helper
extension Binding {
func onChange(_ handler: #escaping (Value) -> Void) -> Binding<Value> {
return Binding(
get: { self.wrappedValue },
set: { selection in
self.wrappedValue = selection
handler(selection)
})
}
}
First of all, full credit to ccwasden for the best answer. I had to modify it slightly to make it work for me, so I'm answering this question hoping someone else will find it useful as well.
Here's what I ended up with (tested on iOS 14 GM with Xcode 12 GM)
struct SwiftUIView: View {
#State private var selection = 0
var body: some View {
Picker(selection: $selection, label: Text("Some Label")) {
ForEach(0 ..< 5) {
Text("Number \($0)") }
}.onChange(of: selection) { _ in
print(selection)
}
}
}
The inclusion of the "_ in" was what I needed. Without it, I got the error "Cannot convert value of type 'Int' to expected argument type '()'"
I think this is simpler solution:
#State private var pickerIndex = 0
var yourData = ["Item 1", "Item 2", "Item 3"]
// USE this if needed to notify parent
#Binding var notifyParentOnChangeIndex: Int
var body: some View {
let pi = Binding<Int>(get: {
return self.pickerIndex
}, set: {
self.pickerIndex = $0
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// USE this if needed to notify parent
self.notifyParentOnChangeIndex = $0
})
return VStack{
Picker(selection: pi, label: Text("Yolo")) {
ForEach(self.yourData.indices) {
Text(self.yourData[$0])
}
}
.pickerStyle(WheelPickerStyle())
.padding()
}
}
I know this is a year old post, but I thought this solution might help others that stop by for a visit in need of a solution. Hope it helps someone else.
import Foundation
import SwiftUI
struct MeasurementUnitView: View {
#State var selectedIndex = unitTypes.firstIndex(of: UserDefaults.standard.string(forKey: "Unit")!)!
var userSettings: UserSettings
var body: some View {
VStack {
Spacer(minLength: 15)
Form {
Section {
Picker(selection: self.$selectedIndex, label: Text("Current UnitType")) {
ForEach(0..<unitTypes.count, id: \.self) {
Text(unitTypes[$0])
}
}.onReceive([self.selectedIndex].publisher.first()) { (value) in
self.savePick()
}
.navigationBarTitle("Change Unit Type", displayMode: .inline)
}
}
}
}
func savePick() {
if (userSettings.unit != unitTypes[selectedIndex]) {
userSettings.unit = unitTypes[selectedIndex]
}
}
}
I use a segmented picker and had a similar requirement. After trying a few things I just used an object that had both an ObservableObjectPublisher and a PassthroughSubject publisher as the selection. That let me satisfy SwiftUI and with an onReceive() I could do other stuff as well.
// Selector for the base and radix
Picker("Radix", selection: $base.value) {
Text("Dec").tag(10)
Text("Hex").tag(16)
Text("Oct").tag(8)
}
.pickerStyle(SegmentedPickerStyle())
// receiver for changes in base
.onReceive(base.publisher, perform: { self.setRadices(base: $0) })
base has both an objectWillChange and a PassthroughSubject<Int, Never> publisher imaginatively called publisher.
class Observable<T>: ObservableObject, Identifiable {
let id = UUID()
let objectWillChange = ObservableObjectPublisher()
let publisher = PassthroughSubject<T, Never>()
var value: T {
willSet { objectWillChange.send() }
didSet { publisher.send(value) }
}
init(_ initValue: T) { self.value = initValue }
}
typealias ObservableInt = Observable<Int>
Defining objectWillChange isn't strictly necessary but when I wrote that I liked to remind myself that it was there.
For people that have to support both iOS 13 and 14, I added an extension which works for both. Don't forget to import Combine.
Extension View {
#ViewBuilder func onChangeBackwardsCompatible<T: Equatable>(of value: T, perform completion: #escaping (T) -> Void) -> some View {
if #available(iOS 14.0, *) {
self.onChange(of: value, perform: completion)
} else {
self.onReceive([value].publisher.first()) { (value) in
completion(value)
}
}
}
}
Usage:
Picker(selection: $selectedIndex, label: Text("Color")) {
Text("Red").tag(0)
Text("Blue").tag(1)
}.onChangeBackwardsCompatible(of: selectedIndex) { (newIndex) in
print("Do something with \(newIndex)")
}
Important note: If you are changing a published property inside an observed object within your completion block, this solution will cause an infinite loop in iOS 13. However, it is easily fixed by adding a check, something like this:
.onChangeBackwardsCompatible(of: showSheet, perform: { (shouldShowSheet) in
if shouldShowSheet {
self.router.currentSheet = .chosenSheet
showSheet = false
}
})
SwiftUI 1 & 2
Use onReceive and Just:
import Combine
import SwiftUI
struct ContentView: View {
#State private var selection = 0
var body: some View {
Picker("Some Label", selection: $selection) {
ForEach(0 ..< 5, id: \.self) {
Text("Number \($0)")
}
}
.onReceive(Just(selection)) {
print("Selected: \($0)")
}
}
}
iOS 14 and CoreData entities with relationships
I ran into this issue while trying to bind to a CoreData entity and found that the following works:
Picker("Level", selection: $contact.level) {
ForEach(levels) { (level: Level?) in
HStack {
Circle().fill(Color.green)
.frame(width: 8, height: 8)
Text("\(level?.name ?? "Unassigned")")
}
.tag(level)
}
}
.onChange(of: contact.level) { _ in savecontact() }
Where "contact" is an entity with a relationship to "level".
The Contact class is an #ObservedObject var contact: Contact
saveContact is a do-catch function to try viewContext.save()...
The very important issue : we must pass something to "tag" modifier of Picker item view (inside ForEach) to let it "identify" items and trigger selection change event. And the value we passed will return to Binding variable with "selection" of Picker.
For example :
Picker(selection: $selected, label: Text("")){
ForEach(data){item in //data's item type must conform Identifiable
HStack{
//item view
}
.tag(item.property)
}
}
.onChange(of: selected, perform: { value in
//handle value of selected here (selected = item.property when user change selection)
})