How to add a NavigationLink inline between 2 Text - swiftui

I want to have a NavigationLink in between 2 Text elements in SwiftUI. Concatenating them together isn't working since Swift apparently only accepts concatenating elements of the same type.
Example
Expected outcome: Read our Terms & Conditions to learn more.
What I've tried:
Text("Read our ") + NavigationLink("Terms & Conditions") { TermsPage() } + Text(" to learn more.")
The error I got
Cannot convert value of type 'NavigationLink<Label, TermsPage>' to expected argument type 'Text'
I know that you can embed links in Markdown format
Text("Read our [Terms & Conditions](example.com/terms) to learn more.")
But that will open a webpage which isn't the desired behavior. I have a page inside the app that I want to navigate into. I just need the link to be inline with the text.

you could try something simple like this approach using a HStack:
#State var showTerms = false
//....
NavigationStack {
HStack {
Text("Read our")
Text("Terms & Conditions").foregroundColor(.blue)
.onTapGesture { showTerms = true }
Text("to learn more.")
}.fixedSize()
NavigationLink("", destination: TermsPage(), isActive: $showTerms)
}

Related

ForEach: ID parameter and closure return type

So, I'm going through the SwiftUI documentation to get familiar. I was working on a grid sample app. It has the following code:
ForEach(allColors, id: \.description) { color in
Button {
selectedColor = color
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(color)
}
.buttonStyle(.plain)
}
It didn't occur to me first that ForEach is actually a struct, I thought it's a variation of the for in loop at first so I'm quite new at this. Then I checked the documentation.
When I read the documentation and some google articles for the ForEach struct, I didn't understand two points in the code:
So we are initializing the foreach struct with an array of colors. For the the ID why did they use .\description instead of .self?
Second is using color in. Since foreach is a struct and the paranthesis is the initializtion parameters this looks like the return type of a closure but why would we return individual colors to foreach? I thought the return is a collection of views or controls like button and label. This is like var anInteger: Int = 1 for example. What type does ForEach accept as a result of the closure? Or am I reading this all wrong?
So we are initializing the foreach struct with an array of colors. For the the ID why did they use .\description instead of .self?
It depends on the type of allColors. What you should have in mind that id here is expected to be stable. The documentation states:
It’s important that the id of a data element doesn’t change unless you replace the data element with a new data element that has a new identity. If the id of a data element changes, the content view generated from that data element loses any current state and animations.
So for example if colors are reference types (which are identifiable) and you swap one object with an identical one (in terms of field values), the identity will change, whereas description wouldn't (for the purposes of this example - just assuming intentions of code I have no access to).
Edit: Also note that in this specific example allColors appears to be a list of Color, which is not identifiable. So that's the reason behind the custom id keyPath.
Regarding your second point, note that the trailing closure is also an initialization parameter. To see this clearly we could use the "non-sugared" version:
ForEach(allColors, id: \.description, content: { color in
Button {
selectedColor = color
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(color)
}
.buttonStyle(.plain)
})
where content is a closure (an anonymous function) that gets passed an element of the collection and returns some View.
So the idea is something like this: "Give me an collection of identifiable elements and I will call a function for each of these elements expecting from you to return me some View".
I hope that this makes (some) sense.
Additional remarks regarding some of the comments:
It appears to me that the main source of confusion is the closure itself. So let's try something else. Let's write the same code without a closure:
ForEach's init has this signature:
init(_ data: Data, id: KeyPath<Data.Element, ID>, content: #escaping (Data.Element) -> Content)
Now, the content translates to:
A function with one parameter of type Data.Element, which in our case is inferred from the data so it is a Color. The function's return type is Content which is a view builder that produces some View
so our final code, which is equivalent to the first one, could look like this:
struct MyView: View {
let allColors: [Color] = [.red, .green, .blue]
#State private var selectedColor: Color?
var body: some View {
List {
ForEach(allColors, id: \.description, content: colorView)
}
}
#ViewBuilder
func colorView(color: Color) -> some View {
Button {
selectedColor = color
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(color)
}
.buttonStyle(.plain)
}
}
I hope that this could help to clarify things a little bit better.

SwiftUI Runtime Error from if condition { Text() } in watchOS

As far as I read about conditional Views this code should work. But it doesn't.
struct Consts {
static let myCondition = false //no difference if true or false
}
struct ContentView: View {
#State var toggle: Bool = false
var body: some View {
List() {
Text("Short Text is in first line")
Text("Second Line Text is a little longer but not much")
if Consts.myCondition {
Text("This is a conditional text. As in: when the user hasn't purchased the option he / she don't need a hint how to use this feature.")
// } else {
// Text("else doesn't help either.")
}
Toggle("I also have a toggle but it has nothing to do with this.", isOn: $toggle)
Text("Here we have a longer Text. Dont know what to type any more.")
Text("More text which is longer than a few lines.")
Text("Not so long Text")
}
.navigationTitle("Hints & Settings")
}
}
It compiles without warnings or errors. It loads up and displays fine, on simulator and on device. But every time I scroll the List upwards from the end, as soon as this if condition { Text() } should become visible the app crashes with
Fatal error: file SwiftUI, line 0
2021-03-07 06:36:26.548126+0100 WTFif WatchKit Extension[23163:641812] Fatal error: file SwiftUI, line 0
This is not limited to watchOS. It reproduces the same way in iOS, just the Texts have to be longer so that the if condition { Text() } becomes invisible when scrolling.
I have worked around the error with an array, conditional ranges and two ForEach() blocks.
struct ContentView: View {
let myHints = ["Short Text is in first line",
"Second Line Text is a little longer but not much",
"This is a conditional text. As in: when the user hasn't purchased the option he / she don't need to hint how to use this feature.",
"Here we have a longer Text. Dont know what to type any more.",
"More text which is longer than a few lines.",
"Not so long Text"]
var myUpperRange: ClosedRange<Int> {
if Consts.myCondition {
return 0...1
} else {
return 0...2
}
}
var myLowerRange: ClosedRange<Int> {
return 3...5
}
#State var toggle: Bool = false
var body: some View {
List() {
ForEach (myUpperRange, id: \.self) { i in
Text(myHints[i])
}
Toggle("I also have a toggle but it has nothing to do with this.", isOn: $toggle)
ForEach (myLowerRange, id: \.self) { i in
Text(myHints[i])
}
}
.navigationTitle("Hints & Settings")
}
}
My question basically is: am I not getting it or did I find a bug in Xcode / SwiftUI? Should my code above work? What could I have done different to make it work with the simple list of Texts?
Background: I also have a TabView with an if condition { MyTab() } which works without crashing. Do I have to worry that this might crash in the same way? Should I work around this as well before shipping?
PS: I am using Xcode 12.4 (12D4e)
Apparently this has been fixed by iOS 15 Beta 4: on my test device I had the error prior to the Beta 4 update, using a test app compiled with Xcode 13 Beta 3. After the update to iOS 15 Beta 4 the error is gone.
So I think it’s reasonable to say that the update to iOS 15 Beta 4 did fix the 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)}))

