SwiftUI navigation decision based on a sheet view presenting 2 choice - swiftui

I am presenting a "wizard" that will be detecting a BLE device and then if it is the correct one the last view will ask if we want to register or skip.
Edit:{
the view order is: MainView presenting in fullScreenCover a first info view informing on how to detect the BLE device then this one pushes a second view with some info on the nearest BLE device and it is in this view that we have the fork where I am presenting a sheet to ask if the user wants to continue and register the BLE device or skip.
So MAIN > INFOView -> BLE detection (> Register or skip ? RegisterView : Destack to main)
}
I have that last view come up as a sheet it has 2 buttons, the first one as mentioned says "Register" and the other one says "skip". If the user presses the register then we dismiss the sheet and navigate to a view that is gathering personal info to register the BLE device. on the other hand, if the user chooses to skip then the wizard need to de-stack back over to the main view.
Normally in UIKit I would just have a delegate inform me of the choice then if skip was selected. I would call pop to root view controller, otherwise, if the register option was selected I would dismiss the sheet view and then navigate to one more final view and get the user registered.
In SwiftUI I do not know how to deal with that navigation fork. I tried using PassthroughSubject but then I have to set the PassthroughSubject var as a state var and in the end, I just did not get the call back from sending in the selection.
Tried binding then Was hoping to make an onReceive but then it is asking for a publisher and that felt wrong to create a publisher just for that.
I am wondering g what is the best way do take care of this in. swiftUI ?
edit:
this is the code (updated with the replay from #Predrag Samardzic) for the view that shows the info on the BLE device (smart bike) and will push at first a request to know if the user wants to register or not, then if yes push that registration screen if not dismiss the entire stack.
struct A18BikeDiscoveryView: View {
#EnvironmentObject var bleManager: ArgonBLEManager
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
private let shouldShowRegistration = CurrentValueSubject<Bool, Never>(false)
#State var isSheetPresented = false
#State var isRegistrationPresented = false
var body: some View {
VStack{
NavigationLink(
destination: A18RegistrationQuestionairy(QuestionairyViewModel()),
isActive: $isRegistrationPresented
) {
EmptyView()
}
A18ImageTextBanner(text: NSLocalizedString("bike_discovery_view_title", comment: ""))
.padding(.bottom, 35)
.navigationBarBackButtonHidden(true)
if let value = bleManager.model?.bikeInfo?.bikeModel{
Text(value)
.fontWeight(.bold)
.scaledFont(.largeTitle)
}
Image("subitoBike")
.resizable()
.frame(minWidth: 0334, idealWidth: 334, maxWidth: .infinity, minHeight: 223, idealHeight: 223, maxHeight: .infinity, alignment: .center)
.aspectRatio(contentMode: .fit)
.padding(.bottom, 10)
Divider()
VStack(alignment: .leading){
HStack{
Text("bike_discovery_view_year_created")
if let v = bleManager.model?.bikeInfo?.year{
Text(v)
}
}
HStack{
Text("bike_discovery_view_model_size")
Text("\(getSizeFromSerial())")
}
HStack{
Text("bike_discovery_view_bike_serial_number")
if let v = bleManager.model?.bikeInfo?.bikeSerialNumber {
Text(v)
}
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 66, alignment: .leading)
.padding(.horizontal, 40)
Divider()
.padding(.bottom, 30)
Button(action: {
isSheetPresented = true
}, label: {
Text("bike_discovery_view_bike_pairing_button_title")
.fontWeight(.bold)
.foregroundColor(.white)
})
.buttonStyle(A18RoundButtonStyle(bgColor: .red))
.padding(.horizontal)
.sheet(
isPresented: $isSheetPresented,
onDismiss: {
if shouldShowRegistration.value {
isRegistrationPresented = true
}},
content: {
A18BikeParingSelection(shouldShowRegistration: shouldShowRegistration)
})
.onReceive(shouldShowRegistration) { shouldShowRegistration in
isSheetPresented = false
}
Button(action: {
bleManager.disconect()
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("bike_discovery_view_bike_pairing_cancel_button_title")
.fontWeight(.bold)
.foregroundColor(Color("grey55"))
})
.padding()
Spacer()
}
.navigationBarColor(backgroundColor: .white, tintColor: .black)
.navigationBarTitleDisplayMode(.inline)
}
func getSizeFromSerial() -> String {
if let serial = bleManager.model?.bikeInfo?.bikeSerialNumber {
if serial.contains("XXS"){
return "XXS"
}else if serial.contains("XSM") {
return "XS"
}else if serial.contains("SML"){
return "S"
}else if serial.contains("MED"){
return "M"
}else if serial.contains("LAR"){
return "L"
}
}
return "N/A"
}
}

This is one possible solution - using CurrentValueSubject in order to trigger dismiss and keep info about the choice made on the presented screen. Then, if registration is needed, you trigger it when sheet is dismissed.
struct MainView: View {
private let shouldShowRegistration = CurrentValueSubject<Bool, Never>(false)
#State var isSheetPresented = false
#State var isRegistrationPresented = false
var body: some View {
VStack {
// this part is if you want to push registration screen, you will need to have MainView inside NavigationView for it
NavigationLink(
destination: RegistrationView(),
isActive: $isRegistrationPresented
) {
EmptyView()
}
// ----------------------------------------------------
Button {
isSheetPresented = true
} label: {
Text("Present sheet")
}
.sheet(
isPresented: $isSheetPresented,
onDismiss: {
if shouldShowRegistration.value {
isRegistrationPresented = true
}},
content: {
ChoiceView(shouldShowRegistration: shouldShowRegistration)
})
.onReceive(shouldShowRegistration) { shouldShowRegistration in
isSheetPresented = false
}
// this part is if you want to present registration screen as sheet
// .sheet(
// isPresented: $isRegistrationPresented,
// content: {
// RegistrationView()
// })
}
}
}
struct ChoiceView: View {
let shouldShowRegistration: CurrentValueSubject<Bool, Never>
var body: some View {
VStack{
Button {
shouldShowRegistration.send(false)
} label: {
Text("Dismiss")
}
Button {
shouldShowRegistration.send(true)
} label: {
Text("Register")
}
}
}
}
struct RegistrationView: View {
var body: some View {
Text("Registration")
}
}

Related

Custom CameraView bugs whole app when integrating it into my app

I have a custom camera view which uses UIKit to capture pictures and store it in a CameraViewModel in my SwiftUI project. The CameraPreview is what acts as the view Finder for my camera view and uses AVFoundation:
struct CameraPreview: UIViewRepresentable {
class VideoPreviewView: UIView {
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
}
let session: AVCaptureSession
func makeUIView(context: Context) -> VideoPreviewView {
let view = VideoPreviewView()
view.backgroundColor = .black
view.videoPreviewLayer.cornerRadius = 0
view.videoPreviewLayer.session = session
view.videoPreviewLayer.connection?.videoOrientation = .portrait
return view
}
func updateUIView(_ uiView: VideoPreviewView, context: Context) {
}
}
and the I use this in my CameraView.swift body as such
#StateObject var model = CameraViewModel()
#State var currentZoomFactor: CGFloat = 1.0
#Binding var showCameraView: Bool
// MARK: [main body starts here]
var body: some View {
GeometryReader { reader in
ZStack {
// This black background lies behind everything.
Color.black.edgesIgnoringSafeArea(.all)
CameraPreview(session: model.session)
.onAppear {
model.configure()
}
.alert(isPresented: $model.showAlertError, content: {
Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: {
model.alertError.primaryAction?()
}))
})
.overlay(
Group {
if model.willCapturePhoto {
Color.black
}
}
)
.scaledToFill()
.ignoresSafeArea()
.frame(width: reader.size.width,height: reader.size.height )
// .animation(.easeInOut)
VStack {
HStack {
Button {
//
} label: {
Image(systemName: "xmark")
.resizable()
.frame(width: 20, height: 20)
.tint(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
Spacer()
flashButton
}
HStack {
capturedPhotoThumbnail
Spacer()
captureButton
Spacer()
flipCameraButton
}
.padding([.horizontal, .bottom], 20)
.frame(maxHeight: .infinity, alignment: .bottom)
}
} // [ZStack Ends Here]
} // [Geometry Reader Ends here]
} // [Main Body Ends here]
I wish to open the camera View when someone presses a button somewhere on my app, like so
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView(showCamera: $showCamera)
}
}
But when I do this in my app, no matter where I put the camera overlay, it stays open and the close button does nothing to close the camera view either. I'm pretty sure this is my fundamental lack of how views are constructed in UIKit and how the UIViewRepresentable works but I'd like some help regardless on how I'd achieve the desired effect. Also, any resources to understand how this works would also be greatly appreciated. Thank you.

