Swift UI and ObservableObject Object Data Cleared - ios

I'm a bit beginner in SWIFT and right now I'm facing a problem whit UI. Let me try to explain my problem.
my homeview screen data coming from web service using Observable object and it loads the data first time. But when I tried to open my left side slide menus than homeView webservice/obervable object data is just cleared when open the left slide menu view. Why my observable object data is empty. Let me share my code:
1.------ This is a my main/parentView
struct ContentView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.viewRouter.showSlideOutMenu = false
self.viewRouter.showDepartmentsMenu = false
self.viewRouter.showAccountMenu = false
}
}
}
return GeometryReader { g in
ZStack(alignment: .leading) {
RouteChanger(viewRouter: self._viewRouter)
if self.viewRouter.showSlideOutMenu {
MainMenuView(viewRouter: self._viewRouter)
.frame(width: g.size.width/2)
.transition(.move(edge: .leading))
}
}
.gesture(drag)
}
}
}
2.----- This is my RouteChanger view for navigate to different pages of my views.
struct RouteChanger: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
GeometryReader { g in
VStack {
if self.viewRouter.currentPage == "Home" {
HomeView()
//.modifier(PageSwitchModifier())
}
}
}
}
}
3.... This is my homeView where I am using Observeable Object
struct HomeView: View {
#ObservedObject var homeController = HomeController()
var body: some View {
GeometryReader { g in
ZStack {
Color(UIColor.midTown.blue)
.edgesIgnoringSafeArea(.top)
VStack { //whole body
if self.homeController.homePageData.CODE == "0" {
ImageViewWidget(imageUrl: (self.homeController.homePageData.DATA?.headerList[0].img_url)!)
.frame(minWidth: g.size.width, maxWidth: g.size.width, minHeight: (g.size.width * UIImage(named: "header")!.size.height) / UIImage(named: "header")!.size.width, maxHeight: (g.size.width * UIImage(named: "header")!.size.height) / UIImage(named: "header")!.size.width)
}
else {
Text("Loading...")
.foregroundColor(Color.blue)
.padding()
.frame(width: g.size.width)
}
}
}
}
}
}

The EnvironmentObject is injected for all subviews automatically, so related part of your ContentView should look like below
ZStack(alignment: .leading) {
RouteChanger() // << here
if self.viewRouter.showSlideOutMenu {
MainMenuView() // << here
.frame(width: g.size.width/2)
.transition(.move(edge: .leading))
}

Related

Updating tapGesture area(frame) after device is rotated SwiftUI

I have an issue with updating the area(frame) of .onTapGesture after a device is rotated. Basically, even after changing #State var orientation the area where .onTapGesture works remain the same as on the previous orientation.
Would appreciate having any advice on how to reset that tap gesture to the new area after rotation.
Thanks in advance!
struct ContentView: View {
var viewModel = SettingsSideMenuViewModel()
var body: some View {
VStack {
SideMenu(viewModel: viewModel)
Button("Present menu") {
viewModel.isShown.toggle()
}
Spacer()
}
.padding()
}
}
final class SettingsSideMenuViewModel: ObservableObject {
#Published var isShown = false
func dismissHostingController() {
guard !isShown else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
debugPrint("viewShoudBeDismissedHere")
}
}
}
struct SideMenu: View {
#ObservedObject var viewModel: SettingsSideMenuViewModel
#State private var orientation = UIDeviceOrientation.unknown
var sideBarWidth = UIScreen.main.bounds.size.width * 0.7
var body: some View {
GeometryReader { proxy in
ZStack {
GeometryReader { _ in
EmptyView()
}
.background(Color.black.opacity(0.6))
.opacity(viewModel.isShown ? 1 : 0)
.animation(.easeInOut.delay(0.2), value: viewModel.isShown)
.onTapGesture {
viewModel.isShown.toggle()
viewModel.dismissHostingController()
}
content
}
.edgesIgnoringSafeArea(.all)
.frame(width: proxy.size.width,
height: proxy.size.height)
.onRotate { newOrientation in
orientation = newOrientation
}
}
}
var content: some View {
HStack(alignment: .top) {
ZStack(alignment: .top) {
Color.white
Text("SOME VIEW HERE")
VStack(alignment: .leading, spacing: 20) {
Text("SOME VIEW HERE")
Divider()
Text("SOME VIEW HERE")
Divider()
Text("SOME VIEW HERE")
}
.padding(.top, 80)
.padding(.horizontal, 40)
}
.frame(width: sideBarWidth)
.offset(x: viewModel.isShown ? 0 : -sideBarWidth)
.animation(.default, value: viewModel.isShown)
Spacer()
}
}
}
struct DeviceRotationViewModifier: ViewModifier {
let action: (UIDeviceOrientation) -> Void
func body(content: Content) -> some View {
content
.onAppear()
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
action(UIDevice.current.orientation)
}
}
}
extension View {
func onRotate(perform action: #escaping (UIDeviceOrientation) -> Void) -> some View {
self.modifier(DeviceRotationViewModifier(action: action))
}
}
struct SideMenu_Previews: PreviewProvider {
static var viewModel = SettingsSideMenuViewModel()
static var previews: some View {
SideMenu(viewModel: viewModel)
}
}
In this example is just slideoutMenu with a blurred area. By opening that menu in portrait and taping on the blurred area this menu should close. The issue is when the menu is opened in portrait and then rotated to landscape - the tapGesture area stays the same as it was in portrait, hence if tapped in the landscape - nothing happens. This works in the same direction too. Thus the question is how to reset the tapGesture area on rotation?
This view is presented in UIHostingController. slideOutView?.modalPresentationStyle = .custom the issue is there. But if slideOutView?.modalPresentationStyle = .fullScreen (or whatever) - everything works okay.

