SwiftUI - triggered breakpoints twice - swift

I am trying to figure out why a breakpoint triggered just once for the first time I used my app and when I did the same steps one more time the breakpoints triggered twice for the same line of code, nothing changed on runtime I just used navigation forward and back. I demo this issue with a comment on my video below. It's 5 minutes video with explanation, but you will see the issue in 2 minutes:
here is a link to a video I made
https://youtu.be/s3fqJ4YhQEc
If you don't want to watch a video I will add a code at the bottom of the question:
here is a link to a repo I used to record a video: https://github.com/matrosovDev/SwiftUINavigatioPopBackIssue
I am also use Apple Feedback Assistant, so guys help me with this as well. I assumed that there is something with deallocating view that was not deallocated in a right way but they answered with this:
There is no notion of “deallocating” structs in Swift. SwiftUI manages
view lifetimes. If you’re writing code that somehow depends on the
existance or non-existance of a particular View at a particular point
in time, that code is wrong.
What leads you to believe that the SecondView isn’t “deallocated”?
The code that leads you to that conclusion is making incorrect
assumptions about the SwiftUI view lifecycle.
Anyway you may see on the video breakpoints triggered twice and even before I actually showing SecondView.
Code example:
struct FirstView: View {
#EnvironmentObject var simpleService: SimpleService
#State var showSecondView = false
var body: some View {
NavigationView {
ZStack {
Button(action: {
self.downloadImageFromURL()
}) {
Text("Next")
.fontWeight(.bold)
.font(.title)
.padding(EdgeInsets(top: 20, leading: 40, bottom: 20, trailing: 40))
.background(Color.blue)
.cornerRadius(8)
.foregroundColor(.white)
}
NavigationLink(destination: SecondView(), isActive: $showSecondView) { EmptyView() }
}
}
}
func downloadImageFromURL() {
simpleService.image = nil
if let url = URL(string: "https://placebear.com/200/300") {
simpleService.downloadImage(from: url) { (image) in
self.showSecondView = true
}
}
}
}
struct CircleImage: View {
var image: Image
var body: some View {
image
.resizable()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
.aspectRatio(contentMode: .fill)
}
}
struct SecondView: View {
#EnvironmentObject var simpleService: SimpleService
var body: some View {
NavigationView {
CircleImage(image: simpleService.image!) // Called twice next time I visit SecondView
.frame(height: 140)
.frame(width: 140)
}.onAppear(perform: fetch)
}
private func fetch() {
print(Date())
}
}

Related

iOS16 Bug Keyboard breaks layout on sheet dismissal SwiftUI

In iOS16 faced a bug with keyboard inside sheet, when sheet is dismissing keyboard disappears(what is ok), but layout is not updated. I saw only 1 question on same problem and wondering maybe somebody found a temporary workaround until Apple don't fix this.
Code to reproduce :
struct Test: View {
#State var isPresented: Bool = false
#State var text: String = ""
var body: some View {
VStack{
Button {
isPresented.toggle()
} label: {
Text("PRESENT")
}
}
.sheet(isPresented: $isPresented) {
ZStack {
Color.red
VStack{
TextField("Test", text: $text)
.frame(height: 50, alignment: .center)
Spacer()
Rectangle()
.fill(Color.blue)
.frame(width:300, height: 50)
}
}
}
}
}
Video:
https://vimeo.com/758845068
The .ignoresSafeArea() fixes the issue, but...
This will have as result of keyboard overlap in your UI and not be able to scroll to see all your elements.
I use the .adaptsToKeyboard() custom modifier taken from this answer
and then using it where needed with this particular order.
VStack {...}
.adaptsToKeyboard()
.ignoresSafeArea()

SwiftUI NavigationView trying to pop to missing destination (Monoceros?)