NavigationLink in SwiftUI not worked as expected

I'm trying to make navigation link, here I'm creating NavigationLink with isActive based on State variable isLoggedIn. But without setting isLoggedIn true getting navigating to next screen.
also, it's navigating on tap of Email Textfield which is wrong.
My expectation is it should navigate only after isLoggedIn setting to true.
struct ContentView: View {
#State private var isLoggedIn = false
#State private var email = ""
var body: some View {
NavigationView {
NavigationLink(destination: Text("Second View"), isActive: $isLoggedIn) {
VStack {
TextField("Email", text: $email)
.frame(maxWidth: .infinity, alignment: .leading)
.border(.gray, width: 1)
.foregroundColor(.blue)
Button("Send") {
isLoggedIn = true
}
}
.padding()
}
}
}
}
The expectation is wrong, NavigationLink handles user input independently (but also, additionally, can be activated programmatically).
In this scenario, to leave only programmatic activation, we need to hide navigation link, like
NavigationView {
VStack {
TextField("Email", text: $email)
.frame(maxWidth: .infinity, alignment: .leading)
.border(.gray, width: 1)
.foregroundColor(.blue)
Button("Send") {
isLoggedIn = true
}
.background(NavigationLink(destination: // << here !!
Text("Second View"), isActive: $isLoggedIn) { EmptyView() })
}
.padding()
}
Here it's working fine with this
struct MoviesListView: View {
#State var navigate = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Hi"), isActive: $navigate) {
Button("Add") {
navigate.toggle()
}
}
}
}
}
}