Frame transition animation works conditionally

I am trying to make child view which contains string array moves back and forth with animation as button on parent view is toggled.
However, child view just show up and disappeared without any animation at all.
struct ParentView: View {
#State isToggle: Bool
var body: some View {
ChildView(isToggle: isToggle)
.onTabGesture {
withAnimation {
isToggle.toggle()
}
}
}
}
struct ChildView: View {
let values = [one, two, three, four]
var isToggle: Bool
var body: some View {
HStack {
ForEach(values) { value in
Text("\(value)")
.frame(width: UIScreen.main.bounds.width / 3)
}
}
.frame(width: UIScreen.main.bounds.width, alignment: isToggle ? .trailing : .leading)
}
I changed code(stored property to viewmodel) as below. and It works as expected.
class ViewModel: ObservableObject {
#Published var values = [one, two, three, four]
}
struct ParentView: View {
#State isToggle: Bool
var body: some View {
ChildView(isToggle: isToggle)
.onTabGesture {
withAnimation {
isToggle.toggle()
}
}
}
}
struct ChildView: View {
#EnvironmentObject private vm: ViewModel
var isToggle: Bool
var body: some View {
HStack {
ForEach(values) { vm.value in
Text("\(value)")
.frame(width: UIScreen.main.bounds.width / 3)
}
}
.frame(width: UIScreen.main.bounds.width, alignment: isToggle ? .trailing : .leading)
}
I thought that toggling state redraws view only when with stored property.
But, child view with viewmodel is still redrawn when toggle state changes.
Data itself not changed at all. Please kindly let me know why this is happening.
there are some minor typos in your first code. If I correct them the code runs, and the animation works:
struct ContentView: View {
#State private var isToggle: Bool = false
var body: some View {
ChildView(isToggle: isToggle)
.onTapGesture {
withAnimation {
isToggle.toggle()
}
}
}
}
struct ChildView: View {
let values = ["one", "two", "three", "four"]
var isToggle: Bool
var body: some View {
HStack {
ForEach(values, id: \.self) { value in
Text("\(value)")
.frame(width: UIScreen.main.bounds.width / 3)
}
}
.frame(width: UIScreen.main.bounds.width, alignment: isToggle ? .trailing : .leading)
}
}

Conditional component in SwiftUI

I'm giving my first steps with SwiftUI and I'm having problems with a component shown depending on a condition.
I'm trying to show a fullscreen popup (full screen with semi transparent black background and the popup in the middle with white background). To achieve this I've made this component:
struct CustomUiPopup: View {
var body: some View {
ZStack {
}
.overlay(CustomUiPopupOverlay, alignment: .top)
.zIndex(1)
}
private var CustomUiPopupOverlay: some View {
ZStack {
Spacer()
ZStack {
Text("POPUP")
.padding()
}
.zIndex(1)
.frame(width: UIScreen.main.bounds.size.width - 66)
.background(Color.white)
.cornerRadius(8)
Spacer()
}
.frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)
.background(Color.black.opacity(0.6))
}
}
If I set this in my main view, the popup is shown correctly over the button:
struct MainView: View {
var body: some View {
CustomUiPopup()
Button("Click to show popup") {
print("click on button")
}
}
}
If I set this, my popup is not shown (correct because hasToShowPopup is false), but if I click on the button it fails, the popup is not shown and the button can not be clicked again (?!), it seems like the view was freezed.
struct MainView: View {
#State private var hasToShowPopup = false
var body: some View {
if hasToShowPopup {
CustomUiPopup()
}
Button("Click to show popup") {
hasToShowPopup = true
}
}
}
I've even tried to initializate hasToShowPopup to true but the popup keeps failing, it's not shown in the first place:
struct MainView: View {
#State private var hasToShowPopup = true
var body: some View {
if hasToShowPopup {
CustomUiPopup()
}
Button("Click to show popup") {
hasToShowPopup = true
}
}
}
So my conclusion is that, I don't know why, but if I put my CustomUiPopup inside an "if" something is not rendered correctly.
What is wrong with my code?
Anyway, if this is not the correct approach to show a popup, I'll be glad to have any advice.
Following Ptit Xav suggestion I've tried this with the same results (my CustomUiPopup doesn't show):
struct MainView: View {
#State private var hasToShowPopup = false
var body: some View {
VStack {
if hasToShowPopup {
CustomUiPopup()
}
Button("Click to show popup") {
hasToShowPopup = true
}
}
}
}
This works fine with me:
struct CustomUiPopup: View {
var body: some View {
ZStack {
Spacer()
Text("POPUP")
.padding()
.zIndex(1)
.frame(width: UIScreen.main.bounds.size.width - 66)
.background(Color.white)
.cornerRadius(8)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
Color.black.opacity(0.6)
.ignoresSafeArea()
)
}
}
struct ContentView: View {
#State private var hasToShowPopup = false
var body: some View {
ZStack {
Button("Click to show popup") {
hasToShowPopup = true
}
if hasToShowPopup {
CustomUiPopup()
}
}
}
}

