Problem passing value with SwiftUI List(), ForEach(), .onTapGesture WatchOS6 - swiftui

I am trying to use the .onTapGesture() to call a method on the tapped view and pass some kind of ID or any other form of identifier, but i dont know how to.
i have a view ListView that i create from data that i get from my array trackPreviewList: [TrackpreviewList].
The view looks like this:
struct ListView: View {
#ObservedObject var model: PlaybackService
#State var list = PlaybackService.sharedInstance.trackPreviewList
var body: some View {
List {
Section(header: Text("Songs")) {
ForEach(model.trackPreviewList!, id: \.id) {
Text($0.artistName + ": " + $0.name)
}
.onDelete(perform: deleteTrack)
.onTapGesture(perform: skipTo)
}
}
}
I am very new to SwiftUI and coding in general, but i think the right approach is to say cell(view?) 2 was tapped, therefore pass 2 to the method func playerSkipTo(skipToIndex: Int) I have tried to hardcode the value 2 like this and it works with the hardcoded value:
Though i am clueless when it comes to how to pass some kind of identifier, i think my problem is that i am trying to access the object outside of the scope where the method "knows about it".
I have tried to use the $0.name variable just to see if i could pass anything but i get this error Contextual closure type '() -> Void' expects 0 arguments, but 1 was used in closure body which i think is because the .onTapGesture() method does not have a closure?.
I have tried to use the same logic as the .onDelete() method using IndexSet like this:
The method that i want to pass the value to in the end looks like this:
func playerSkipTo(skipToIndex: Int) {
if skipToIndex < ((trackPreviewList!.underestimatedCount)-1) {
_ = firstly {
TracklistProvider.sharedInstance.getPlaybackURL(trackPreview: trackPreviewList![skipToIndex])
}.map { playbackoptions in
self.createPlayerItemFromLink(assetURL: (playbackoptions?.playbackUrl)!)
}.done{
self.replacePlayerItem()
}
} else {
print("The index \(skipToIndex) is out of bounds")
}
}
I am aware that i will need to change the playerSkipTo() method to take in IndexSet also, which i have also tried to play around with, trying to cast it to Int and such.
Any help or pointers are greatly appreciated!
Heres the playerSkipTo method:
if skipToIndex < ((trackPreviewList!.underestimatedCount)-1) {
_ = firstly {
TracklistProvider.sharedInstance.getPlaybackURL(trackPreview: trackPreviewList![skipToIndex])
}.map { playbackoptions in
self.createPlayerItemFromLink(assetURL: (playbackoptions?.playbackUrl)!)
}.done{
self.replacePlayerItem()
}
} else {
print("The index \(skipToIndex) is out of bounds")
}
}

Instead of skipping to an index, skip to a specific track object:
func playerSkipTo(_ playbackoptions: PlaybackOptions) {
// ...
}
Then change your View code to call this function on the Text instead of the ForEach:
List {
Section(header: Text("Songs")) {
ForEach(model.trackPreviewList!, id: \.id) {
Text($0.artistName + ": " + $0.name)
.onTapGesture {
self.playerSkipTo($0)
}
}
}
}

You can use a button on top of a ZStack:
List{
ForEach(appData.assets) { asset in
ZStack {
HStack{
VStack(alignment: .leading) {
Text(asset.value)
.font(.headline)
}
Spacer()
}
Button(action: {assetPressed(id: asset.id)}, label: {
Text("")
})
}
}
.onDelete(perform: deleteAsset)
}

Related

How to execute non-view code inside a SwiftUI view

