TabView causing preview to crash - swiftui

I am currently trying to make a featured games section in my app. I am using the TabView in SwiftUI to do this, but am running into an issue where the preview crashes from it. I am not getting any errors and also am unable to run it live. Below is the code of the view causing the preview crash.
import SwiftUI
struct FeaturedGamesView: View {
var numberOfImages: Int
#ObservedObject var featuredGames: GameQuery
#State private var currentIndex: Int = 0
private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
var body: some View {
VStack(alignment: .leading) {
Text("Featured")
.font(.headline)
.padding(.leading, 15)
.padding(.top, 1)
HStack(alignment: .top, spacing: 0) {
TabView() {
ForEach(featuredGames.games.results) { game in
NavigationLink(destination: NavigationLazyView(GameDetailsView(gameDetailsQuery: GameQuery(gameName: game.slug)))){
GameItem(game: game)
}
}
}
.offset(x: (CGFloat(self.currentIndex) * -185), y: 0)
.animation(.spring())
.onReceive(self.timer) { _ in
self.currentIndex = (self.currentIndex + 1) % 19
}
}
.frame(height: 200)
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
}
struct FeaturedGamesView_Previews: PreviewProvider {
static var previews: some View {
FeaturedGamesView(numberOfImages: 3, featuredGames: GameQuery(gameCategory: GameCategory.featured))
}
}
From some of my experimentation it seems to either not like the observable object I am using to get an array of data to populate the tabview. Any help would be greatly appreciated.
Update: I created a simpler example which still produces the error. In this case everything is generic except the array being used which is an observable object. Switching out the data structure used in the ForEach fixes it, but am looking to understand why my observable object does not work here.
import SwiftUI
struct Test: View {
#State private var selection = 0
#ObservedObject var featuredGames: GameQuery
var body: some View {
VStack(alignment: .leading) {
Text("Featured")
.font(.headline)
.padding(.leading, 15)
.padding(.top, 1)
HStack {
TabView() {
ForEach(featuredGames.games.results) { game in
Text("Hi")
}
}.frame(height: 170)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test(featuredGames: GameQuery(gameCategory: GameCategory.featured))
}
}

Please see below code.
I fixed below part to simple then it works.
Maybe this has a problem.
ForEach(featuredGames.games.results) { game in
NavigationLink(destination: NavigationLazyView(GameDetailsView(gameDetailsQuery: GameQuery(gameName: game.slug)))){
GameItem(game: game)
}
}
Working code (I made my self simple GameQuery observableObject)
import SwiftUI
struct FeaturedGamesView: View {
var numberOfImages: Int
#ObservedObject var featuredGames: GameQuery
#State private var currentIndex: Int = 0
private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
var body: some View {
VStack(alignment: .leading) {
Text("Featured")
.font(.headline)
.padding(.leading, 15)
.padding(.top, 1)
HStack(alignment: .top, spacing: 0) {
TabView() {
ForEach(featuredGames.results, id:\.self) { game in
NavigationLink(destination: VStack {}){
Text("Hello")
}
}
}
.offset(x: (CGFloat(self.currentIndex) * -185), y: 0)
.animation(.spring())
.onReceive(self.timer) { _ in
self.currentIndex = (self.currentIndex + 1) % 19
}
}
.frame(height: 200)
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
}
final class GameQuery: ObservableObject {
var gameCagetory: Int = 0
var results: [Int] = [1,2,3]
}
struct FeaturedGamesView_Previews: PreviewProvider {
static var previews: some View {
FeaturedGamesView(numberOfImages: 3, featuredGames: GameQuery())
}
}

Related

how can i make a conditional navigation in swiftui [duplicate]

I am trying to push from login view to detail view but not able to make it.even navigation bar is not showing in login view. How to push on button click in SwiftUI? How to use NavigationLink on button click?
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text("Let's get you signed in.")
.bold()
.font(.system(size: 40))
.multilineTextAlignment(.leading)
.frame(width: 300, height: 100, alignment: .topLeading)
.padding(Edge.Set.bottom, 50)
Text("Email address:")
.font(.headline)
TextField("Email", text: $email)
.frame(height:44)
.accentColor(Color.white)
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
Text("Password:")
.font(.headline)
SecureField("Password", text: $password)
.frame(height:44)
.accentColor(Color.white)
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
Button(action: {
print("login tapped")
}) {
HStack {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
.accentColor(Color.black)
.padding()
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
.padding(Edge.Set.vertical, 20)
}
.padding(.horizontal,30)
}
.navigationBarTitle(Text("Login"))
}
To fix your issue you need to bind and manage tag with NavigationLink, So create one state inside you view as follow, just add above body.
#State var selection: Int? = nil
Then update your button code as follow to add NavigationLink
NavigationLink(destination: Text("Test"), tag: 1, selection: $selection) {
Button(action: {
print("login tapped")
self.selection = 1
}) {
HStack {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
.accentColor(Color.black)
.padding()
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
.padding(Edge.Set.vertical, 20)
}
Meaning is, when selection and NavigationLink tag value will match then navigation will be occurs.
I hope this will help you.
iOS 16+
Note: Below is a simplified example of how to present a new view. For a more advanced generic example please see this answer.
In iOS 16 we can access the NavigationStack and NavigationPath.
Usage #1
A new view is activated by a simple NavigationLink:
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink(value: "NewView") {
Text("Show NewView")
}
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
}
}
Usage #2
A new view is activated by a standard Button:
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Button {
path.append("NewView")
} label: {
Text("Show NewView")
}
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
}
}
Usage #3
A new view is activated programmatically:
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Text("Content View")
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
.onAppear {
path.append("NewView")
}
}
}
iOS 13+
The accepted answer uses NavigationLink(destination:tag:selection:) which is correct.
However, for a simple view with just one NavigationLink you can use a simpler variant: NavigationLink(destination:isActive:)
Usage #1
NavigationLink is activated by a standard Button:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
Button(action: {
self.isLinkActive = true
}) {
Text("Login")
}
}
}
.navigationBarTitle(Text("Login"))
}
}
}
Usage #2
NavigationLink is hidden and activated by a standard Button:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
Button(action: {
self.isLinkActive = true
}) {
Text("Login")
}
}
.navigationBarTitle(Text("Login"))
.background(
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
EmptyView()
}
.hidden()
)
}
}
}
Usage #3
NavigationLink is hidden and activated programmatically:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
}
.navigationBarTitle(Text("Login"))
.background(
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
EmptyView()
}
.hidden()
)
}
.onAppear {
self.isLinkActive = true
}
}
}
Here is a GitHub repository with different SwiftUI extensions that makes navigation easier.
Another approach:
SceneDelegate
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: BaseView().environmentObject(ViewRouter()))
self.window = window
window.makeKeyAndVisible()
}
BaseView
import SwiftUI
struct BaseView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
if viewRouter.currentPage == "view1" {
FirstView()
} else if viewRouter.currentPage == "view2" {
SecondView()
.transition(.scale)
}
}
}
}
#if DEBUG
struct MotherView_Previews : PreviewProvider {
static var previews: some View {
BaseView().environmentObject(ViewRouter())
}
}
#endif
ViewRouter
import Foundation
import Combine
import SwiftUI
class ViewRouter: ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
var currentPage: String = "view1" {
didSet {
withAnimation() {
objectWillChange.send(self)
}
}
}
}
FirstView
import SwiftUI
struct FirstView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Button(action: {self.viewRouter.currentPage = "view2"}) {
NextButtonContent()
}
}
}
}
#if DEBUG
struct FirstView_Previews : PreviewProvider {
static var previews: some View {
FirstView().environmentObject(ViewRouter())
}
}
#endif
struct NextButtonContent : View {
var body: some View {
return Text("Next")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.blue)
.cornerRadius(15)
.padding(.top, 50)
}
}
SecondView
import SwiftUI
struct SecondView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Spacer(minLength: 50.0)
Button(action: {self.viewRouter.currentPage = "view1"}) {
BackButtonContent()
}
}
}
}
#if DEBUG
struct SecondView_Previews : PreviewProvider {
static var previews: some View {
SecondView().environmentObject(ViewRouter())
}
}
#endif
struct BackButtonContent : View {
var body: some View {
return Text("Back")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.blue)
.cornerRadius(15)
.padding(.top, 50)
}
}
Hope this helps!
Simplest and most effective solution is :
NavigationLink(destination:ScoresTableView()) {
Text("Scores")
}.navigationBarHidden(true)
.frame(width: 90, height: 45, alignment: .center)
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.contentShape(Rectangle())
.padding(EdgeInsets(top: 16, leading: UIScreen.main.bounds.size.width - 110 , bottom: 16, trailing: 20))
ScoresTableView is the destination view.
In my opinion a cleaner way for iOS 16+ is using a state bool to present the view.
struct ButtonNavigationView: View {
#State private var isShowingSecondView : Bool = false
var body: some View {
NavigationStack {
VStack{
Button(action:{isShowingSecondView = true} ){
Text("Show second view")
}
}.navigationDestination(isPresented: $isShowingSecondView) {
Text("SecondView")
}
}
}
}
I think above answers are nice, but simpler way should be:
NavigationLink {
TargetView()
} label: {
Text("Click to go")
}