SwiftUI - NavigationBar in View from NavigationLink quickly showing then disappearing

I have a ContentView containing a NavigationView that leads to a DestinationView. I want to hide the navigation bar in the ContentView, but show it in the DestinationView. To hide it in the ContentView I set navigationBarHidden to true and give navigationBarTitle an empty string.
In the DestinationView I set navigationBarHidden to false and give it the title "DestinationView".
If I run the project and tap on the NavigationLink, the DestinationView shows the NavigationBar but quickly hides it after the view appeared. Can anybody help me with this?
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.red.frame(maxWidth: .infinity, maxHeight: .infinity)
NavigationLink(destination: DestinationView()) {
ZStack {
Color.green.frame(width: 200, height: 200)
Text("Tap me")
}
}
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
struct DestinationView: View {
var body: some View {
List {
Text("1")
Text("2")
}
.navigationBarTitle("DestinationView")
.navigationBarHidden(false)
}
}
You need to use variable to achieve this and bind it with your destination
struct ContentView: View {
#State var isNavigationBarHidden: Bool = true
var body: some View {
NavigationView {
ZStack {
Color.red.frame(maxWidth: .infinity, maxHeight: .infinity)
NavigationLink(destination: DestinationView(isNavigationBarHidden: self.$isNavigationBarHidden)) {
ZStack {
Color.green.frame(width: 200, height: 200)
Text("Tap me")
}
}
}
.navigationBarHidden(self.isNavigationBarHidden)
.navigationBarTitle("")
.onAppear {
self.isNavigationBarHidden = true
}
}
}
}
struct DestinationView: View {
#Binding var isNavigationBarHidden: Bool
var body: some View {
List {
Text("1")
Text("2")
}
.navigationBarTitle("DestinationView")
.onAppear {
self.isNavigationBarHidden = false
}
}
}
There is an issue with the safe area layout guide
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.red.frame(maxWidth: .infinity, maxHeight: .infinity)
VStack {
NavigationLink(destination: DestinationView()) {
ZStack {
Color.green.frame(width: 200, height: 200)
Text("Tap me")
}
}
}
}.edgesIgnoringSafeArea(.all)
.navigationBarHidden(true)
}
}
}
struct DestinationView: View {
var body: some View {
VStack {
List {
Text("1")
Text("2")
}
}.navigationBarTitle("DestinationView")
.navigationBarHidden(false)
}
}
Happy Coding...
Edit: use the accepted answer as it's a much cleaner solution.
I encountered this bug and ended up using UIViewControllerRepresentable to wrap a controller which sets the navigation bar hidden state in its viewDidAppear method:
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.red.frame(maxWidth: .infinity, maxHeight: .infinity)
NavigationLink(destination: DestinationView()) {
ZStack {
Color.green.frame(width: 200, height: 200)
Text("Tap me")
}
}
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
struct DestinationView: View {
var body: some View {
List {
Text("1")
Text("2")
}
.navigationBarTitle("DestinationView")
.navigationBarHidden(false)
.background(HorribleHack())
}
}
struct HorribleHack: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> HorribleHackViewController {
HorribleHackViewController()
}
func updateUIViewController(_ uiViewController: HorribleHackViewController, context: Context) {
}
}
class HorribleHackViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
DispatchQueue.main.async {
self.navigationController?.setNavigationBarHidden(false, animated: false)
}
}
}
For me passing a binding around through the view hierarchy wasn't optimal, adding the state to an environment var was preferable.
class SceneState: ObservableObject {
#Published var isNavigationBarHidden = true
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
...
var sceneState = SceneState()
...
let contentView = ContentView().environmentObject(sceneState)
...
}
struct ContentView: View {
var body: some View {
NavigationView {
View1()
}
}
}
struct View1: View {
#EnvironmentObject var sceneState: SceneState
#State private var showView2: Bool = false
var body: some View {
VStack {
Text("NO nav bar.")
Button("Go to View2") {
self.showView2 = true
}
NavigationLink(destination: View2(), isActive: $showView2, label: {EmptyView()})
}
.navigationBarHidden(self.sceneState.isNavigationBarHidden)
.navigationBarTitle("")
.navigationBarBackButtonHidden(self.sceneState.isNavigationBarHidden)
}
}
struct View2: View {
#EnvironmentObject var sceneState: SceneState
var body: some View {
VStack {
Text("WITH nav bar.")
}
.navigationBarHidden(self.sceneState.isNavigationBarHidden)
.navigationBarTitle("WWDC")
.navigationBarBackButtonHidden(self.sceneState.isNavigationBarHidden)
.onAppear {
self.sceneState.isNavigationBarHidden = false
}
}
}
There ist actually a really simple solution to this problem. After many tries I figured it, that you have to add the .navigationBarHidden(false) directly to the destination view inside the NavigationLink like this:
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.red.frame(maxWidth: .infinity, maxHeight: .infinity)
NavigationLink(destination: DestinationView()
.navigationBarHidden(false)) {
ZStack {
Color.green.frame(width: 200, height: 200)
Text("Tap me")
}
}
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
struct DestinationView: View {
var body: some View {
List {
Text("1")
Text("2")
}
.navigationBarTitle("DestinationView")
.navigationBarHidden(false)
}
}
It will work like desired, and won't disappear after showing up.
I run this code on an iOS 14 Simulator and the navigation bar did not hide, so I assume this might be an issue with iOS 13. I had a similar problem and my code which resulted in nav bar disappearing on iOS 13.5 Simulator worked fine on iOS 14.4 Simulator.

