SwiftUI: why can't I map views in a previews in PreviewProvider? - swiftui

I know that I can display several previews doing this:
struct TestView: View {
let number: Int
var body: some View {
ZStack {
Color.orange
Text("\(number)")
.fontWeight(.heavy)
.font(.system(size: 56.0))
}
.frame(width: 100, height: 100, alignment: .center)
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView(number: 3)
TestView(number: 5)
}
}
But why can't I, instead, write this instead?
static var previews: some View {
[3, 5].map { TestView(number: $0) }
}
(I'm getting this error: Return type of static property 'previews' requires that '[TestView]' conform to 'View')
Thank you for your help

[TestView], also known as Array<TestView>, does not conform to View.
SwiftUI's solution is the ForEach view:
static var previews: some View {
ForEach([3, 5], id: \.self) {
TestView(number: $0)
}
}
Note that this assumes every element of the array is unique (which is true in this example).

Because it's return type is array of TestView and arrays not conform the View.
You can do it like this:
static var previews: some View {
Group {
let numbers = [3, 5]
ForEach(numbers, id: \.self) { number in
TestView(number: number)
}
}
}

Related

Make updated Int stay the same in a new struct

I have a button that adds + 1 to an Int. I want to be able to access the value of the Int in another struct. Right now I am available to access the Int, but it starts from zero, not showing the amount of + 1 added after pressing the button in the previous struct.
Is there a way to "remember" the Ints that are added?
import SwiftUI
class NumberOrder: ObservableObject {
#Published var Number: Int = 0
}
struct Choose: View {
#State var buttonClick: Bool = false
#StateObject var numbeorder = NumberOrder()
var body: some View {
ZStack {
Color.black
VStack{
Text("\(numbeorder.Number)")
.font(.system(size: 50))
Button {
buttonClick.toggle()
} label: {
Image(systemName: "arrow.left.circle")
.foregroundColor(.white)
.font(.system(size: 50))
}
Button {
self.numbeorder.Number += 1
} label: {
Text("ADD ONE")
.font(.system(size: 40, weight: .medium))
}
}
if buttonClick == true {
Feeling1()
}
}
}
}
struct Choose_Previews: PreviewProvider {
static var previews: some View {
Choose()
}
}
struct Feeling1: View {
#State var goBack0: Bool = false
#StateObject var numbeorder = NumberOrder()
var body: some View {
ZStack {
Color.purple
Text("\(numbeorder.Number)")
.font(.system(size: 50))
Button {
goBack0.toggle()
} label: {
Image(systemName: "arrow.left.circle")
.foregroundColor(.black)
.font(.system(size: 50))
}.buttonStyle(.plain)
.padding(.init(top: -250, leading: 0, bottom: 0, trailing: 150))
if goBack0 == true {
Choose()
}
}
}
}
You have 2 different #StateObject var numbeorder = NumberOrder() that have no relations to each other.
Have a look at this link, it gives you some good examples of how to manage data in your app :
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
Note, you should use the common practice to name your vars in lower case, particularly Number,
in NumberOrder, as Swift has already a Number.
So, in Feeling1, use #ObservedObject var numbeorder: NumberOrder
and use Feeling1(numbeorder: numbeorder) in Choose
Use let for read access or #Binding var for write access, e.g.
struct Feeling1: View {
// let goBack0: Bool // if only need read access
#Binding var goBack0: Bool // if need read and write access
Note: don't use #StateObject unless you need a reference type instead of an #State struct which doesn't appear to be the case here, e.g.
struct NumberOrder {
var number: Int = 0
// mutating func someLogic() { }
}
#State var order = NumberOrder()

Passing Data to Second View in SwiftUI

I am trying to pass data (incomeAmount) from the First View to the Second View in my SwiftUI app, but I need to declare the BindingString. What does it mean that I need to declare the BindingString?
View 1
struct ContentView: View {
#State var incomeAmount = ""
var body: some View {
NavigationView {
VStack {
TextField("Your income amount", text: $incomeAmount)
.frame(width: 300)
.padding(.bottom, 30)
NavigationLink(destination: NewView()){
Text("Continue")
.frame(width: 300, height: 50, alignment: .center)
.background(Color.black)
.cornerRadius(130)
.foregroundColor(.white)
.padding(.bottom, 30)
}
Text("$\(incomeAmount)")
}.navigationTitle("First View")
}
}
}
View 2
struct NewView: View {
#Binding var incomeAmount: String
var body: some View {
NavigationView {
VStack {
Text("$\(incomeAmount)")
}
}.navigationTitle("Second View")
}
}
struct NewView_Previews: PreviewProvider {
static var previews: some View {
NewView(incomeAmount: <#Binding<String>#>)
}
}
To pass the incomeAmount to the second view, you have to do this:
NavigationLink(destination: NewView(incomeAmount: $incomeAmount)) { ... }
Also, you need to provide it to:
struct NewView_Previews: PreviewProvider {
static var previews: some View {
NewView(incomeAmount: .constant("something here"))
}
}
Because in PreviewProvider, NewView expect to have a Binding<String> passed in. So you need to provide it.

Disable a segment in a SwiftUI SegmentedPickerStyle Picker?

My SwiftUI app has a segmented Picker and I want to be able to disable one or more options depending on availability of options retrieved from a network call. The View code looks something like:
#State private var profileMetricSelection: Int = 0
private var profileMetrics: [RVStreamMetric] = [.speed, .heartRate, .cadence, .power, .altitude]
#State private var metricDisabled = [true, true, true, true, true]
var body: some View {
VStack(alignment: .leading, spacing: 2.0) {
...(some views)...
Picker(selection: $profileMetricSelection, label: Text("")) {
ForEach(0 ..< profileMetrics.count) { index in
Text(self.profileMetrics[index].shortName).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
...(some more views)...
}
}
What I want to be able to do is modify the metricDisabled array based on network data so the view redraws enabling the relevant segments. In UIKit this can be done by calls to setEnabled(_:forSegmentAt:) on the UISegmentedControl but I can't find a way of doing this with the SwiftUI Picker
I know I can resort to wrapping a UISegmentedControl in a UIViewRepresentable but before that I just wanted to check I'm not missing something...
you can use this simple trick
import SwiftUI
struct ContentView: View {
#State var selection = 0
let data = [1, 2, 3, 4, 5]
let disabled = [2, 3] // at index 2, 3
var body: some View {
let binding = Binding<Int>(get: {
self.selection
}) { (i) in
if self.disabled.contains(i) {} else {
self.selection = i
}
}
return VStack {
Picker(selection: binding, label: Text("label")) {
ForEach(0 ..< data.count) { (i) in
Text("\(self.data[i])")
}
}.pickerStyle(SegmentedPickerStyle())
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Maybe something like
ForEach(0 ..< data.count) { (i) in
if !self.disabled.contains(i) {
Text("\(self.data[i])")
} else {
Spacer()
}
}
could help to visualize it better
NOTES (based on the discussion)
From user perspective, the Picker is one control, which could be in disabled / enabled state.
The option selected from Picker is not control, it is some value. If you make a list of controls presented to the user, some of them could be disabled, just to inform the user, that the action associated with it is not currently available (like menu, some buttons collection etc.)
I suggest you to show in Picker only values which could be selected. This collection of values could be updated any time.
UPDATE
Do you like something like this?
No problem at all ... (copy - paste - try - modify ...)
import SwiftUI
struct Data: Identifiable {
let id: Int
let value: Int
var disabled: Bool
}
struct ContentView: View {
#State var selection = -1
#State var data = [Data(id: 0, value: 10, disabled: true), Data(id: 1, value: 20, disabled: true), Data(id: 2, value: 3, disabled: true), Data(id: 3, value: 4, disabled: true), Data(id: 4, value: 5, disabled: true)]
var filteredData: [Data] {
data.filter({ (item) -> Bool in
item.disabled == false
})
}
var body: some View {
VStack {
VStack(alignment: .leading, spacing: 0) {
Text("Select from avaialable")
.padding(.horizontal)
.padding(.top)
HStack {
GeometryReader { proxy in
Picker(selection: self.$selection, label: Text("label")) {
ForEach(self.filteredData) { (item) in
Text("\(item.value.description)").tag(item.id)
}
}
.pickerStyle(SegmentedPickerStyle())
.frame(width: CGFloat(self.filteredData.count) * proxy.size.width / CGFloat(self.data.count), alignment: .topLeading)
Spacer()
}.frame(height: 40)
}.padding()
}.background(Color.yellow.opacity(0.2)).cornerRadius(20)
Button(action: {
(0 ..< self.data.count).forEach { (i) in
self.data[i].disabled = false
}
}) {
Text("Enable all")
}
Button(action: {
self.data[self.selection].disabled = true
self.selection = -1
}) {
Text("Disable selected")
}.disabled(selection < 0)
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

SwiftUI - Animation in view stops when scrolling the list

Considering the following code, why the animations in the views that are initialized without the n property stops when you scroll the list?
Tested on Xcode 11.3 (11C29) with a new default project on device and simulator.
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
List(1...50, id: \.self) { n in
HStack {
KeepRolling()
Spacer()
KeepRolling(n: n)
}
}
}
}
}
struct KeepRolling: View {
#State var isAnimating = false
var n: Int? = nil
var body: some View {
Rectangle()
.frame(width: 50, height: 50)
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0))
.onAppear {
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
self.isAnimating = true
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
IMO it is due to caching/reuse in List. For List all the values of KeepRolling() is the same, so .onAppear is not always re-called.
If to make every such view unique, say using .id as below, all works (tested with Xcode 11.2)
KeepRolling().id(UUID().uuidString)

SwiftUI How to stop the animation of List in Multi-level NavigationView

I just want to stop the animation when I have a multi List in multi-level NavigationView. Maybe this is not "ANIMATION", I just want to fix that.
On Xcode Version 11.3.1 (11C504) + iOS 13.2
The code is simple and you can find out it's wired.
import SwiftUI
struct TestView: View {
var body: some View {
NavigationView {
List {
ForEach(1...4, id: \.self) {_ in
NavigationLink(destination: AView()) {
Text("root")
}
}
}
}
}
}
struct AView: View {
var body: some View {
List {
ForEach(1...4, id: \.self) {_ in
NavigationLink(destination: BView()) {
Text("aview")
}
}
}
}
}
struct BView: View {
var body: some View {
List {
ForEach(1...4, id: \.self) {_ in
NavigationLink(destination: BView()) {
Text("bview")
}
}
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
Ok... I've installed this bad-luck Xcode 11.3.1... and here is a solution (or workaround, anyway) - use explicit .listRowInsets as in below example (btw, insets can be any value)
List {
ForEach(1...1000, id: \.self) {_ in
NavigationLink(destination: BView()) {
Text("bview")
}
}
.listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20))
}
Works for any dynamic List