SwiftUI onReceive is it possible to get oldValue? - swiftui

I have a custom TabView and I want to Bind to a State to change tabs. I also want to detect if the user has tapped the same tab again in order to scroll to the top of that view.
didSet isn't called when I use a binding. onChange isn't called because the value hasn't changed, and onReceive doesn't give me the old value to compare.
Any ideas? (Trying to avoid using a published property)
struct ContentView: View {
#State private var scrollToTop1: Bool = false
#State private var scrollToTop2: Bool = false
#State private var selectedTab: Int = 1
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selectedTab) {
NavigationView {
View1(scrollToTop: $scrollToTop1)
}
.tag(1)
NavigationView {
View2(scrollToTop: $scrollToTop2)
}
.tag(2)
}
.onReceive(Just(selectedTab)) { [oldValue = selectedTab] newValue in
print("Old: \(oldValue)") //Shows newValue
print("New: \(newValue)")
if oldValue == newValue {
switch selectedTab {
case 1:
scrollToTop1.toggle()
case 2:
scrollToTop2.toggle()
default:
break
}
}
}
TabBar(selectedTab: $selectedTab)
}
}
}
struct TabBar: View {
#Binding var selectedTab: Int
var body: some View {
HStack {
TabItem(selectedTab: $selectedTab, text: "View 1", tab: 1)
TabItem(selectedTab: $selectedTab, text: "View 2", tab: 2)
}
.background(Color.green)
}
}
struct TabItem: View {
#Binding var selectedTab: Int
let text: String
let tab: Int
var body: some View {
Button {
selectedTab = tab
} label: {
Text(text)
}
.frame(maxWidth: .infinity)
.frame(height: 50)
}
}

I think this is a great scenario for a custom Binding, where you can intercept the value before its set and compare it:
struct ContentView: View {
#State private var scrollToTop1: Bool = false
#State private var scrollToTop2: Bool = false
#State private var selectedTab: Int = 1
var customBinding: Binding<Int> {
.init {
selectedTab
} set: { newValue in
print("New value: ", newValue)
if newValue == selectedTab {
print("Scroll to top")
}
selectedTab = newValue
}
}
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: customBinding) {
NavigationView {
Text("1")
}
.tag(1)
NavigationView {
Text("2")
}
.tag(2)
}
TabBar(selectedTab: customBinding)
}
}
}
struct TabBar: View {
#Binding var selectedTab: Int
var body: some View {
HStack {
TabItem(selectedTab: $selectedTab, text: "View 1", tab: 1)
TabItem(selectedTab: $selectedTab, text: "View 2", tab: 2)
}
.background(Color.green)
}
}

Related

Can't transfer variable from one watchos view to another view, Using swiftui

I am trying to get data from one view to another.
I can not figure out how to get values from the fourth view array into the Third view.
I am not using storyboards. I tried using #EnvironmentObject but can not make it work. New to coding. In xcode I am using watchos without app.
I tried to strip out most of the code and leave just the important stuff that can be tested. I used NavigationLink(destination: )to transfer between views.
enter code here
class viewFromEveryWhere: ObservableObject {
#Published var testname2: String = "testTTname"
}
struct secondView: View {
var body: some View {
Text("second view")
List(1..<7) {
Text("\($0)")
}
}
}
struct thirdView: View {
#EnvironmentObject var testname2: viewFromEveryWhere
#EnvironmentObject var testSixTestArray: viewFromEveryWhere
#State var sixTestArray:[String] = ["ww","GS","DW","CD","TS","JW",]
var body: some View {
List(sixTestArray, id:\.self) {
Text($0)
}
}
}
struct fourthView: View {
#StateObject var testname2 = viewFromEveryWhere()
#State private var name: String = ""
#State var testSixTestArray:[String] = []
func collectName () {
print("collectName triggered")
if testSixTestArray.count < 5 {
// testSixTestArray.append(name)
print(name)
print(testSixTestArray)
}
// .enviromentObject(testSixTestArray)
}
var body: some View {
VStack(alignment: . leading) {
Text("Type a name")
TextField("Enter your name", text: $name)
Text("Click to add, \(name)!")
// Button("click this if \(name) is correct") {}
Button(action:{
print("Button Tapped")
collectName()
print(testSixTestArray.count)
name = ""
}) {
Text("Add \(name) to list")
}
// .buttonStyle(GrowingButton1())
}
Text("forth view")
// testSixTestArray.append(name)
.environmentObject(testname2)
}
}
/*func presentTextInputControllerWithSuggestions(forLanguage suggestionsHandler:
((String)-> [Any]?)?,
allowedInputMode inputMode:
WKTextInputMode,
completion: #escaping ([Any]?) -> Void) {}
*/
struct ContentView: View {
#State var sixNameArray:[String] = ["","","","","","",]
#State var messageTextBox: String = "Start"
#State var button1: String = "Button 1"
#State var button2: String = "Button 2"
#State var button3: String = "Button 3"
var body: some View {
NavigationView {
VStack{
Text(messageTextBox)
.frame(width: 120, height: 15, alignment: .center)
.truncationMode(.tail)
.padding()
NavigationLink(destination: secondView(),
label:{
Text(button1)
})
.navigationBarTitle("Main Page")
NavigationLink(destination: thirdView(),
label:{
Text(button2)
})
NavigationLink(destination: fourthView(),
label:{
Text(button3)
})
}
}
}
}
enter code here