Change text based on toggle values in SwiftUI

I have created a View with Text, Image and Navigation Button in SwiftUI.When navigation button is pressed it will navigate to another view which contains Toggle.When I change the Toggle Value , I also want to change the Text value in the previous View.
Value is getting updated when changing the toggle but didn't reflected when accessed in previous View.
//BluetoothView.swift
struct BluetoothView: View {
#ObjectBinding var bluetooth = Settings()
var body: some View {
return NavigationButton(destination: ToggleBluetoothView()) {
HStack() {
Image("default")
.resizable()
.cornerRadius(12)
.frame(width: 25, height: 25)
.clipped()
.aspectRatio(contentMode: .fit)
Text("Bluetooth")
.color(.blue)
.font(.system(size: 18))
Text(bluetooth.isBluetoothOn ? "On" : "Off")
.color(.gray)
.font(.subheadline)
.frame(width: 50, height: 40, alignment: .trailing)
}
}
}
}
//ToggleBluetoothView.swift
struct ToggleBluetoothView: View {
#ObjectBinding var bluetooth = Settings()
var body: some View {
Form {
Section(header: Text("ENABLE TO CONNECT WITH NEARBY DEVICES")) {
Toggle(isOn: $bluetooth.isBluetoothOn) {
Text("Bluetooth")
}
}
}
}
}
//Settings.swift
class Settings: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var isBluetoothOn = false { didSet { update() } }
func update() {
didChange.send(())
}
}
You are instantiating Settings separately in each view. Both views need to be seeing the same Settings object:
Change the following:
NavigationButton(destination: ToggleBluetoothView(bluetooth: bluetooth)) { ... }
and remove the initial value in ToggleBluetoothView:
struct ToggleBluetoothView: View {
#ObjectBinding var bluetooth: Settings
var body: some View {
Form {
Section(header: Text("ENABLE TO CONNECT WITH NEARBY DEVICES")) {
Toggle(isOn: $bluetooth.isBluetoothOn) {
Text("Bluetooth")
}
}
}
}
}

Resources