I'm trying to make something similar to the iOS notes app but for journaling; basically, I want there to be a list of journal entry cells users can scroll through which each display a detail view after they're clicked on where the user can view and edit their journal entry. The updating works fine, the only issue is that JournalDetailView dismisses itself after updateEntry() is called (after the user taps the "Done" button). I'm guessing this is because updateEntry() forces the view to reload, but I'm not sure how to get around this.
Here's the model:
struct JournalEntry: Identifiable, Hashable, Codable {
#DocumentID var id: String? = UUID().uuidString
#ServerTimestamp var date: Timestamp?
var text: String
var userId: String?
}
Here's the view code:
struct JournalCellView: View {
#ObservedObject var vm: JournalViewModel
#Binding var addButtonTapped: Bool
#State var showDetail = false
#State var entry: JournalEntry
var body: some View {
NavigationLink(destination: JournalDetailView(vm: vm, entry: $entry, text: entry.text), isActive: $showDetail, label: {
VStack {
HStack {
Text(entry.date!.dateValue(), style: .date)
.fontWeight(.bold)
.font(.system(size: 18))
.foregroundColor(.black)
.padding(.bottom, 3)
Spacer()
}
HStack {
Text(entry.text)
.font(.system(size: 14))
.foregroundColor(.secondary)
.lineLimit(1)
Spacer()
}
}.padding()
.background(RoundedRectangle(cornerRadius: 18).foregroundColor(.white))
.padding(.vertical, 4)
.onTapGesture {
showDetail = true
}
.onAppear {
if addButtonTapped {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
showDetail = true
}
addButtonTapped = false
}
}
})
}
}
struct JournalDetailView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var vm: JournalViewModel
#Binding var entry: JournalEntry
#State var text: String
#State var isTyping = false
var body: some View {
VStack {
HStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: { Image(systemName: "chevron.left").foregroundColor(.burgundy) })
Spacer()
if isTyping {
Button(action: {
endEditing()
updateEntry()
isTyping = false
}) {
Text("Done")
.foregroundColor(.burgundy)
}
} else {
Text("")
}
}.padding(.vertical)
Text(entry.date!.dateValue(), style: .date)
TextEditor(text: $text)
.onTapGesture {
isTyping = true
}
Spacer()
}.padding()
.navigationBarHidden(true)
}
func updateEntry() {
vm.updateJournalEntry(docID: entry.id!, date: entry.date!, text: text)
}
}
Here's updateJournalEntry():
func updateJournalEntry(docID: String, date: Timestamp, text: String) {
db.collection("journals").document(docID
).updateData(["date": date, "text": text, "userId": Auth.auth().currentUser!.uid])
}
I managed to get around this by only updating after the view would be dismissed naturally using .onDisappear and .onReceive. Not the cleanest solution, but it works. If someone has another suggestion, please contribute!
.onDisappear {
updateEntry()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
updateEntry()
}
Related
I want when I finish selecting the language and click the Save button it will return the ContentView page and display the language I have selected. And when I click again, it has to checkmark the language I selected before.
I have successfully displayed the data, but I don't know how to save it when I click the Save button
Here is all my code currently
ContentView
struct ContentView: View {
var body: some View {
NavigationView {
HStack {
NavigationLink(destination:LanguageView() ) {
Text("Language")
Spacer()
Text("I want to show the language here ")
}
}
}
}
}
LanguageView
struct LanguageView: View {
var body: some View {
VStack {
CustomLanguageView()
Button(action: {
})
{
Text("Save")
.foregroundColor(.black)
}
.padding()
Spacer()
}
}
}
struct CustomLanguageView: View {
var language = ["US", "English", "Mexico", "Canada"]
#State var selectedLanguage: String? = nil
var body: some View {
LazyVStack {
ForEach(language, id: \.self) { item in
SelectionCell(language: item, selectedLanguage: self.$selectedLanguage)
.padding(.trailing,40)
Rectangle().fill(Color.gray)
.frame( height: 1,alignment: .bottom)
}
.frame(height:15)
}
}
}
struct SelectionCell: View {
let language: String
#Binding var selectedLanguage: String?
var body: some View {
HStack {
Text(language)
Spacer()
if language == selectedLanguage {
Image(systemName: "checkmark")
.resizable()
.frame(width:20, height: 15)
}
}
.onTapGesture {
self.selectedLanguage = self.language
}
}
}
There are multiple ways to "Save" something but if you are just trying to get it back to the other view you could do something like this that I quickly setup.
struct ContentView: View {
#State var language: String? = ""
var body: some View {
NavigationView {
HStack {
NavigationLink(destination:LanguageView(language: $language)) {
Text("Language")
.padding()
Spacer()
Text(language!)
.padding()
}
}
}
}
}
struct LanguageView: View {
#Binding var language: String?
#State var selectedLanguage: String? = ""
var body: some View {
VStack {
CustomLanguageView(selectedLanguage: $selectedLanguage)
Button(action: {
language = selectedLanguage
})
{
Text("Save")
.foregroundColor(.black)
}
.padding()
Spacer()
}
}
}
struct CustomLanguageView: View {
var language = ["US", "English", "Mexico", "Canada"]
#Binding var selectedLanguage: String?
var body: some View {
LazyVStack {
ForEach(language, id: \.self) { item in
SelectionCell(language: item, selectedLanguage: self.$selectedLanguage)
.padding(.trailing,40)
Rectangle().fill(Color.gray)
.frame( height: 1,alignment: .bottom)
}
.frame(height:15)
}
}
}
struct SelectionCell: View {
let language: String
#Binding var selectedLanguage: String?
var body: some View {
HStack {
Text(language)
Spacer()
if language == selectedLanguage {
Image(systemName: "checkmark")
.resizable()
.frame(width:20, height: 15)
}
}
.onTapGesture {
self.selectedLanguage = self.language
}
}
}
Or if you are actually trying to save it to the device for later use you could use
UserDefaults.standard.setValue(selectedLanguage, forKey: "language")
Then to Retrieve it later do
UserDefaults.standard.value(forKey: "language") as! String
I'm new to Swift and I'm currently developing my own Timer Application for practice purposes. (I do it without storyboard)
Now I have the Problem that i made a View called "TimePickerView" (Code below), where I created my own Picker. Then I use that TimePickerView in another part of my Application with other Views (in a View). In that View I want to pick my time but I don't know how i can get the Values of the Picker (The Picker works by the way)
This is my TimePickerView
import SwiftUI
struct TimePickerView: View {
#State private var selectedTimeIndexSecond = 0
#State private var selectedTimeIndexMinutes = 0
#State private var seconds : [Int] = Array(0...59)
#State private var minutes : [Int] = Array(0...59)
var body: some View {
VStack{
Text("Select Your Time")
HStack{
//minutes-Picker
Picker("select time", selection: $selectedTimeIndexMinutes, content: {
ForEach(0..<minutes.count, content: {
index in
Text("\(minutes[index]) min").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
//seconds-Picker
Picker("select time", selection: $selectedTimeIndexSecond, content: {
ForEach(0..<seconds.count, content: {
index in
Text("\(seconds[index]) sec").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
Spacer()
}
Text("You picked the time")
.multilineTextAlignment(.center)
.font(.title2)
.padding()
Text("\(minutes[selectedTimeIndexMinutes]) min : \(seconds[selectedTimeIndexSecond]) sec")
.font(.title)
.bold()
.padding(.top, -14.0)
}
}
func getValues() -> (Int, Int) {
return (self.minutes[selectedTimeIndexMinutes] ,self.seconds[selectedTimeIndexSecond])
}
}
and that is the View I want to use my Picker, but I don't know how I get those values from the Picker:
struct SetTimerView: View {
#State var timepicker = TimePickerView()
var body: some View {
NavigationView{
VStack{
//Select the time
timepicker
//Timer variables (This doesn't work)
var timerTime = timepicker.getValues()
var minutes = timerTime.0
var seconds = timerTime.1
Spacer()
let valid : Bool = isValid(timerTime: minutes+seconds)
//Confirm the time
NavigationLink(
destination:
getRightView(
validBool: valid,
timerTime: minutes*60 + seconds),
label: {
ConfirmButtonView(buttonText: "Confirm")
});
Spacer()
}
}
}
func isValid(timerTime : Int) -> Bool {
if (timerTime == 0) {
return false
} else {
return true
}
}
#ViewBuilder func getRightView(validBool : Bool, timerTime : Int) -> some View{
if (validBool == true) {
TimerView(userTime: CGFloat(timerTime), name: "David", isActive: true)
} else {
UnvalidTimeView()
}
}
}
I think main problem is misunderstanding conceptions between data and views.
At first you need a model witch will override your data (create it in separate swift file):
import Foundation
class Time: ObservableObject {
#Published var selectedTimeIndexMinutes: Int = 0
#Published var selectedTimeIndexSecond: Int = 0
}
Pay attention on ObservableObject so that swiftUI can easily detect changes to it that trigger any active views to redraw.
Next I try to change the value of the model in the view
import SwiftUI
struct TimePickerView: View {
#EnvironmentObject var timeData: Time
#State private var seconds : [Int] = Array(0...59)
#State private var minutes : [Int] = Array(0...59)
var body: some View {
VStack{
Text("Select Your Time")
HStack{
//minutes-Picker
Picker("select time", selection: $timeData.selectedTimeIndexMinutes, content: {
ForEach(0..<minutes.count, content: {
index in
Text("\(minutes[index]) min").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
//seconds-Picker
Picker("select time", selection: $timeData.selectedTimeIndexSecond, content: {
ForEach(0..<seconds.count, content: {
index in
Text("\(seconds[index]) sec").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
Spacer()
}
Text("You picked the time")
.multilineTextAlignment(.center)
.font(.title2)
.padding()
Text("\(timeData.selectedTimeIndexMinutes) min : \(timeData.selectedTimeIndexSecond) sec")
.font(.title)
.bold()
.padding(.top, -14.0)
}
}
}
struct TimePickerView_Previews: PreviewProvider {
static var previews: some View {
TimePickerView()
.environmentObject(Time())
}
}
Like you can see I don't using #Blinding, instead of it I connecting our Model with a View
On the next view I can see changes, I created a new one because your example have view that don't indicated here...
import SwiftUI
struct ReuseDataFromPicker: View {
#EnvironmentObject var timeData: Time
var body: some View {
VStack{
Text("You selected")
Text("\(timeData.selectedTimeIndexMinutes) min and \(timeData.selectedTimeIndexSecond) sec")
}
}
}
struct ReuseDataFromPicker_Previews: PreviewProvider {
static var previews: some View {
ReuseDataFromPicker()
.environmentObject(Time())
}
}
And collect all in a Content View
struct ContentView: View {
var body: some View {
TabView {
TimePickerView()
.tabItem {Label("Set Timer", systemImage: "clock.arrow.2.circlepath")}
ReuseDataFromPicker()
.tabItem {Label("Show Timer", systemImage: "hourglass")}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(Time())
}
}
Like that you can easily change or reuse your data on any other views
My App currently has two pages, first page has a circle plus button which could lead us to a second page. Basically, I have a save button which after clicking it, we could get back to the rood page. I followed this link for going back to root view. I tried the most up voted code, his code works perfectly. I reduced his code to two scene (basically the same scenario as mine), which also works perfectly. But then I don't know why my own code, pasted below, doesn't work. Basically my way of handling going back to root view is the same as the one in the link.
//
// ContentView.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/22/20.
//
import SwiftUI
import UIKit
#if canImport(UIKit)
extension View {
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#endif
struct ContentView: View {
#EnvironmentObject private var fridge : Fridge
private var dbStartWith=0;
#State var pushed: Bool = false
#State private var selection = 1;
#State private var addFood = false;
var body: some View {
TabView(selection: $selection) {
NavigationView {
List(fridge.container!){
food in NavigationLink(destination: FoodView()) {
Text("HI")
}
}.navigationBarTitle(Text("Fridge Items"), displayMode: .inline)
.navigationBarItems(trailing:
NavigationLink(destination: AddFoodView(pushed: self.$pushed),isActive: self.$pushed) {
Image(systemName: "plus.circle").resizable().frame(width: 22, height: 22)
}.isDetailLink(false) )
}
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
.tag(1)
Text("random tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "bookmark.circle.fill")
Text("profile")
}
.tag(0)
}.environmentObject(fridge)
}
}
struct FoodView: View{
var body: some View{
NavigationView{
Text("food destination view ");
}
}
}
struct AddFoodView: View{
#Binding var pushed : Bool
#EnvironmentObject private var fridgeView : Fridge
#State private var name = ""
#State private var count : Int = 1
#State private var category : String = "肉类";
#State var showCategory = false
#State var showCount = false
var someNumberProxy: Binding<String> {
Binding<String>(
get: { String(format: "%d", Int(self.count)) },
set: {
if let value = NumberFormatter().number(from: $0) {
self.count = value.intValue;
}
}
)
}
var body: some View{
ZStack{
NavigationView{
VStack{
Button (action: {
self.pushed = false ;
//let tempFood=Food(id: fridgeView.index!,name: name, count: count, category: category);
//fridgeView.addFood(food: tempFood);
} ){
Text("save").foregroundColor(Color.blue).font(.system(size: 18,design: .default)) }
}.navigationBarTitle("Three")
}
ZStack{
if self.showCount{
Rectangle().fill(Color.gray)
.opacity(0.5)
VStack(){
Spacer(minLength: 0);
HStack{
Spacer()
Button(action: {
self.showCount=false;
}, label: {
Text("Done")
}).frame(alignment: .trailing).offset(x:-15,y:15)
}
Picker(selection: $count,label: EmptyView()) {
ForEach(1..<100){ number in
Text("\(number)").tag("\(number)")
}
}.labelsHidden()
} .frame(minWidth: 300, idealWidth: 300, maxWidth: 300, minHeight: 250, idealHeight: 100, maxHeight: 250, alignment: .top).fixedSize(horizontal: true, vertical: true)
.background(RoundedRectangle(cornerRadius: 27).fill(Color.white.opacity(1)))
.overlay(RoundedRectangle(cornerRadius: 27).stroke(Color.black, lineWidth: 1))
.offset(x:10,y:-10)
Spacer()
}
if self.showCategory{
let categoryArr = ["肉类","蔬菜类","饮料类","调味品类"]
ZStack{
Rectangle().fill(Color.gray)
.opacity(0.5)
VStack(){
Spacer(minLength: 0);
HStack{
Spacer()
Button(action: {
self.showCategory=false;
}, label: {
Text("Done")
}).frame(alignment: .trailing).offset(x:-15,y:15)
}
Picker(selection: $category,label: EmptyView()) {
ForEach(0..<categoryArr.count){ number in
Text(categoryArr[number]).tag(categoryArr[number])
}
}.labelsHidden()
} .frame(minWidth: 300, idealWidth: 300, maxWidth: 300, minHeight: 250, idealHeight: 100, maxHeight: 250, alignment: .top).fixedSize(horizontal: true, vertical: true)
.background(RoundedRectangle(cornerRadius: 27).fill(Color.white.opacity(1)))
.overlay(RoundedRectangle(cornerRadius: 27).stroke(Color.black, lineWidth: 1))
Spacer()
}.offset(x:10,y:20)
}
}
}.animation(.easeInOut)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you read my code carefully, there are some variables are missing referencing. That's because I pasted part of the code that relates to my issue.
Food Class
//
// Food.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/23/20.
//
import Foundation
class Food: Identifiable {
init(id:Int, name: String, count: Int, category: String){
self.id=id;
self.name=name;
self.count=count;
self.category=category;
}
var id: Int
var name: String
var count: Int
var category: String
}
Fridge class
//
// Fridge.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/27/20.
//
import Foundation
class Fridge: ObservableObject{
init(){
db=DBhelper();
let result = setIndex(database: db!);
self.index = result.1;
self.container=result.0;
}
var db:DBhelper?
var index : Int?
#Published var container : [Food]?;
func setIndex(database: DBhelper) -> ([Food],Int){
let foodList : [Food] = database.read();
var index=0;
for food in foodList{
index = max(food.id,index);
}
return (foodList,(index+1));
}
func updateindex(index: inout Int){
index=index+1;
}
func testExist(){
if let data = db {
print("hi")
}
else{
print("doesnt exist")
}
}
func addFood(food:Food){
self.db!.insert(id: self.index!, name: food.name, count:food.count, category: food.category);
self.container!.append(food);
}
}
Because you implemented a new NaviagtionView in AddFoodView. Simply remove this and it should work. Look at the link you provided. There is no NavigationView in the child.
Correct me if Im wrong but the core code parts here that produce this issue are as follows:
Here you start:
struct ContentView: View {
#State var pushed: Bool = false
// Deleted other vars
var body: some View {
TabView(selection: $selection) {
NavigationView {
List(fridge.container!){
food in NavigationLink(destination: FoodView()) {
Text("HI")
}
}.navigationBarTitle(Text("Fridge Items"), displayMode: .inline)
.navigationBarItems(trailing:
// Here you navigate to the child view
NavigationLink(destination: AddFoodView(pushed: self.$pushed),isActive: self.$pushed) {
Image(systemName: "plus.circle").resizable().frame(width: 22, height: 22)
}.isDetailLink(false) )
}
Here you land and want to go back to root:
struct AddFoodView: View{
#Binding var pushed : Bool
// Deleted the other vars for better view
var body: some View{
ZStack{
NavigationView{ // <-- remove this
VStack{
Button (action: {
// here you'd like to go back
self.pushed = false;
} ){
Text("save").foregroundColor(Color.blue).font(.system(size: 18,design: .default)) }
}.navigationBarTitle("Three")
}
For the future:
I have the feeling you might have troubles with the navigation in general.
Actually it is really simple:
You implement one NavigationView at the "root" / start of your navigation.
From there on you only use NavigationLinks to go further down to child pages. No NavigationView needed anymore.
I'm having trouble with a bug in the TextField Keyboard.
When I tap the corresponding textField, the Keyboard appears, but something like a white View appears together and the TextField is hidden. (See image)
Xcode12 Iphone11-Ios13.5 Simulator doesn't have this bug, but Ios14 does. Does anyone know a solution?
struct PlaceholderTextField: View {
var placeholderTxt: String
var keyboardType: UIKeyboardType?
#Binding var text: String
var body: some View {
ZStack(alignment: .trailing) {
VStack(alignment: .leading) {
VStack {
if self.keyboardType != nil {
TextField(self.placeholderTxt, text: $text)
.autocapitalization(.none)
.padding(20)
.keyboardType(self.keyboardType!)
} else {
TextField(self.placeholderTxt, text: $text)
.autocapitalization(.none)
.padding(20)
}
}
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 15))
.padding()
}
}
}
}
struct LoginView: View {
#ObservedObject(initialValue: LoginViewModel()) var loginController: LoginViewModel
#EnvironmentObject var userData: UserData
#State var emailLogin: Bool = true
#Environment(\.presentationMode) var presentation
#Binding var rootIsActive: Bool
var body: some View {
ZStack {
GeometryReader { bodyView in
ZStack {
Color.backgroundColor.edgesIgnoringSafeArea(.all)
VStack(spacing: 0) {
SwitchAccountIDButton(emailLogin: self.$emailLogin)
self.userIdTextField
self.passwordTextField
Button(action: {
self.userData.isLoading = true
if self.emailLogin {
self.loginController.singin(self.loginController.email, self.loginController.password, self.emailLogin)
} else {
self.loginController.singin(self.loginController.phoneNumber, self.loginController.password, self.emailLogin)
}
}) {
ButtonView(title: "ログイン", fontColor: .white, bgColor: Color.primaryColor, width: bodyView.size.width * 0.9)
.accessibility(identifier: "login_login_button")
}
NavigationLink(destination: ReissuePassword(shouldPopToRootView: self.$rootIsActive)) {
Text("パスワードを忘れた場合")
.underline()
.foregroundColor(Color.primaryColor)
.padding(.top)
}.isDetailLink(false)
Spacer()
}
}
}
.navigationBarTitle("ログイン")
.navigationBarBackButtonHidden(true)
.navigationBarItems(trailing: VStack {
Button(action: {
self.presentation.wrappedValue.dismiss()
}, label: { Text("キャンセル").foregroundColor(Color.primaryColor).fontWeight(.regular) })
})
}
}
var userIdTextField: some View {
VStack {
if self.emailLogin {
PlaceholderTextField(placeholderTxt: "メールアドレス",keyboardType: .default ,text: self.$loginController.email)
.accessibility(identifier: "login_mailaddress_textfield")
} else {
PlaceholderTextField(placeholderTxt: "電話番号", keyboardType: .phonePad, text: self.$loginController.phoneNumber)
.accessibility(identifier: "login_phonenumber_textfield")
}
}
}
}
I want to hide keyboard when tapped the list background, but onTapGesture will cover NavigationLink. Is this a bug or have a better solution?
struct ContentView: View {
#State var text: String = ""
var body: some View {
NavigationView {
List {
NavigationLink("NextPage", destination: Text("Page"))
TextField("Placeholder", text: $text)
}
.onTapGesture {
// hide keyboard...
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
}
}
Thanks!
update
Thanks to asperi, and I found an alternative way: just put it in section header. As for style, we should create a custom ButtonStyle for NavigationLink.
Here is an example of using InsetGroupedListStyle.
struct ContentView: View {
#State var text: String = ""
var body: some View {
NavigationView {
List {
Section(
header: NavigationLink(destination: Text("Page")) {
HStack {
Text("NextPage")
.font(.body)
.foregroundColor(Color.primary)
Spacer()
Image(systemName: "chevron.forward")
.imageScale(.large)
.font(Font.caption2.bold())
.foregroundColor(Color(UIColor.tertiaryLabel))
}
.padding(.vertical, 12)
.padding(.horizontal)
}
.textCase(nil)
.buttonStyle(CellButtonStyle())
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding(.horizontal, -16)
) {}
TextField("Placeholder", text: $text)
}.listStyle(InsetGroupedListStyle())
.onTapGesture {
// hide keyboard...
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
}
}
struct CellButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(
configuration.isPressed
? Color(UIColor.systemGray5)
: Color(UIColor.secondarySystemGroupedBackground)
)
}
}
Here is a possible direction to solve this - by making all taps handled simultaneously and navigate programmatically. Tested with Xcode 12 / iOS 14.
truct ContentView: View {
#State var text: String = ""
var body: some View {
NavigationView {
List {
MyRowView()
TextField("Placeholder", text: $text)
}
.simultaneousGesture(TapGesture().onEnded {
// hide keyboard...
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
})
}
}
}
struct MyRowView: View {
#State private var isActive = false
var body: some View {
NavigationLink("NextPage", destination: Text("Page"), isActive: $isActive)
.contentShape(Rectangle())
.onTapGesture {
DispatchQueue.main.async { // maybe even with some delay
self.isActive = true
}
}
}
}