I'm using Xcode 12 with deployment for iOS 14.0.
My home screen has a NavigationView
Within the NavigationView there is a TabView (with 4 tabs)
Within each tab are subviews that have buttons and NavigationLinks
The navigation on the app is functioning correctly (when I click a NavigationLink on one of the subviews, it navigates to the correct view and when I click the back button, it dismisses the view.) However, when I click the back button, the console prints the following error:
Trying to pop to a missing destination at /Library/Caches/com.apple.xbs/Sources/Monoceros/Monoceros-103/Shared/NavigationBridge_PhoneTV.swift:337
Aside from the error log, the app is functioning fine, so I'm planning to just ignore the error for now... but I'm wondering what it means? I don't have anything within my code named "Monoceros". I'm guessing it has something to do with the TabView being a subview of the NavigationView?
EDIT:
Several months later, this issue still persists. Here is reproducible code. Open the ContentView(), on the FirstScreen() click on the NavigationLink, then click the back button. It will print out Monoceros lol
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
FirstScreen()
.tabItem {
Text("One")
Image(systemName: "house.fill")
}
Text("Second Screen")
.tabItem {
Text("Two")
Image(systemName: "heart.fill")
}
}
}
}
}
struct FirstScreen: View {
var body: some View {
NavigationLink("Click here", destination: Text("Final Screen"))
// Click the back button on FinalScreen prints:
//Trying to pop to a missing destination at /Library/Caches/com.apple.xbs/Sources/Monoceros_Sim/Monoceros-120/Shared/NavigationBridge_PhoneTV.swift:341
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Unfortunately this is an active issue with a TabView placed inside NavigationView.
The error would not manifest itself if you placed your NavigationView inside the TabView, but this of course would lead to the tabs being displayed from within your Final Screen, which you are probably trying to avoid.
                                      
There is currently no work-around for this, and as of to date we need to wait for Apple to properly implement the corresponding .navigationBarHidden() of TabViews as well.
Problems and unexpected behaviours have been reported when embedding a TabView into a NavigationView, however if you have tested your app thoroughly and found no particular problems, it is safe to say that you can stick to this method.
Alternatively you will have to build a TabView component manually, like below:
import SwiftUI
enum Tab {
case house, heart
}
struct TabView: View {
#Binding var tabIdx: Tab
var body: some View {
HStack {
Group {
Spacer()
Button (action: {
self.tabIdx = .house
}) {
VStack{
Image(systemName: "house.fill")
Text("House")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .house ? .blue : .secondary)
Spacer()
Button (action: {
self.tabIdx = .heart
}) {
VStack{
Image(systemName: "heart.fill")
Text("Heart")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .heart ? .blue : .secondary)
Spacer()
}
}
.padding(.bottom, 30)
.padding(.top, 10)
.background(Color(red: 0.1, green: 0.1, blue: 0.1))
.font(.system(size: 30))
.frame(height: 80)
}
}
struct FirstScreen: View {
var body: some View {
NavigationLink("Click here", destination: Text("Final Screen"))
.font(.system(size:20))
}
}
struct ContentView: View {
#State var tabIdx: Tab = .house
var body: some View {
NavigationView {
VStack(spacing: 20) {
Spacer()
if tabIdx == .house {
FirstScreen()
} else if tabIdx == .heart {
Text("Second Screen")
}
Spacer(minLength: 0)
TabView(tabIdx: self.$tabIdx)
}
.ignoresSafeArea()
}
}
}
The above bug is well detailed in this blog post, which you could consult for further reference and more examples.

SwiftUI .onTapGesture not being called from inside a foreach loop

Here's the code that I am using:
var array = ["Q","W","E","R","T","Y"]
HStack(){
ForEach(0..<topRow.count, id: \.self){i in
MyView(letter: self.array[i])
.onTapGesture {
print("Test")
}
}
}
All the "MyView"s are displaying correctly, but when I tap any of them nothing is printed. Also the MyViews are just a text().
I found the problem and solution. As New Dev pointed out, the error was being called by something not included in the code shown. I'll post what what wrong here anyways incase someone stumbles upon this.
What wasn't included was the code for MyView which I'll put here:
struct MyView: View {
var letter:String
#State var pressed = false
var body: some View {
ZStack{
if pressed{
Text(letter)
.font(Font.custom("ComicNeue-Bold", size: 30))
.foregroundColor(.white)
.opacity(0.23)
} else if !pressed{
Text(letter)
.font(Font.custom("ComicNeue-Bold", size: 30))
.foregroundColor(.white)
}
}.padding(5)
.onTapGesture {
self.pressed = true
}
}
}
I solved the issue by removing the .onTapGesture from the ZStack. So I'm guessing that the issue is that views cannot have two .onTapGesture modifiers.

SwiftUI - Image offset preview issue

I have a navigation view which is a root view. After user login I would like to show Welcome View where I want to show user's image and user name.
Currently preview is a bit different to my device screen:
You may noticed that there is a Login back button on the simulator screen. Probably that's why I have the differences here.
Here is my code:
struct WelcomeView: View {
#EnvironmentObject var userService: UserService
var body: some View {
VStack {
Image("MountainWelcomBackground").resizable().frame(height: 300).edgesIgnoringSafeArea(.top)
CircleImage(image: userService.user.image!)
.offset(y: -220)
.frame(height: 140)
.frame(width: 140)
VStack(alignment: .leading) {
HStack(alignment: .top) {
Spacer()
Text(userService.user.username)
.font(.headline)
Spacer()
}
}
.offset(y: -200)
Spacer()
}
}
}
struct WelcomeView_Previews: PreviewProvider {
static var previews: some View {
let userService = UserService()
userService.user = User(username: "Alex Matrosov", email: "test.developer#gmail.com", avatar: nil, image: Image("ManPlaceholderAvatar"))
return WelcomeView().environmentObject(userService)
}
}
struct CircleImage: View {
var image: Image
var body: some View {
image
.resizable()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
.aspectRatio(contentMode: .fit)
}
}
How to make preview the same as on simulator? Probably I need to add Navigation view to preview somehow or maybe I set offset in a wrong way. Actually I just followed apple swiftui tutorial and they do this ui like me.
Offset depends on parent container, which differs in your case for real app running in Simulator and Preview showing only Welcome view.
Here is PreviewProvided to have same
struct WelcomeView_Previews: PreviewProvider {
static var previews: some View {
let userService = UserService()
userService.user = User(username: "Alex Matrosov", email: "test.developer#gmail.com", avatar: nil, image: Image("icon"))
return NavigationView {
WelcomeView().environmentObject(userService)
}
}
}

Is there a way in SwiftUI to return a different view based on an optional binding having a value?

I am trying to show a view after fetching some data on my view model (where the data can be optional because the view model only fetches it on request).
Why is the following not possible / how should I go about it?
#Binding var someViewModel: SomeViewModel?
var body: some View {
if let viewModel = self.someViewModel {
return filledView(with: viewModel)
}
return emptyView()
}
The part not working here is an if let in the Swift UI view builder.
One solution would be to have a separate Bool that fires when data is loaded, or even an enum to identify when data is in and when it's not, but then the view code is full of optional value checking which isn't ideal.
E.g. I want to avoid doing something like:
Image(systemName: someViewModel?.icon.symbol() ?? "plus_sign")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.foregroundColor(Color.red)
.padding()
Any help would be greatly appreciated.
You could use map. It will unwrap the optional if it has some value or ignore it altogether:
var body: some View {
Group {
someViewModel.map { FilledView(with: $0) }
}
}
Just found another option, we could wrap both the empty and filled view function returns in an AnyView and the problems disappear as well.
Example of the empty would then become:
func emptyView() -> AnyView {
return AnyView(Text(""))
}
And example of the filled view becomes:
func filledView(for viewModel: SomeViewModel) -> AnyView {
return AnyView(VStack(alignment: .leading) {
Image(systemName: viewModel.icon.symbol())
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.foregroundColor(Color.red)
.padding()
}
}
}