I have been struggling with this over and over again, so I think I'm missing something. I need to do math, make a setting, assign a value or any of a host of simple operations in reaction to some user action, such as the example shown here, and SwiftUI is wanting a View where I don't need a view. There's got to be a way around the ViewBuilder's rules. I kind of worked around this by creating an unnecessary view and executing the code I need inside the View's init(), but that seems terribly awkward.
import SwiftUI
struct ContentView: View
{
#State var showStuff = false
var body: some View
{
VStack
{
Toggle(isOn: $showStuff)
{
Text("Label")
}
if showStuff
{
UserDefaults.standard.set(true, forKey: "Something")
}
}
}
}
Way 1 (best):
struct ExecuteCode : View {
init( _ codeToExec: () -> () ) {
codeToExec()
}
var body: some View {
return EmptyView()
}
}
usage:
HStack {
ExecuteCode {
print("SomeView1 was re-drawn!")
}
SomeView1()
}
Way 2:
( my first way is better - you're able to write only simple code here )
Code with let _ = works inside of View!
HStack {
let _ = print("SomeView1 was re-drawn!")
SomeView1()
}
Way 3:
( my first way is better - too difficult code structure; But code doings the same )
HStack {
// here is the magic
{ () -> SomeView1() in
// here is code to execute
print("SomeView1 was re-drawn!")
// here is the magic
return SomeView1()
}
}
Views are actually so-called Function Builders, and the contents of the view body are used as arguments to to the buildBlock function, as mentioned by #Asperi.
An alternative solution if you must run code inside this context is using a closure that returns the desired view:
VStack {
// ... some views ...
{ () -> Text in
// ... any code ...
return Text("some view") }()
// ... some views ...
}
In SwiftUI 2.0, there's a new ViewModifier onChange(of:perform:), that allows you to react to changes in values.
But you can create something similar to that with a neat trick (I forgot where I saw it, so unfortunately I can't leave proper attribution), by extending a Binding with onChange method:
extension Binding {
func onChange(perform action: #escaping (Value, Value) -> Void) -> Self {
.init(
get: { self.wrappedValue },
set: { newValue in
let oldValue = self.wrappedValue
DispatchQueue.main.async { action(newValue, oldValue) }
self.wrappedValue = newValue
})
}
}
You can use it like so:
Toggle(isOn: $showStuff.onChange(perform: { (new, old) in
if new {
UserDefaults.standard.set(true, forKey: "Something")
}
}))
You cannot do what you try to do, because actually every view block inside body is a ViewBuidler.buildBlock function arguments. Ie. you are in function arguments space. I hope you would not expect that expression like
foo(Toggle(), if showStuff { ... } )
would work (assuming foo is func foo(args: View...). But this is what you try to do in body.
So expressions in SwiftUI have to be out of ViewBuilder block (with some exceptions which ViewBuilder itself supports for views).
Here is a solution for your case:
SwiftUI 2.0
struct ContentView: View {
#AppStorage("Something") var showStuff = false
var body: some View {
VStack {
Toggle(isOn: $showStuff) {
Text("Label")
}
}
}
}
SwiftUI 1.0
Find in already solved SwiftUI toggle switches
Note: View.body (excluding some action modifiers) is equivalent of UIView.draw(_ rect:)... you don't store UserDefaults in draw(_ rect:), do you?

Is there a way to conditionally flip the order of an HStack's contents?

I'm using an HStack to layout some elements in my view hierarchy. I'd love to be able to conditionally flip the order of the elements.
HStack {
Text("Hello")
Text("World")
}
The idea is that this will either be layouted as "Hello World" or "World Hello" depending on my view's state.
HStack itself doesn't provide any functionality for this, but it also appears to be pretty much impossible trying to pull this out of the view itself attempting to use other ViewBuilders, ForEach-based approaches, etc.
The only way I can resolve this is by actually specifying both layouts entirely, which is what I'm trying to avoid.
let isFlipped: Bool
HStack {
if isFlipped {
Text("World")
Text("Hello")
} else {
Text("Hello")
Text("World")
}
}
Here is possible generic approach for any pair of views in any container base on using ViewBuilder.
Tested with Xcode 12
struct TestHStackFlip: View {
#State private var flipped = false
var body: some View {
VStack {
HStack {
FlipGroup(if: flipped) {
Text("Text1")
Text("Text2")
}
}.animation(.default) // animatable
Divider()
Button("Flip") { self.flipped.toggle() }
}
}
}
#ViewBuilder
func FlipGroup<V1: View, V2: View>(if value: Bool,
#ViewBuilder _ content: #escaping () -> TupleView<(V1, V2)>) -> some View {
let pair = content()
if value {
TupleView((pair.value.1, pair.value.0))
} else {
TupleView((pair.value.0, pair.value.1))
}
}

Swift Ui - Different View(Layout) for first item of a List

I am relatively new to Swift. I want to accomplish that the first item of the weatherList is displayed with a different View.
Unfortunately I get this error in those 2 Lines (VStack and at ForEach-Line). If i put away the if i == 0 then and just use for normal layout for all listitems there is no error.
ERROR: Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
Here is my code:
var body: some View {
NavigationView {
VStack { // here first error
if self.model.error != nil {
Text(self.model.error!)
}
ForEach(0..<self.model.weatherList.count, id: \.self) { i in // here second error
if i == 0 {
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
}
}
else{
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
}
}
}
}
.navigationBarTitle(Text("Weather in " + UserSettings.instance.locationSetting))
.navigationBarItems(trailing:
Button(action: {
self.model.reload()
}) {
Image(systemName: "arrow.clockwise")
}
.disabled(model.reloading)
)
}
}
Would appreciate each advice. Thanks.
I haven't tries this, but what if you wrap the NavigationLink like this:
if i == 0 {
AnyView(
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
})
} else {
AnyView(
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
})
}
It's a little tricky to know for sure without having the rest of your code to make this compile, but I mocked it up locally and it looks like all you need to change is:
ForEach(0..<self.model.weatherList.count, id: \.self) { i in
to
ForEach(0..<self.model.weatherList.count) { i in // remove the id: argument
Explanation:
If you're using a range to iterate through your array, you don't use the id: argument. The range is automatically identifiable. The ForEach(_:id:content:) initialiser is used for cases where you might say
ForEach(self.model.weatherlist, id: \.self) { model in
SomeView(model: model)
}
Does that compile for you?
Edit: The example you've given looks like it will do the same thing whether the index is 0 or not. Is there a reason why it doesn't work if you try:
ForEach(0..<self.model.weatherList.count) { i in
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
}
}

Conditionally Text in SwiftUI depending on Array value

I want make placeholder custom style so i try to use the method of Mojtaba Hosseini in SwiftUI. How to change the placeholder color of the TextField?
if text.isEmpty {
Text("Placeholder")
.foregroundColor(.red)
}
but in my case, I use a foreach with a Array for make a list of Textfield and Display or not the Text for simulate the custom placeholder.
ForEach(self.ListeEquip.indices, id: \.self) { item in
ForEach(self.ListeJoueurs[item].indices, id: \.self){idx in
// if self.ListeJoueurs[O][O] work
if self.ListeJoueurs[item][index].isEmpty {
Text("Placeholder")
.foregroundColor(.red)
}
}
}
How I can use dynamic conditional with a foreach ?
Now I have a another problem :
i have this code :
struct EquipView: View {
#State var ListeJoueurs = [
["saoul", "Remi"],
["Paul", "Kevin"]
]
#State var ListeEquip:[String] = [
"Rocket", "sayans"
]
var body: some View {
VStack { // Added this
ForEach(self.ListeEquip.indices) { item in
BulleEquip(EquipName: item, ListeJoueurs: self.$ListeJoueurs, ListeEquip: self.$ListeEquip)
}
}
}
}
struct BulleEquip: View {
var EquipName = 0
#Binding var ListeJoueurs :[[String]]
#Binding var ListeEquip :[String]
var body: some View {
VStack{
VStack{
Text("Équipe \(EquipName+1)")
}
VStack { // Added this
ForEach(self.ListeJoueurs[EquipName].indices) { index in
ListeJoueurView(EquipNamed: self.EquipName, JoueurIndex: index, ListeJoueurs: self.$ListeJoueurs, ListeEquip: self.$ListeEquip)
}
HStack{
Button(action: {
self.ListeJoueurs[self.EquipName].append("") //problem here
}){
Text("button")
}
}
}
}
}
}
struct ListeJoueurView: View {
var EquipNamed = 0
var JoueurIndex = 0
#Binding var ListeJoueurs :[[String]]
#Binding var ListeEquip :[String]
var body: some View {
HStack{
Text("Joueur \(JoueurIndex+1)")
}
}
}
I can run the App but I have this error in console when I click the button :
ForEach, Int, ListeJoueurView> count (3) != its initial count (2). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
Can someone enlighten me?
TL;DR
You need a VStack, HStack, List, etc outside each ForEach.
Updated
For the second part of your question, you need to change your ForEach to include the id parameter:
ForEach(self.ListeJoueurs[EquipName].indices, id: \.self)
If the data is not constant and the number of elements may change, you need to include the id: \.self so SwiftUI knows where to insert the new views.
Example
Here's some example code that demonstrates a working nested ForEach. I made up a data model that matches how you were trying to call it.
struct ContentView: View {
// You can ignore these, since you have your own data model
var ListeEquip: [Int] = Array(1...3)
var ListeJoueurs: [[String]] = []
// Just some random data strings, some of which are empty
init() {
ListeJoueurs = (1...4).map { _ in (1...4).map { _ in Bool.random() ? "Text" : "" } }
}
var body: some View {
VStack { // Added this
ForEach(self.ListeEquip.indices, id: \.self) { item in
VStack { // Added this
ForEach(self.ListeJoueurs[item].indices, id: \.self) { index in
if self.ListeJoueurs[item][index].isEmpty { // If string is blank
Text("Placeholder")
.foregroundColor(.red)
} else { // If string is not blank
Text(self.ListeJoueurs[item][index])
}
}
}.border(Color.black)
}
}
}
}
Explanation
Here's what Apple's documentation says about ForEach:
A structure that computes views on demand from an underlying collection of of [sic] identified data.
So something like
ForEach(0..2, id: \.self) { number in
Text(number.description)
}
is really just shorthand for
Text("0")
Text("1")
Text("2")
So your ForEach is making a bunch of views, but this syntax for declaring views is only valid inside a View like VStack, HStack, List, Group, etc. The technical reason is because these views have an init that looks like
init(..., #ViewBuilder content: () -> Content)
and that #ViewBuilder does some magic that allows this unique syntax.

How to print() to Xcode console in SwiftUI?

So I tried to put a print statement while debugging in a SwiftUI View.
print("landmark: \(landmark)")
In the following body.
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Favorite only")
}
ForEach(landmarkData) { landmark in
print("landmark: \(landmark)")
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
Compiler errors out:
So, what is the proper way to print to console in SwiftUI?
EDIT:
I made Landmark conform to CustomStringConvertible:
struct Landmark: Hashable, Codable, Identifiable, CustomStringConvertible {
var description: String { name+"\(id)" }
var id: Int
var name: String
.....
I still get the "String is not convertible to any" error. Should it work now?
You can easily add a print statement anywhere in a function builder by simply storing its return value in a wildcard, effectively ignoring it:
let _ = print("hi!")
No setup or other verbosity needed!
Why does this work while a regular print() doesn't?
The way SwiftUI's #ViewBuilder (and result builders in general) is that they consume any values in a closure that aren't used otherwise (e.g. if you just have 42 on its own line). The print function returns Void (nothing), which the builder would have to build into a view, so it fails. By instead assigning it to a variable (in this case _, basically a variable that you can never access), the Void is never offered to the view builder in the first place.
You could argue the builder should simply accept and ignore Void values, but the idea is that your builder closures should not have side effects (I'd remove print statements after finishing debugging too)—you should not rely on these closures being called at certain times.
Here's a helper Print( ... ) View that acts like a print( ... ) function but within a View
Put this in any of your view files
extension View {
func Print(_ vars: Any...) -> some View {
for v in vars { print(v) }
return EmptyView()
}
}
and use inside of body like so
Print("Here I am", varOne, varTwo ...)
or inside a ForEach {} like so
self.Print("Inside ForEach", varOne, varTwo ...)
Note: you might need to put Print() into a Group {} when combining with existing views
Try right-clicking on the live preview play button and selecting 'Debug Preview from the popup
You can print in the body structure but to do so you have to explicitly return the view you want to render. The body property inside a View is just a computed property like any other in Swift that implicitly returns the view. And just like any other computed property, you can perform operations inside the computed property as long as a value is explicitly returned. For example, this will throw an error when you try to print because there is no explicit return:
struct SomeView: View {
#State var isOpen = false
var body: some View {
print(isOpen) // error thrown here
VStack {
// other view code
}
}
}
But if we explicitly return the view we want then it will work e.g.
struct SomeView: View {
#State var isOpen = false
var body: some View {
print(isOpen) // this ok because we explicitly returned the view below
// Notice the added 'return' below
return VStack {
// other view code
}
}
}
The above will work well if you're looking to view how state or environment objects are changing before returning your view, but if you want to print something deeper down within the view you are trying to return, then I would go with #Rok Krulec answer.
It is possible to use print() remembering that all SwiftUI View content are (a) implicit closures and (b) it is highly recommended to decompose views as much as possible to have simple structure, so it might look like the following...
struct Model: Identifiable {
let value: String
var id: String {
value
}
init (_ value: String) {
self.value = value
}
}
struct TestView: View {
#State var showFavoritesOnly = false
#State var listData: [Model] = [Model("one"), Model("two"), Model("three")]
var body: some View {
NavigationView {
List {
Toggle(isOn: $showFavoritesOnly) {
Text("Favorite only")
}
ForEach(listData) { data in
self.rowView(data: data)
}
}
}
}
private func rowView(data: Model) -> some View {
#if DEBUG
print(">> \(data.value)")
#endif
return NavigationLink(destination: Text("Details")) {
Text("Go next from \(data.value)")
}
}
}
... and right clicking in Preview to select run as Debug Preview we get:
2019-10-31 14:28:03.467635+0200 Test[65344:11155167] [Agent] Received connection, creating agent
2019-10-31 14:28:04.472314+0200 Test[65344:11155168] [Agent] Received display message
>> one
>> two
>> three
You can declare a printing() method that includes print() and returns EmptyView struct.
struct ContentView: View {
#State private var offset = CGSize.zero
func printing(_ items: Any...) -> some View {
let _ = print(items)
return EmptyView()
}
var body: some View {
#if DEBUG
printing(offset) // prints [(0.0, 0.0)]
#endif
ZStack {
Text("Hello")
}
}
}
The safest and easiest way to print while debugging in a SwiftUI View.
extension View {
func Print(_ item: Any) -> some View {
#if DEBUG
print(item)
#endif
return self
}
}
Usage Example:
struct ContentView: View {
var body: some View {
VStack {
ForEach((1...5), id: \.self) { number in
Text("\(number)")
.Print(number)
}
}
}
}
Console output:
1
2
3
4
5
It can be generalized to:
extension View {
func Perform(_ block: () -> Void) -> some View {
block()
return EmptyView()
}
}
So in your example:
ForEach(landmarkData) { landmark in
Perform { print("landmark: \(landmark)") }
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
Here you go. It will just work like simple print but inside a view.
func printv( _ data : Any)-> EmptyView{
print(data)
return EmptyView()
}
and use it like that
struct ContentView: View {
var body: some View {
VStack() {
Text("hello To SwiftUI")
printv("its easy to code in SwiftUI")
Text("And Good to have you here")
}
}
}
The following extension on View is as intuitive as print because it's made to replicate the default print(_:separator:terminator:) function signature & behavior.
extension View {
func printUI(_ args: Any..., separator: String = " ", terminator: String = "\n") -> EmptyView {
let output = args.map(String.init(describing:)).joined(separator: separator)
print(output, terminator: terminator)
return EmptyView()
}
}
Usage Example:
struct ContentView: View {
var body: some View {
VStack {
printUI("ContentView", "1")
printUI("ContentView", "2", separator: ", ", terminator: "\n.\n.\n")
printUI("ContentView", "3", separator: "; ")
Text("Hello, World!")
}
}
}
Console Output:
ContentView 1
ContentView, 2
.
.
ContentView; 3
EDIT: Debug Preview is no longer supported in the latest versions of Xcode.
Very easy way to debug your Preview:
Open your Swift project in Xcode 11.
Right-click (or Control-click) on the Live Preview button in the bottom right corner of the preview.
Select Debug Preview.
How to debug your SwiftUI previews in Xcode
// Try this, add a 'return' on a view then the 'print' can stay alive in.
struct ContentView: View {
var num: Int = 1
var body: some View {
print(num)
return Text("hello")
}
}
You can't because you're in a computed property. You need for example a button and in the action you define the print. Or work with breakpoints
You can not print in body structure i.e. a structure which is some view type.For print you need to make function out of body structure and call it using button or something else.
This should work
if true {
print(aVar, "xx")
}
return ZStack {
...
}