SwiftUI: how to remove spacing around Text()?

As example I have 3 properties:
var path1FilePath:String = "Src/"
var path2FileName: String = "filename"
var path3Extension: String = ".jpg"
I need to display them with the following way:
HStack {
Text(status.path1FilePath)
Text(status.path2FileName).bold()
Text(status.path3Extension)
}
problem is spacing between Text() views. How to remove them?
SwiftUI allows us to combine strings together like Text("Hello ") + Text("World!"), so you can do the same here:
Text(path1FilePath)
+ Text(path2FileName)
+ Text(path3Extension)
Alternatively, if you still want or need to use an HStack, just use HStack(spacing: 0) and you'll get the same result.
There are 2 ways:
Solution 1:
Text(path1FilePath)
+ Text(path2FileName)
+ Text(path3Extension)
but in this way you cannot apply modifiers =(
Solution 2:
HStack (spacing: 0) {
Text(path1FilePath)
Text(path2FileName)
.bold()
Text(path3Extension)
}
.someTextModifier()

SwiftUI PickerView with working CallBack HOW?

Ok -
I want a picker view to pick one operator: "=","<",">"
This operator will be sent as a binding:
#Binding var op:String
My Picker:
Picker(selection: binding, label: Text("Query Type")) {
ForEach(0..<self.operators.count) { index in
Text(self.operators[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
.padding()
Now My Binding with CallBack:
let binding = Binding<Int>(
get: {
return self.pickerSelection
},
set: {
//pickerSelection = $0
print("SETTTING: \($0)")
self.op = self.operators[self.pickerSelection]
self.queryCallback()
})
So, I can set the pickers perfectly. BUT, when I go back to edit my data, the picker never can choose the existing bound operator, say "<"
I put in the init an:
pickerSelection = operators.firstIndex(opValue)
However this will just start an infinite loop as pickerSelection is a #State variable
Anyone have a solution?
This is a method that works. It uses Combine to make an observable one can use to trigger the needed events. Also I see how useful Combine is with SwiftUI
https://stackoverflow.com/a/57519105/810300