How to observe updates in binded values SwiftUI

I'm not sure if I created my custom TextField properly, because I am unable to observe the value changes to an #Binded text. Running the following code, you may observe that print(text) is not executed when you manually enter text into the text field.
import SwiftUI
#main
struct TestOutWeirdTextFieldApp: App {
#State var text: String = "" {
didSet {
print(text)
}
}
var body: some Scene {
WindowGroup {
StandardTextField(placeholderText: "Enter text", defaultText: $text)
}
}
}
struct StandardTextField: View {
#State var placeholderText: String {
didSet {
print(#line)
print(placeholderText)
}
}
#Binding var defaultText: String {
didSet {
print(#line)
print(defaultText)
}
}
#State var systemImage: String?
#State var underlineColor: Color = .accentColor
#State var edges: Edge.Set = .all
#State var length: CGFloat? = nil
#State var secure: Bool = false
var body: some View {
HStack {
if secure {
SecureField(placeholderText, text: $defaultText)
.foregroundColor(underlineColor)
} else {
TextField(placeholderText, text: $defaultText)
.foregroundColor(underlineColor)
}
if let systemImage = systemImage {
Image(systemName: systemImage)
.foregroundColor(.white)
}
}
.overlay(
Rectangle()
.frame(height: 2)
.padding(.top, 35)
)
.foregroundColor(underlineColor)
.padding(edges, length)
}
}
struct StandardTextView_Previews: PreviewProvider {
static var previews: some View {
StandardTextField(placeholderText: "Placement text", defaultText: .constant("")).previewLayout(.sizeThatFits)
}
}
Instead of didSet you need to use .onChange(of: modifier, like
HStack {
// ... your code here
}
.onChange(of: defaultText) { print($0) } // << this !!
.overlay(

How to Add multi text into the list in SwiftUI?(Data Flow)

I'm trying to build an demo app by swiftUI that get multi text from user and add them to the list, below , there is an image of app every time user press plus button the AddListView show to the user and there user can add multi text to the List.I have a problem to add them to the list by new switUI data Flow I don't know how to pass data.(I comment more information)
Thanks 🙏
here is my code for AddListView:
import SwiftUI
struct AddListView: View {
#State var numberOfTextFiled = 1
#Binding var showAddListView : Bool
var body: some View {
ZStack {
Title(numberOfTextFiled: $numberOfTextFiled)
VStack {
ScrollView {
ForEach(0 ..< numberOfTextFiled, id: \.self) { item in
PreAddTextField()
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView)
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
AddListView(showAddListView: .constant(false))
}
}
struct PreAddTextField: View {
// I made this standalone struct and use #State to every TextField text be independent
// if i use #Binding to pass data all Texfield have the same text value
#State var textInTextField = ""
var body: some View {
VStack {
TextField("Enter text", text: $textInTextField)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
// What should happen here to add Text to List???
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
#Binding var numberOfTextFiled : Int
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
numberOfTextFiled += 1
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
and for DataModel:
import SwiftUI
struct Text1 : Identifiable , Hashable{
var id = UUID()
var text : String
}
var textData = [
Text1(text: "SwiftUI"),
Text1(text: "Data flow?"),
]
and finally:
import SwiftUI
struct ListView: View {
#State var showAddListView = false
var body: some View {
NavigationView {
VStack {
ZStack {
List(textData, id : \.self){ text in
Text(text.text)
}
if showAddListView {
AddListView(showAddListView: $showAddListView)
.offset(y:-100)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
Because of the multiple-items part of the question, this becomes a lot less trivial. However, using a combination of ObservableObjects and callback functions, definitely doable. Look at the inline comments in the code for explanations about what is going on:
struct Text1 : Identifiable , Hashable{
var id = UUID()
var text : String
}
//Store the items in an ObservableObject instead of just in #State
class AppState : ObservableObject {
#Published var textData : [Text1] = [.init(text: "Item 1"),.init(text: "Item 2")]
}
//This view model stores data about all of the new items that are going to be added
class AddListViewViewModel : ObservableObject {
#Published var textItemsToAdd : [Text1] = [.init(text: "")] //start with one empty item
//save all of the new items -- don't save anything that is empty
func saveToAppState(appState: AppState) {
appState.textData.append(contentsOf: textItemsToAdd.filter { !$0.text.isEmpty })
}
//these Bindings get used for the TextFields -- they're attached to the item IDs
func bindingForId(id: UUID) -> Binding<String> {
.init { () -> String in
self.textItemsToAdd.first(where: { $0.id == id })?.text ?? ""
} set: { (newValue) in
self.textItemsToAdd = self.textItemsToAdd.map {
guard $0.id == id else {
return $0
}
return .init(id: id, text: newValue)
}
}
}
}
struct AddListView: View {
#Binding var showAddListView : Bool
#ObservedObject var appState : AppState
#StateObject private var viewModel = AddListViewViewModel()
var body: some View {
ZStack {
Title(addItem: { viewModel.textItemsToAdd.append(.init(text: "")) })
VStack {
ScrollView {
ForEach(viewModel.textItemsToAdd, id: \.id) { item in //note this is id: \.id and not \.self
PreAddTextField(textInTextField: viewModel.bindingForId(id: item.id))
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView, save: {
viewModel.saveToAppState(appState: appState)
})
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct PreAddTextField: View {
#Binding var textInTextField : String //this takes a binding to the view model now
var body: some View {
VStack {
TextField("Enter text", text: $textInTextField)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var save : () -> Void //callback function for what happens when "Add" gets pressed
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
save()
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
var addItem : () -> Void //callback function for what happens when the plus button is hit
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
addItem()
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
struct ListView: View {
#StateObject var appState = AppState() //store the AppState here
#State private var showAddListView = false
var body: some View {
NavigationView {
VStack {
ZStack {
List(appState.textData, id : \.self){ text in
Text(text.text)
}
if showAddListView {
AddListView(showAddListView: $showAddListView, appState: appState)
.offset(y:-100)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}

SwiftUI: button in ToolbarItem(placement: .principal) not work after change it's label

Xcode 12 beta 6
There is a button in toolbar, its label text is binding to a state var buttonTitle. I want to tap this button to trigger a sheet view, select to change the binding var.
After back to content view, the button's title is updated. But if you tap the button again, it not work.
Code:
struct ContentView: View {
#State var show = false
#State var buttonTitle = "button A"
var body: some View {
NavigationView {
Text("Hello World!")
.toolbar {
ToolbarItem(placement: .principal) {
Button {
show.toggle()
} label: {
Text(buttonTitle)
}
.sheet(isPresented: $show) {
SelectTitle(buttonTitle: $buttonTitle)
}
}
}
}
}
}
struct SelectTitle: View {
#Environment(\.presentationMode) var presentationMode
#Binding var buttonTitle: String
var body: some View {
Button("Button B") {
buttonTitle = "Button B"
presentationMode.wrappedValue.dismiss()
}
}
}
It is known toolbar-sheet layout issue, see also here. You can file another feedback to Apple.
Here is a workaround for your case - using callback to update toolbar item after sheet closed. Tested with Xcode 12b5.
struct ContentView: View {
#State var show = false
#State var buttonTitle = "button A"
var body: some View {
NavigationView {
Text("Hello World!")
.toolbar {
ToolbarItem(placement: .principal) {
Button {
show.toggle()
} label: {
Text(buttonTitle)
}
.sheet(isPresented: $show) {
SelectTitle(buttonTitle: buttonTitle) {
self.buttonTitle = $0
}
}
}
}
}
}
}
struct SelectTitle: View {
#Environment(\.presentationMode) var presentationMode
#State private var buttonTitle: String
let callback: (String) -> ()
init(buttonTitle: String, callback: #escaping (String) -> ()) {
_buttonTitle = State(initialValue: buttonTitle)
self.callback = callback
}
var body: some View {
Button("Button B") {
buttonTitle = "Button B"
presentationMode.wrappedValue.dismiss()
}
.onDisappear {
callback(buttonTitle)
}
}
}
Move sheet(...) outside of ToolbarItem scope like this:
NavigationView {
..
}.sheet(...)

SwiftUI how to perform action when EditMode changes?

I'd like to perform an action when the EditMode changes.
Specifically, in edit mode, the user can select some items to delete. He normally presses the trash button afterwards. But he may also press Done. When he later presses Edit again, the items that were selected previously are still selected. I would like all items to be cleared.
struct ContentView: View {
#State var isEditMode: EditMode = .inactive
#State var selection = Set<UUID>()
var items = [Item(), Item(), Item(), Item(), Item()]
var body: some View {
NavigationView {
List(selection: $selection) {
ForEach(items) { item in
Text(item.title)
}
}
.navigationBarTitle(Text("Demo"))
.navigationBarItems(
leading: EditButton(),
trailing: addDelButton
)
.environment(\.editMode, self.$isEditMode)
}
}
private var addDelButton: some View {
if isEditMode == .inactive {
return Button(action: reset) {
Image(systemName: "plus")
}
} else {
return Button(action: reset) {
Image(systemName: "trash")
}
}
}
private func reset() {
selection = Set<UUID>()
}
}
Definition of Item:
struct Item: Identifiable {
let id = UUID()
let title: String
static var i = 0
init() {
self.title = "\(Item.i)"
Item.i += 1
}
}
UPDATED for iOS 15.
This solution catches 2 birds with one stone:
The entire view redraws itself when editMode is toggle
A specific action can be performed upon activation/inactivation of editMode
Hopes this helps someone else.
struct ContentView: View {
#State var editMode: EditMode = .inactive
#State var selection = Set<UUID>()
#State var items = [Item(), Item(), Item()]
var body: some View {
NavigationView {
List(selection: $selection) {
ForEach(items) { item in
Text(item.title)
}
}
.navigationTitle(Text("Demo"))
.environment(\.editMode, self.$editMode)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
editButton
}
ToolbarItem(placement: .navigationBarTrailing) {
addDelButton
}
}
}
}
private var editButton: some View {
Button(action: {
self.editMode.toggle()
self.selection = Set<UUID>()
}) {
Text(self.editMode.title)
}
}
private var addDelButton: some View {
if editMode == .inactive {
return Button(action: addItem) {
Image(systemName: "plus")
}
} else {
return Button(action: deleteItems) {
Image(systemName: "trash")
}
}
}
private func addItem() {
items.append(Item())
}
private func deleteItems() {
for id in selection {
if let index = items.lastIndex(where: { $0.id == id }) {
items.remove(at: index)
}
}
selection = Set<UUID>()
}
}
extension EditMode {
var title: String {
self == .active ? "Done" : "Edit"
}
mutating func toggle() {
self = self == .active ? .inactive : .active
}
}
I was trying forever, to clear List selections when the user exited editMode. For me, the cleanest way I've found to react to a change of editMode:
Make sure to reference the #Environment variable:
#Environment(\.editMode) var editMode
Add a computed property in the view to monitor the state:
private var isEditing: Bool {
if editMode?.wrappedValue.isEditing == true {
return true
}
return false
}
Then use the .onChange(of:perform:) method:
.onChange(of: self.isEditing) { value in
if value == false {
// do something
} else {
// something else
}
}
All together:
struct ContentView: View {
#Environment(\.editMode) var editMode
#State private var selections: [String] = []
#State private var colors: ["Red", "Yellow", "Blue"]
private var isEditing: Bool {
if editMode?.wrappedValue.isEditing == true {
return true
}
return false
}
var body: some View {
List(selection: $selections) {
ForEach(colors, id: \.self) { color in
Text("Color")
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}
.onChange(of: isEditing) { value in
if value == false {
selection.removeAll()
}
}
}
}
In case someone want to use SwiftUI's EditButton() instead of custom a Button and still want to perform action when isEditing status changes
You can use View extension
extension View {
func onChangeEditMode(editMode: EditMode?, perform: #escaping (EditMode?)->()) -> some View {
ZStack {
Text(String(describing: editMode))
.opacity(0)
.onChange(of: editMode, perform: perform)
self
}
}
}
Then you can use it like this
struct TestEditModeView: View {
#Environment(\.editMode) var editMode
#State private var editModeDescription: String = "nil"
var body: some View {
VStack {
Text(editModeDescription)
EditButton()
}
.onChangeEditMode(editMode: editMode?.wrappedValue) {
editModeDescription = String(describing: $0)
}
}
}