SwiftUI Binding Test for Button And Picker

I would like to make a test with SwiftUI binding.
Aim was changing Picker selection with a button and with an other Picker.
In following code ContentView2's Picker selection will be changed with button in ContentView and Picker in ContentView1.
Button can change ContentView2 picker selection but ContentView1 Picker does not.
I can not find the reason.
You can copy paste code to test.
import SwiftUI
struct ContentView: View {
#State private var index1 = 0
#State private var index2 = 1
var body: some View {
GeometryReader { mainView in
HStack {
Button(action: {
if index2 == 0 {
index2 = 1
} else {
index2 = 0
}
}) {
Text("Button")
}
ContentView1(pickerIndex: $index1)
ContentView2(pickerIndex: $index2)
}
}
}
}
struct ContentView1: View {
#State var pickerData = ["Data1", "Data2"]
#Binding var pickerIndex: Int
#State var pickerIndex2 = 0
var customLabel0: some View{
HStack {
VStack(spacing: 10) {
Text("Picker One")
.foregroundColor(.black)
Text(pickerData[pickerIndex])
.multilineTextAlignment(.center)
}
.foregroundColor(.white)
}
.frame(width: (UIScreen.main.bounds.width - (2.5 * 8 )) * 0.6 * 0.5, height: 100)
.background(Color.gray)
.cornerRadius(5)
}
#State private var testIndex = 1
var body: some View {
Menu {
Picker("", selection: self.$pickerIndex) {
ForEach(0..<pickerData.count, id: \.self) {index in
Text(pickerData[index])
}
}
} label: {
customLabel0
}
.onChange(of: pickerIndex, perform: {_ in
ContentView2(pickerIndex: $pickerIndex)
})
}
}
struct ContentView2: View {
#State var pickerData = ["Data1", "Data2"]
#Binding var pickerIndex: Int
var customLabel0: some View{
HStack {
VStack(spacing: 10) {
Text("Picker Two")
.foregroundColor(.black)
Text(pickerData[pickerIndex])
.multilineTextAlignment(.center)
}
.foregroundColor(.white)
}
.frame(width: (UIScreen.main.bounds.width - (2.5 * 8 )) * 0.6 * 0.5, height: 100)
.background(Color.gray)
.cornerRadius(5)
}
var body: some View {
Menu {
Picker("", selection: $pickerIndex) {
ForEach(0..<pickerData.count, id: \.self) {index in
Text(pickerData[index])
}
}
} label: {
customLabel0
}
.onChange(of: pickerIndex, perform: {_ in
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Initializing 2 variables in init(): 'self' used before all stored properties are initialized

I'm trying to initialize 2 variables
self.customerVM = BuildCustomerViewModel()
self.showSurvey = showSurvey.wrappedValue
inside of my init() function and xCode returns
'self' used before all stored properties are initialized
When i try to initialize just the 1st one and do not use the 2nd variable - everything goes smoothly.. I don't understand why..
I wonder how should i change the code to make it work. Any help is appreciated.
import SwiftUI
struct BuildCustomerView: View {
#EnvironmentObject var thisSession: CurrentSession
#ObservedObject var customerVM: BuildCustomerViewModel
#State var fitnessLevel: Double = 0.0
#Binding var showSurvey: Bool
init(showSurvey: Binding<Bool>) {
self.customerVM = BuildCustomerViewModel()
self.showSurvey = showSurvey.wrappedValue
}
var body: some View {
ZStack {
VStack (alignment: .leading) {
VStack {
Text("What is your fitness level?")
.font(.headline)
}
HStack (alignment: .top) {
Slider(value: self.$fitnessLevel, in: -1...1, step: 0.1)
}
.frame(height: 50)
// save changes
Rectangle()
.fill( Color.blue )
.frame(height: 150, alignment: .leading)
.overlay(
Text("Next")
.font(.largeTitle)
.fontWeight(.semibold)
.foregroundColor(.white)
)
.onTapGesture {
self.customerVM.insertCustomerData(userId: self.thisSession.userId!, customerData: CustomerData(fitnessLevel: self.fitnessLevel)) { success in
if success == true {
print("FitnessLevel update succeed")
self.showSurvey.wrappedValue = false
} else {
print("FitnessLevel update failed")
}
}
}
Spacer()
}
.padding(.top, 30)
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
}
}
}
MainViewWrapper code, where this view is called from:
struct MainViewWrapper: View {
#EnvironmentObject var thisSession: CurrentSession
#ObservedObject var mainData: MainViewModel
// show profile if all data is loaded
#State var showProfile: Bool = false
#State var showSurvey: Bool = false
#State var selection: String? = nil
init(mainData: MainViewModel) {
self.mainData = mainData
}
var body: some View {
NavigationView {
ZStack {
ProfileView()
.opacity(self.showProfile ? 1 : 0)
BuildCustomerView(showSurvey: self.$showSurvey)
.opacity(self.showSurvey ? 1 : 0)
}
}
}
}
Binding as a property (hidden) has _ (underscore), so you have to initialize it as
init(showSurvey: Binding<Bool>) {
self.customerVM = BuildCustomerViewModel()
self._showSurvey = showSurvey // << this !!
}

Making a chat app in SwiftUI: How to make ScrollView keep its place when the keyboard shows up?

I am making a chat app in SwiftUI. Here is the effect that I want to have: open any chat in Telegram or Whatsapp, tap on the input box. The content of the chat slides up when the keyboard slides up. So if you were looking at say the bottom message, you can still see it.
I am unable to get this effect in SwiftUI. Keyboard sliding up does not slide the content of the chat:
import SwiftUI
struct SlidingKeyboardTest: View {
#State var inputText = "Placeholder"
var body: some View {
VStack {
ScrollView {
LazyVStack {
ForEach(1...100, id: \.self) { id in
HStack {
Spacer()
Text("message \(id)")
Spacer()
}
}
}
}
TextEditor(text: $inputText)
.frame(height: 50)
}
.background(LinearGradient(gradient: Gradient(colors: [.white, .blue, .white]), startPoint: .top, endPoint: .bottom))
.onTapGesture { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }
}
}
struct SlidingKeyboardTest_Previews: PreviewProvider {
static var previews: some View {
SlidingKeyboardTest()
}
}
Any ideas how to get this effect?
You need to use Introspect to receive access to UIScrollView and listen to keyboard height changes.
Here is a code:
import Combine
import Introspect
import SwiftUI
struct SlidingKeyboardTest: View {
#State var inputText = "Placeholder"
#State var keyboardHeight = CGFloat(0)
#State var scrollView: UIScrollView? = nil
var body: some View {
VStack {
ScrollView {
LazyVStack {
ForEach(1 ... 100, id: \.self) { id in
HStack {
Spacer()
Text("message \(id)")
Spacer()
}
}
}
}.introspectScrollView {
scrollView = $0
}
TextEditor(text: $inputText)
.frame(height: 50)
}.onReceive(Publishers.keyboardHeight) { height in
if height > 0 {
self.scrollView!.setContentOffset(CGPoint(x: 0, y: self.scrollView!.contentOffset.y + height), animated: true)
} else {
self.scrollView!.contentOffset.y = max(self.scrollView!.contentOffset.y - keyboardHeight, 0)
}
keyboardHeight = height
}
.background(LinearGradient(gradient: Gradient(colors: [.white, .blue, .white]), startPoint: .top, endPoint: .bottom))
.onTapGesture { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }
}
}
struct SlidingKeyboardTest_Previews: PreviewProvider {
static var previews: some View {
SlidingKeyboardTest()
}
}

How to remove List selection indicator and separator in SwiftUI?

I code a List in a ScrollView, when I selected a List cell to translate to another view and return back, the cell selected indicator did not disappear after selecting.
I hope after selected the list cell, the selected indicator should be disappear.
I debugged, I found that the ScrollView has some problems when it worked with List.If no ScrollView, the list selection behavior is all right, if plus the ScrollView outside the list, the problem become.
The other problem is How to remove the List Separator.
Thank you for your help!!!
#State var valueData: [String] = ["Apple", "Pear", "Orange", "Cake"]
var body: some View {
NavigationView {
ScrollView(.vertical) {
VStack(spacing: 10) {
DietListView(valueData: self.$valueData)
DietListView(valueData: self.$valueData)
.padding()
}
}
.frame(width: 352)
}
}
struct DietListView: View {
#Binding var valueData: [String]
var body: some View {
VStack {
List {
ForEach(self.valueData, id: \.self) { item in
NavigationLink(destination: DietItemDetailView()) {
HStack {
Text(item)
Spacer()
Text("100")
}
}
}
.onDelete { index in
self.valueData.remove(at: index.first!)
}
}
.frame(height: 300)
}
.frame(width: 352, height: 350)
.background(Color.white)
.cornerRadius(16)
.shadow(radius: 10)
}
}
the problem just like this:
At the moment (SwiftUI Beta 5) you can't customise List very much changing, for example, the divider style. What you can do, depending on your needs, is to use a ScrollView with ForEach and give the cell the style you want. For example:
struct ContentView: View {
#State var valueData: [String] = ["Apple", "Pear", "Orange", "Cake"]
var body: some View {
NavigationView {
ScrollView(.vertical) {
VStack(spacing: 10) {
DietListView(valueData: self.$valueData)
DietListView(valueData: self.$valueData)
.padding()
}
}
.frame(width: 352)
}
}
}
struct DietListView: View {
#Binding var valueData: [String]
var body: some View {
VStack {
ScrollView {
ForEach(self.valueData, id: \.self) { item in
NavigationLink(destination: Text("ciao")) {
HStack {
Text(item)
Spacer()
Text("100")
}
.foregroundColor(.primary)
.padding(10)
}
}
}
.frame(height: 300)
}
.frame(width: 352, height: 350)
.background(Color.white)
.cornerRadius(16)
.shadow(radius: 10)
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Also, take a look at this: https://stackoverflow.com/a/56498261/1291872
EDIT: you can use onDelete, onMove and onInsert only within a List. If you want to let users delete a row in a ScrollView you must implement something yourself. Take a look at the code below for a simple (pretty ugly) example:
struct ContentView: View {
#State var valueData: [String] = ["Apple", "Pear", "Orange", "Cake"]
var body: some View {
NavigationView {
ScrollView(.vertical) {
VStack(spacing: 10) {
DietListView(valueData: self.$valueData)
DietListView(valueData: self.$valueData)
.padding()
}
}
.frame(width: 352)
}
}
}
struct DietListView: View {
#Binding var valueData: [String]
var body: some View {
VStack {
ScrollView {
ForEach(self.valueData.indices, id: \.self) { idx in
NavigationLink(destination: Text("ciao")) {
HStack {
Text(self.valueData[idx])
Spacer()
Text("100")
.padding(.trailing, 20)
Button(action: {
self.valueData.remove(at: idx)
}) {
Image(systemName: "xmark.circle")
.resizable()
.frame(width: 25, height: 25)
.foregroundColor(Color.red)
}
}
.foregroundColor(.primary)
.padding([.leading, .trailing], 20)
.padding([.top, .bottom], 10)
}
}
}
.frame(height: 300)
}
.frame(width: 352, height: 350)
.background(Color.white)
.cornerRadius(16)
.shadow(radius: 10)
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif