I have a CustomSearchBar view that looks like this
However, when I wrap it with NavigationLink, the placeholder text will be centered. And user inputs will be centered too.
How do I maintain the leading alignment while using NavigationLink?
My code structure looks like this:
enum Tab {
case social
}
struct MainAppView: View {
#State var selection: Tab = .social
var body: some View {
TabView(selection: $selection) {
ZStack{
CustomButton()
NavigationView { SocialView() }
}.tabItem{Image(systemName: "person.2")}.tag(Tab.social)
// other tabs....
}
struct SocialView: View {
// ...
var body: some View {
GeometryReader{ geometry in
VStack{
NavigationLink(destination: Text("test")) {
CustomSearchBar()
//...
}.navigationBarHidden(true)
.navigationBarTitle(Text(""))
}
}
}
}
struct CustomSearchBar: View {
var body: some View {
VStack{
HStack {
SearchBarSymbols(// some binding arguments)
CustomTextField(// some binding arguments)
CancelButton(// some binding arguments)
}
.padding(.vertical, 8.0)
.padding(.horizontal, 10.0)
.background(Color("SearchBarBackgroundColor"))
.clipShape(Capsule())
}
.padding(.horizontal)
}
}
struct CustomTextField: View {
var body: some View {
TextField("friend name", text: $searchText)
.frame(alignment: .leading)
.onTapGesture {
// some actions
}
.foregroundColor(Color("SearchBarSymbolColor"))
.accentColor(Color("SearchBarSymbolColor"))
.disableAutocorrection(true)
}
}
The issues with your code are:
Your navigation view contains the search field. This means that any new view that gets pushed will cover the search field.
Your search field is inside of the navigation link. There are conflicting interactions here as it effectively turns the field into a button, ie tapping the search field vs tapping the navigation link.
Solution:
Move the navigation view below the text field, so that the new view will appear without covering it. Then change the navigation link so that it is activated via a binding that gets triggered when the search field is editing:
struct SocialView: View {
#State private var text: String = ""
#State private var isActive: Bool = false
var body: some View {
GeometryReader{ geometry in
VStack {
CustomTextField(searchText: $text, isActive: $isActive)
.padding(.vertical, 8.0)
.padding(.horizontal, 10.0)
.background(Color("SearchBarBackgroundColor"))
.clipShape(Capsule())
NavigationView {
NavigationLink(isActive: $isActive, destination: { Text("test") }, label: { EmptyView() })
}
}
}
}
}
struct CustomTextField: View {
#Binding var searchText: String
#Binding var isActive: Bool
var body: some View {
TextField("friend name", text: $searchText) { editing in
self.isActive = editing
} onCommit: {
}
.frame(alignment: .leading)
.disableAutocorrection(true)
}
}
Related
I have a button that opens up a Profile & Settings view in a sheet that has additional navigation views in it.
I am aware how to dismiss the sheet, however this method seems to not work with additional navigation views, as when I'm deeper into the navigation and I tap "Done" to dismiss the sheet, it only returns me back to the previous navigation view until I go back to the main Profile & Settings view.
The view with the button:
import SwiftUI
struct TodayView: View {
#State private var showSheet = false
var body: some View {
NavigationView {
ScrollView {
VStack(alignment: .leading) {
TodayTabDateComponent()
.padding(.top, -10)
ForEach(0 ..< 32) { item in
VStack(alignment: .leading) {
Text("Title")
Text("Description")
}
.padding(.vertical)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
}
.navigationTitle("Today")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
showSheet = true
}, label: {
Image(systemName: "person.circle.fill")
.foregroundColor(.primary)
})
.sheet(isPresented: $showSheet) {
ProfileAndSettingsView()
}
}
}
}
}
}
struct TodayView_Previews: PreviewProvider {
static var previews: some View {
TodayView()
}
}
The Profile & Settings view:
import SwiftUI
struct ProfileAndSettingsView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
List {
Section {
NavigationLink {
UserProfileView()
} label: {
HStack(alignment: .center) {
Image("avatar")
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
.frame(width: 60, height: 60)
VStack(alignment: .leading) {
Text("Name Surname")
.font(.title2)
.fontWeight(.bold)
Text("Profile Settings, Feed Preferences\n& Linked Accounts")
.font(.caption)
}
}
}
.padding(.vertical, 6)
} }
.listStyle(.insetGrouped)
.navigationTitle("Profile & Settings")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("Done")
}
}
}
}
}
}
}
struct ProfileAndSettingsView_Previews: PreviewProvider {
static var previews: some View {
ProfileAndSettingsView()
}
}
I have looked into the issue but couldn't find any working solutions.
Is your issue here that you're applying the .sheet to the Button inside the Toolbar? I think you need to apply it to the NavigationView itself?
import SwiftUI
struct TodayView: View {
#State private var showSheet = false
var body: some View {
NavigationView {
....
}
.navigationTitle("Today")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
showSheet = true
}, label: {
Image(systemName: "person.circle.fill")
.foregroundColor(.primary)
})
}
}
.sheet(isPresented: $showSheet) {
ProfileAndSettingsView()
}
}
}
}
If you're targeting iOS15 or higher don't use presentationMode use #Environment(\.isPresented) private var isPresented instead this will perform the action that you want.
presentationMode was deprecated and replaced by isPresented and dismiss
I believe that presentationMode performs a similar action as dismiss does which according to Apple Docs (on the dismiss)
If you do this, the sheet fails to dismiss because the action applies to the environment where you declared it, which is that of the detail view, rather than the sheet. In fact, if you’ve presented the detail view in a NavigationView, the dismissal pops the detail view the navigation stack.
The dismiss action has no effect on a view that isn’t currently presented. If you need to query whether SwiftUI is currently presenting a view, read the isPresented environment value.
If you're targeting a lower iOS version you can create your own key like so
struct SheetOpen: EnvironmentKey {
static var defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var sheetOpen: Binding<Bool> {
get { self[SheetOpen.self] }
set { self[SheetOpen.self] = newValue }
}
}
Where you have your sheet defined you do this
.sheet(isPresented: $showSheet) {
ProfileAndSettingsView()
.environment(\.sheetOpen, $showSheet)
}
Then you can use it like any other environment variable
#Environment(\.sheetOpen) var sheetOpen
To dismiss it you simply do this sheetOpen.wrappedValue.toggle()
I am having a problem while I want to dismiss a popup (that appears automatically depending on a specific condition) by clicking a button.
This is the PopUp struct:
struct dataPrivacyPopUp: View {
let model: OffersView.Model
let termsOfUseText = "Nutzungsbedingungen"
let privacyPolicyText = "Datenschutzerklärung"
#State var termsOfUseChecked = false
#State var privacyPolicyChecked = false
#State var buttonDisabled = true
#State private var showPopUp: Bool = false
#Binding var showModal: Bool
var body: some View {
ZStack {
if ( model.showPopUp == true) {
// PopUp Window
VStack(alignment: .center){
Image("logo")
.aspectRatio(contentMode: .fill)
.frame(alignment: .center)
.padding()
VStack(alignment: .leading) {
Text((model.acceptance?.salutation)!)
.multilineTextAlignment(.leading)
.padding()
.foregroundColor(Color.black)
Text((model.acceptance?.statement)!)
.multilineTextAlignment(.leading)
.padding()
.foregroundColor(Color.black)
Text((model.acceptance?.declarationIntro)!)
.multilineTextAlignment(.leading)
.padding()
.foregroundColor(Color.black)
if ((model.acceptance?.dpr)! == true) {
VStack(alignment: .leading){
HStack {
CheckBoxView(checked: $privacyPolicyChecked)
HStack(spacing: 0){
Text(R.string.localizable.dataPrivacyPopupText())
.foregroundColor(Color.black)
Button(privacyPolicyText) {
model.openUrl(url: API.privacyPolicyURL)
}
}
}
Text((model.acceptance?.declarationOutro)!)
.multilineTextAlignment(.leading)
.padding()
}
.padding()
Button(action: {
model.setTos()
print("showModal PopUpView2 1: \(showModal)")
self.showModal.toggle()
print("showModal PopUpView2 2: \(showModal)")
}, label: {
Text(R.string.localizable.dataPrivacyButton())
.foregroundColor(Color.white)
.font(Font.system(size: 23, weight: .semibold))
})
.disabled(model.buttonDisabledForOne(privacyPolicyChecked: privacyPolicyChecked, termsOfUseChecked: termsOfUseChecked))
.padding()
}
}
}
// .onAppear(perform: )
.background(Color.white01)
.padding()
}
}
}
}
and this is where I call it (contentView):
struct OffersView: View {
#StateObject var model = Model()
#State private var showingPopUp = false
#State private var showModal = false
#State private var showingAddUser = false
// var showPopup : Bool = true
var body: some View {
NavigationView {
Group {
switch model.sections {
case .loading:
ActivityIndicator(animate: true)
case .success(let sections):
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 0) {
Text(R.string.localizable.offersHello(model.firstName))
.aplFont(.headline02)
.padding(.bottom, 24)
VStack(spacing: 48) {
ForEach(sections) { section in
OffersSectionView(section: section, model: model)
}
}
}
.useFullWidth(alignment: .leading)
.padding()
}
default:
Color.clear
if ( model.showPopUp == true) {
ZStack {
Color.black.opacity(model.showPopUp ? 0.3 : 0).edgesIgnoringSafeArea(.all)
dataPrivacyPopUp(model: model, showModal: self.$showModal)
.onAppear(perform: {
self.showModal.toggle()
})
}
}
}
}
.navigationBarHidden(true)
.handleNavigation(model.navigationPublisher)
.onAppear(perform: model.onAppear)
.onDisappear(perform: model.onDisappear)
.environment(\.dynamicTypeEnabled, false)
.safariView(isPresented: model.showSafari) {
SafariView(url: model.safariUrl!)
}
}
}
}
I need help about this, I tried the traditional method to set a #Binding variable etc .. but that's not working, the boolean value is changing but the UI is not updating (the popup is not dismissing), thank you
I tried to look at your code - I suggest you simplify it to the bare minimum to exemplify your issue - and it seems that you are using 2 properties to show your pop-up: showingPopUp and showModal. It is quite likely that you are having trouble keeping them both in sync.
For starters, I would suggest to use only one variable, either it is true or false - "a man with two watches never knows what time it is".
For the solution:
If you prefer keeping your ZStack approach, the solution would look something like:
struct MyPrivacy: View {
#Binding var showMe: Bool
var body: some View {
VStack {
Text("The content of the pop-up")
.padding()
Button {
withAnimation {
showMe.toggle()
}
} label: {
Text("Dismiss")
}
}
}
}
struct Offers: View {
#State private var showPopup = false
var body: some View {
NavigationView {
ZStack {
VStack {
Text("View behind the pop-up")
.padding()
Button {
withAnimation {
showPopup.toggle()
}
} label: {
Text("Pop")
}
}
if showPopup {
Color.white
MyPrivacy(showMe: $showPopup)
}
}
}
}
}
If instead you want to go for a more flexible approach, if you are developing for iOS, SwiftUI has a convenient object - Sheets. You can use it as suggested in the documentation, or build a specific struct that manages all the modal views of this type and use your model to handle the presentation.
The process goes like:
Create a struct that will handle all kinds of Sheets of your app.
Add to your view-model the property to present any sheet.
Create the Views that will be the content of each sheet.
Call the .sheet(item:content:) method on each View the requires a sheet.
Here's the sample code:
SheetView handler:
struct SheetView: Identifiable {
// This struct controls what modal view will be presented.
// The enum SheetScreenType can grow to as many as different
// modal views your app needs - add the content in the switch below.
let id = UUID()
var screen: SheetScreenType
#ViewBuilder
var content: some View {
switch screen {
case .dataPrivacy:
DataPrivacy()
default:
EmptyView()
}
}
enum SheetScreenType {
case dataPrivacy
case none
}
}
Presenter in your view-model:
class MyViewModel: ObservableObject {
// This code can fit anywhere within your view-model.
// It controls the presentation of the modal view, which in
// this case is a Sheet.
private let sharedSheet = SheetView(screen: .none)
// Show the selected sheet
#Published var sheetView: SheetView?
var showSheet: SheetView.SheetScreenType {
get {
return sheetView?.screen ?? .none
}
set {
switch newValue {
case .none:
sheetView = nil
default:
sheetView = sharedSheet
}
sheetView?.screen = newValue
}
}
}
Content of your modal view:
struct DataPrivacy: View {
#EnvironmentObject var model: MyViewModel // Pass YOUR model here
var body: some View {
VStack(alignment: .center){
Text("Respecting your privacy, no details are shown here")
.padding()
Button {
print("Anything you need")
// Set the showSheet property of your model to
// present a modal view. Setting it to .none dismisses
// the modal view.
model.showSheet = .none
} label: {
Text("Time do dismiss the modal view")
}
.padding()
}
}
}
Enable your view to listen to your model to present the sheet:
struct OffersView: View {
#ObservedObject var model = MyViewModel() // Pass YOUR model here
var body: some View {
VStack {
Text("Anything you wish")
.padding()
Button {
withAnimation {
// Set the showSheet property of your model to
// present a modal view. Set it to any choice
// among the ones in the SheetScreen.SheetScreenType enum.
model.showSheet = .dataPrivacy
}
} label: {
Text("Tap here for the privacy in modal view")
}
}
// Show a modal sheet.
// Add this property at the top level of every view that
// requires a modal view presented - whatever content it might have.
.sheet(item: $model.sheetView) { sheet in
sheet.content
.environmentObject(model)
}
}
}
Good luck with your project!
I have made a view with two possible bottom sheets. The action works, and Bottom Sheets do open. Crazy thing is they open without the view inside. I have to close the one I opened and open the other one. When I do and than come back to the first one I will see the content. The code builds without warnings:
LogInView - where the logic is:
import SwiftUI
struct LogInView: View {
#EnvironmentObject var userInfo: UserInfo
enum Action{
case resetPW, signUp
}
#State private var showSheet = false
#State private var action:Action?
var body: some View {
LoginEmailView(showSheet: $showSheet, action: $action)
.sheet(isPresented: $showSheet){
if self.action == .resetPW{
ModalResetPWView()
}else if self.action == .signUp{
ModalSignUpView()
}
}
}
}
The view from which actions come:
import SwiftUI
struct LoginEmailView: View {
#EnvironmentObject var userInfo: UserInfo
#StateObject var user:LogInViewModel = LogInViewModel()
// ----- > THERE IS BINDING
#Binding var showSheet: Bool
#Binding var action:LogInView.Action?
// ----- >
var body: some View {
VStack{
Spacer()
Image("logo")
HStack{
Text("Adres email:")
.padding(.horizontal, 10)
.font(.title)
.foregroundColor(.black)
Spacer()
}
TextField("Enter e-mail adress", text: self.$user.email)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.title)
.padding(.horizontal, 10)
.keyboardType(.emailAddress)
HStack{
Text("Password:")
.padding(.horizontal, 10)
.font(.title)
.foregroundColor(.black)
Spacer()
}
SecureField("Enter password", text: self.$user.password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.title)
.padding(.horizontal,10)
HStack{
Spacer()
// ----- > First Bottom sheet
Button(action: {
self.action = .resetPW
self.showSheet = true
}) {
Text("Forgot Password")
}
.padding(.top, 5)
.padding(.trailing, 10)
// ----- >
}
Button(action: {
self.userInfo.isAuthenticated = .signedIn
}) {
Text("Log in")
}
.font(.title)
.padding(5)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.padding(.top, 10)
.opacity(user.isLogInComplete ? 1 : 0.7)
.disabled(!user.isLogInComplete)
// ----- > Second bottom sheet
Button(action: {
self.action = .signUp
self.showSheet = true
}) {
Text("Sign Up")
}
// ----- >
.padding(.top, 35)
Spacer()
}
}
}
The .sheet modifier will create the sheet view as soon as LogInView() is initialized. In your 'if.. else if..' statement, there is no logic to catch 'else' situations (situations where action == nil). Therefore, since action == nil on init(), the first .sheet that will present will fail your 'if..else if' and an EmptyView will present.
But don't worry! This is a common issue and can be easily solved. Here are 2 easy ways to implement methods to fix this (I prefer the 2nd method bc it's cleaner):
METHOD 1: Present a single view & change that view's content instead of switching between which view to present.
Instead of doing the 'if.. else if..' statement within the .sheet modifier, present a static view (I've called it SecondaryView ) that has a #Binding variable connected to your action. This way, when LogInView() appears, we can ensure that it will definitely render this view and then we can simply modify this view's content by changing the #Binding action.
import SwiftUI
struct LogInView: View {
enum Action{
case resetPW, signUp
}
#State private var showSheet = false
#State private var action: Action?
var body: some View {
LoginEmailView(showSheet: $showSheet, action: $action)
.sheet(isPresented: $showSheet) {
SecondaryView(action: $action)
}
}
}
struct LoginEmailView: View {
#Binding var showSheet: Bool
#Binding var action: LogInView.Action?
var body: some View {
VStack(spacing: 40 ){
Text("Forgot Password")
.onTapGesture {
action = .resetPW
showSheet.toggle()
}
Text("Sign Up")
.onTapGesture {
action = .signUp
showSheet.toggle()
}
}
}
}
struct SecondaryView: View {
#Binding var action: LogInView.Action?
var body: some View {
if action == .signUp {
Text("SIGN UP VIEW HERE")
} else {
Text("FORGOT PASSWORD VIEW HERE")
}
}
}
METHOD 2: Make each Button it's own View, so that it can have it's own .sheet modifier.
In SwiftUI, we are limited to 1 .sheet() modifier per View. However, we can always add Views within Views and each subview is then allowed it's own .sheet() modifier as well. So the easy solution is to make each of your buttons their own view. I prefer this method because we no longer need to pass around the #State/#Binding variables between views.
struct LogInView: View {
var body: some View {
LoginEmailView()
}
}
struct LoginEmailView: View {
var body: some View {
VStack(spacing: 40 ){
ForgotPasswordButton()
SignUpButton()
}
}
}
struct ForgotPasswordButton: View {
#State var showSheet: Bool = false
var body: some View {
Text("Forgot Password")
.onTapGesture {
showSheet.toggle()
}
.sheet(isPresented: $showSheet, content: {
Text("FORGOT PASSWORD VIEW HERE")
})
}
}
struct SignUpButton: View {
#State var showSheet: Bool = false
var body: some View {
Text("Sign Up")
.onTapGesture {
showSheet.toggle()
}
.sheet(isPresented: $showSheet, content: {
Text("SIGN UP VIEW HERE")
})
}
}
In the following sample code, a button is placed on a (form-) section header, which will toggle a sheet whenever it is pressed. The sheet has a list of elements to show.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack{
Form{
Section(header: headerView()) {
Text("Some Text")
}
}
}
}
}
struct headerView: View {
#State var showSheet = false
var body: some View {
Button(action: { self.showSheet.toggle()}){
HStack{
Spacer()
Image(systemName: "pencil.and.ellipsis.rectangle")
Text("View Sheet")
}
}.sheet(isPresented: $showSheet) {sheetView()}
}
}
struct sheetView: View {
#Environment(\.presentationMode) private var presentationMode
var body: some View {
NavigationView{
VStack(alignment: .leading) {
List() {
Text("List element 1")
Text("List element 2")
Text("List element 3")
Text("List element 4")
}
}
.navigationBarTitle(Text("Logs"), displayMode: .inline)
.navigationBarItems(leading: EditButton(), trailing: Button(action: {self.presentationMode.wrappedValue.dismiss()}) { Text("Done").bold()})
}
}
}
This has been working totally fine on iOS 13. However, in iOS 14 as you can see in my screenshot bellow it renders fully corrupted:
List elements have strange font size, color and are in upper-case (most important one!)
NavigationBar Buttons are greyed and in upper-case
NavigationBar title is in upper-case
The corrupted behaviour stays as long as you don't touch the screen. When you touch the screen and drag the sheet a little bit down then the list appearance will get corrected. If you do the same to the NavigationBar, it will then also be rendered correctly.
Is anybody also facing this issue? Any known fixes?
This looks like a bug. The possible workaround is to move sheet out of Form.
Tested with Xcode 12.0 / iOS 14.
struct ContentView: View {
#State var showSheet = false
var body: some View {
VStack{
Form{
Section(header:
headerView(showSheet: $showSheet)
) {
Text("Some Text")
}
}
}.sheet(isPresented: $showSheet) {sheetView()}
}
}
struct headerView: View {
#Binding var showSheet: Bool
var body: some View {
Button(action: { self.showSheet.toggle()}){
HStack{
Spacer()
Image(systemName: "pencil.and.ellipsis.rectangle")
Text("View Sheet")
}
}
}
}
Description:
When an object in a list (created from a fetchrequest) is deleted from a context, and the context is saved, the list does not properly update.
Error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value (Thrown on line 5 below)
struct DetailView: View {
#ObservedObject var event: Event
var body: some View {
Text("\(event.timestamp!, formatter: dateFormatter)")
.navigationBarTitle(Text("Detail"))
}
}
Steps to reproduce:
Create a new Master Detail App project with SwiftUI and Core Data.
In the ContentView, set the body to a TabView with the first tab being the prebuilt NavigationView, and add a second arbitrary tab.
struct ContentView: View {
#Environment(\.managedObjectContext)
var viewContext
var body: some View {
TabView {
NavigationView {
MasterView()
.navigationBarTitle(Text("Master"))
.navigationBarItems(
leading: EditButton(),
trailing: Button(
action: {
withAnimation { Event.create(in: self.viewContext) }
}
) {
Image(systemName: "plus")
}
)
Text("Detail view content goes here")
.navigationBarTitle(Text("Detail"))
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
.tabItem { Text("Main") }
Text("Other Tab")
.tabItem { Text("Other Tab") }
}
}
}
Add a few items. Interact with those items in any way.
Change tabs.
Change back to Main Tab.
Attempt to delete an item.
I found a pure SwiftUI working solution:
/// This View that init the content view when selection match tag.
struct SyncView<Content: View>: View {
#Binding var selection: Int
var tag: Int
var content: () -> Content
#ViewBuilder
var body: some View {
if selection == tag {
content()
} else {
Spacer()
}
}
}
You can use it then in this way:
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
SyncView(selection: $selection, tag: 0) {
ViewThatNeedsRefresh()
}
.tabItem { Text("First") }
.tag(0)
Text("Second View")
.font(.title)
.tabItem { Text("Second") }
.tag(1)
}
}
}
You can use the SyncView for each view that needs a refresh.