SwiftUI - Change direction of view hide animation

I've been perusing SO, and elsewhere, trying to find a way, if possible, to change the direction of how a view is hidden. The examples I've found hide a view by replacing it with an EmptyView, or changing the view's frame dimensions to zero. I am trying to hide a view by 'collapsing' it vertically, but everywhere I've looked the collapse/hide animation happens upwards.
In my case, a view will have a button underneath it that will collapse (hide) or expand (show) the view. The view and button are embedded in a scroll view. What I'd like to have happen is that when the view is wholly or partially scrolled up off the top of the screen, and the collapse button is tapped, the view should 'collapse downward' such that the button remains where it is. Everything I've tried causes the button to move upward off the screen.
I think that what has to happen is that the view origin's y value needs to change. But what would happen to all other views above this view? I've tried to accomplish what I've described by changing the transition but it's not having any affect.
I feel like I'm really missing something basic here so thanks for any help.
struct Collapsible<Content: View>: View {
private var content: () -> Content
#Binding var isCollapsed: Bool
#State var isOffscreen: Bool = false
init(isCollapsed: Binding<Bool>, content: #escaping () -> Content) {
self._isCollapsed = isCollapsed
self.content = content
}
var body: some View {
VStack {
content()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: isCollapsed ? 0 : nil)
.clipped()
.animation(.default, value: isCollapsed)
.transition(transition.combined(with: .opacity))
}
.background(
GeometryReader { proxy -> Color in
DispatchQueue.main.async {
let frame = proxy.frame(in: CoordinateSpace.global)
self.isOffscreen = frame.origin.y < 0
var _ = print("isOffscreen: \(self.isOffscreen)")
}
return Color.clear
}
)
}
var transition: AnyTransition {
if isOffscreen {
return AnyTransition.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top))
} else {
return AnyTransition.asymmetric(insertion: .move(edge: .top), removal: .move(edge: .bottom))
}
}
}
struct ContentView: View {
#State var isCollapsed = false
var body: some View {
ScrollView {
VStack {
Color.clear.frame(height: 250)
Collapsible(isCollapsed: $isCollapsed) {
HStack {
Text("Content")
}
.frame(maxWidth: .infinity)
.padding([.top, .bottom], 75)
.background(.secondary)
.border(.blue, width: 2)
}
Button(action: {
self.isCollapsed.toggle()
}, label: {
Text(isCollapsed ? "Expand" : "Collapse")
})
.buttonStyle(PlainButtonStyle())
Color.clear.frame(height: 1000)
}
.padding()
}
}
}

Swiftui Textfield not updating when value changes

I have a textfield on a form where user can type value, but I would also like to update the content of the textfield with a button.
Here is my code :
struct test: View {
#State private var amount: Double = 0.0
var body: some View {
Form {
VStack {
HStack {
Text("Amount EUR")
Spacer()
TextField("Type amount", value: $amount, format: .number)
.keyboardType(.numberPad)
.multilineTextAlignment(.trailing)
}
Text("Set MAX (999)")
.frame(maxWidth: .infinity, alignment: .leading)
.onTapGesture {
print("before tap \(amount )")
amount = 999
print("after tap \(amount)")
}
}
}
}
When I just launch the app, the first tap on the Text updates the textfield with 999, but after it does not work anymore.
The amount value is correctly updated but the textfield does not reflect the change.
Would you have an explanation ?
The answer is simply that TextFields don't update while they are in focus. To solve this problem, you need to incorporate a #FocusState in to the view, and cause the TextField to lose focus right before updating the variable. You can test it in your own view by tapping your Text prior to tapping in to the TextField. You will see it updates just fine.
struct ButtonUpdateTextField: View {
#State private var amount: Double = 0.0
#FocusState var isFocused: Bool // <-- add here
var body: some View {
Form {
VStack {
HStack {
Text("Amount EUR")
Spacer()
TextField("Type amount", value: $amount, format: .number)
.keyboardType(.numberPad)
.multilineTextAlignment(.trailing)
.focused($isFocused) // <-- add here
}
Text("Set MAX (999)")
.frame(maxWidth: .infinity, alignment: .leading)
.onTapGesture {
print("before tap \(amount )")
isFocused = false // <-- add here
amount = 999
print("after tap \(amount)")
}
}
}
}
}

SwiftUI - how to respond to TextField onCommit in an other View?

I made a SearchBarView view to use in various other views (for clarity, I removed all the layout modifiers, such as color and padding):
struct SearchBarView: View {
#Binding var text: String
#State private var isEditing = false
var body: some View {
HStack {
TextField("Search…", text: $text, onCommit: didPressReturn)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
if isEditing {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
}
}
}
)
}
func didPressReturn() {
print("did press return")
}
}
It looks and works great to filter data in a List.
But now I'd like to use the SearchBarView to search an external database.
struct SearchDatabaseView: View {
#Binding var isPresented: Bool
#State var searchText: String = ""
var body: some View {
NavigationView {
VStack {
SearchBarView(text: $searchText)
// need something here to respond to onCommit and initiate a network call.
}
.navigationBarTitle("Search...")
.navigationBarItems(trailing:
Button(action: { self.isPresented = false }) {
Text("Done")
})
}
}
}
For this, I only want to start the network access when the user hits return. So I added the onCommit part to SearchBarView, and the didPressReturn() function is indeed only called when tapping return. So far, so good.
What I don't understand is how SearchDatabaseView that contains the SearchBarView can respond to onCommit and initiate the database searh - how do I do that?
Here is possible approach
struct SearchBarView: View {
#Binding var text: String
var onCommit: () -> () = {} // inject callback
#State private var isEditing = false
var body: some View {
HStack {
TextField("Search…", text: $text, onCommit: didPressReturn)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
if isEditing {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
}
}
}
)
}
func didPressReturn() {
print("did press return")
// do internal things...
self.onCommit() // << external callback
}
}
so now in SearchDatabaseView you can
VStack {
SearchBarView(text: $searchText) {
// do needed things here ...
}
}