SwiftUI: detecting when #State variable has change in their #Binding - ios

I'm trying to figure how can change the value from different View. Here is the implementation of my main view:
import SwiftUI
import Combine
struct ContentView: View {
#State private var isPresented: Bool = false
#State private var textToProces = "" {
didSet{
print("text: oldValue=\(oldValue) newValue=\(textToProces)")
}
}
var body: some View {
ZStack{
VStack{
Button("Show Alert"){
self.isPresented = true
}.background(Color.blue)
}
ItemsAlertView(isShown: $isPresented, textToProcess: $textToProces)
}
}
}
On this view I'm trying to change the textToProces variable:
struct AnotherView: View {
#Binding var isShown: Bool
#Binding var textToProcess: String
var title: String = "Add Item"
let screenSize = UIScreen.main.bounds
var body: some View {
VStack {
Button(action: {
self.textToProcess = "New text"
self.isShown = false
}, label: {
Text("dissmis")
})
Text(self.textToProcess)
}
.background(Color.red)
.offset(y: isShown ? 0 : screenSize.height)
}
}
When I change the value on this line self.textToProcess = "New text" the textToProcess in the main view never gets the notification of the change. What I can I do to get the notification of the change in the main view any of you knows?
I'll really appreciate your help

You have to use the onChange modifier to track changes to textToProces.
import SwiftUI
import Combine
struct ContentView: View {
#State private var isPresented: Bool = false
#State private var textToProces = ""
var body: some View {
ZStack{
VStack{
Button("Show Alert"){
self.isPresented = true
}.background(Color.blue)
}
ItemsAlertView(isShown: $isPresented, textToProcess: $textToProces)
}
.onChange(of: textToProces) { value in
print("text: \(value)")
}
}
}

Related

Swift Closure containing control flow statement cannot be used with result builder 'CommandsBuilder'

I am getting some errors in my code and am unsure why and cannot find any answers online. The errors occur in my ContentView file.
Here is my ContentView:
import SwiftUI
struct ContentView: View {
// Log Status..
#AppStorage("log_status") var log_Status: Bool = false
var body: some View {
Group{
if log_Status{
MainPage()
}
else{
OnBoardingPage()
}
}
}
}
On the line with the if statement I get the error:
Closure containing control flow statement cannot be used with result builder 'CommandsBuilder'
On the line with var body I get the errors:
Return type of property 'body' requires that 'EmptyCommands' conform to 'View'
Struct 'ViewBuilder' requires that 'EmptyCommands' conform to 'View'
The code for MainPage:
import SwiftUI
struct MainPage: View {
#StateObject var appModel: AppViewModel = .init()
#StateObject var sharedData: SharedDataModel = SharedDataModel()
#Binding var shirtData : Shirt
#Binding var showDetailShirt: Bool
#Binding var trouserData : Trouser
#Binding var showDetailTrouser: Bool
#Namespace var animation
init(shirtData: Binding<Shirt>, showDetailShirt: Binding<Bool>, trouserData: Binding<Trouser>, showDetailTrouser: Binding<Bool>){
self._shirtData = shirtData
self._showDetailShirt = showDetailShirt
self._trouserData = trouserData
self._showDetailTrouser = showDetailTrouser
UITabBar.appearance().isHidden = true
}
var body: some View {
VStack {
TabView(selection: $appModel.currentTab) {
Marketplace(animation: _animation, charityData: $charityData, showDetailCharity: $showDetailCharity, businessData: $businessData, showDetailBusiness: $showDetailBusiness)
.environmentObject(sharedData)
.tag(Tab.Market)
.setUpTab()
Profile()
.environmentObject(sharedData)
.tag(Tab.Profile)
.setUpTab()
Page3()
.environmentObject(sharedData)
.tag(Tab.Page3)
.setUpTab()
Page4()
.environmentObject(sharedData)
.tag(Tab.Page4)
.setUpTab()
Page5()
.environmentObject(sharedData)
.tag(Tab.Page5)
.setUpTab()
}
.overlay(alignment: .bottom) {
CustomTabBar(currentTab: $appModel.currentTab, animation: animation)
.offset(y: appModel.showDetailViewTab ? 150 : 0)
}
}
}
}
// MARK: Custom Extensions
extension View{
#ViewBuilder
func setUpTab()->some View{
self
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Here is the code for OnboardingPage:
import SwiftUI
struct OnBoardingPage: View {
#State var showLoginPage: Bool = false
var body: some View {
VStack {
Text("Welcome")
Image("Photo1")
Button {
withAnimation{
showLoginPage = true
}
} label: {
Text("Get started")
}
}
.overlay(
Group{
if showLoginPage{
LoginPage()
}
}
)
}
}
extension View{
func getRect()->CGRect{
return UIScreen.main.bounds
}
}
Maybe I'm missing arguments for parameters for MainPage in the if statement in ContentView? If this is the case how would that be written?

Missing arguments for parameters even when initialised

I am trying to call a detailed view of an item when an item is tapped. In this case, the item is a pair of trousers in the MarketplaceTrouserView. When I call TrouserDetailView, I get an error. This must be to do with initialising but I've repeatedly tried this and failed. What could be the solution?
MarketplaceTrouserView:
import SwiftUI
struct MarketplaceTrouserView: View {
#StateObject var MarketplaceModel = MarketplaceViewModel()
#State private var selectedMarketplaceFilter: MarketplaceFilterViewModel = .trouser
#State var showDetailTrouser = false
#State var selectedTrouser : Trouser!
#EnvironmentObject var sharedData: SharedDataModel
var body: some View {
var columns = Array(repeating: GridItem(.flexible()), count: 2)
ZStack{
VStack{
HStack {
Text("Find Trousers To Buy")
}
}
}
if MarketplaceModel.trousers.isEmpty{
ProgressView()
}
else{
ScrollView {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(),spacing: 10), count: 2),spacing: 20){
ForEach(MarketplaceModel.filteredTrouser){trouser in
// Trouser items in grid view
TrouserView(trouserData: trouser)
.onTapGesture {
withAnimation {
selectedTrouser = trouser
showDetailTrouser.toggle()
}
}
}
}
}
}
}
if selectedTrouser != nil && showDetailTrouser{
TrouserDetailView(/*Here is the error asking for trouserData & showDetailTrouser*/)
}
}
}
}
TrouserDetailView:
import SwiftUI
import SDWebImageSwiftUI
struct TrouserDetailView: View {
#State var trouserData : Trouser
#State var showDetailTrouser: Bool
#EnvironmentObject var sharedData: SharedDataModel
#EnvironmentObject var marketplaceData: MarketplaceViewModel
var body: some View {
ScrollView {
VStack{
HStack {
Button(action: {
withAnimation(.easeOut){showDetailTrouser.toggle()}
}) {
Image(systemName: "arrow.backward.circle.fill")
}
Text(trouserData.trouser_name)
}
VStack {
WebImage(url: URL(string: trouserData.trouser_image))
}
}
}
}
Trouser Model:
import SwiftUI
import FirebaseFirestoreSwift
import Firebase
struct Trouser: Identifiable, Codable {
#DocumentID var id: String?
var trouser_name: String = ""
var trouser_image: String = ""
}
The error is when i call TrouserDetailView (as marked in the code)
After applying changes, my MainPage view has errors in the initialiser.
MainPage
struct MainPage: View {
#StateObject var appModel: AppViewModel = .init()
#StateObject var sharedData: SharedDataModel = SharedDataModel()
#State var shirtData : Shirt
#State var showDetailShirt: Bool
#State var trouserData : Trouser
#State var showDetailTrouser: Bool
#Namespace var animation
init () {
#State var shirtData = Shirt()
#State var showDetailShirt = false
#State var trouserData = Trouser()
#State var showDetailTrouser = false
UITabBar.appearance().isHidden = true
} //ERROR HERE: 'Return from initializer without initializing all stored properties'
var body: some View {
VStack {
TabView(selection: $appModel.currentTab) {
Marketplace()
.environmentObject(sharedData)
.tag(Tab.Market)
.setUpTab()
Home()
.environmentObject(sharedData)
.tag(Tab.Home)
.setUpTab()
}
By the logic of provided code, state is not needed for the first one and second should be replaced with binding, ie.
struct TrouserDetailView: View {
var trouserData : Trouser
#Binding var showDetailTrouser: Bool
// ...
and call
if selectedTrouser != nil && showDetailTrouser {
TrouserDetailView(trouserData: selectedTrouser,
showDetailTrouser: $showDetailTrouser)
}
#State var trouserData : Business
#State var showDetailTrouser: Bool
These variables you must initialize the value locally or switch to #Binding to bind the data with the parent variables.

SwiftUI view parameter does not update as expected

I am curious why this .fullScreenCover display of a view does not update properly with a passed-in parameter unless the parameter is using the #Binding property wrapper. Is this a bug or intended behavior? Is this the fact that the view shown by the fullScreenCover is not lazily generated?
import SwiftUI
struct ContentView: View {
#State private var showFullScreen = false
#State private var message = "Initial Message"
var body: some View {
VStack {
Button {
self.message = "new message"
showFullScreen = true
} label: {
Text("Show Full Screen")
}
}.fullScreenCover(isPresented: $showFullScreen) {
TestView(text: message)
}
}
}
struct TestView: View {
var text: String
var body: some View {
Text(text)
}
}
There is a different fullScreenCover for passing in dynamic data, e.g.
import SwiftUI
struct CoverData: Identifiable {
var id: String {
return message
}
let message: String
}
struct FullScreenCoverTestView: View {
#State private var coverData: CoverData?
var body: some View {
VStack {
Button {
coverData = CoverData(message: "new message")
} label: {
Text("Show Full Screen")
}
}
.fullScreenCover(item: $coverData, onDismiss: didDismiss) { item in
TestView(text: item.message)
.onTapGesture {
coverData = nil
}
}
}
func didDismiss() {
// Handle the dismissing action.
}
}
struct TestView: View {
let text: String
var body: some View {
Text(text)
}
}
More info and an example in the docs:
https://developer.apple.com/documentation/SwiftUI/AnyView/fullScreenCover(item:onDismiss:content:)

How to use #State if #Binding not provided in the initializer

Imagine a view with some #Binding variables:
init(isEditing: Binding<Bool>, text: Binding<Bool>)
How can we have the selection working with an internal #State if it is not provided in the initializer?
init(text: Binding<Bool>)
This is how to make TextField become first responder in SwiftUI
Note that I know we can pass a constant like:
init(isEditing: Binding<Bool> = .constant(false), text: Binding<Bool>)
But!
This will kill the dynamicity of the variable and it won't work as desire. Imagine re-inventing the isFirstResponder of the UITextField.
It can't be .constant(false). The keyboard will be gone on each view update.
It can't be .constant(true). The view will take the keyboard on each view update.
Maybe! Apple is doing it somehow with TabView.
One solution is to pass an optional binding and use a local state variable if the binding is left nil. This code uses a toggle as an example (simpler to explain) and results in two interactive toggles: one being given a binding and the other using local state.
import SwiftUI
struct ContentView: View {
#State private var isOn: Bool = true
var body: some View {
VStack {
Text("Special toggle:")
SpecialToggle(isOn: $isOn)
.padding()
SpecialToggle()
.padding()
}
}
}
struct SpecialToggle: View {
/// The binding being passed from the parent
var isOn: Binding<Bool>?
/// The fallback state if the binding is left `nil`.
#State private var defaultIsOn: Bool = true
/// A quick wrapper for accessing the current toggle state.
var toggleIsOn: Bool {
return isOn?.wrappedValue ?? defaultIsOn
}
init(isOn: Binding<Bool>? = nil) {
if let isOn = isOn {
self.isOn = isOn
}
}
var body: some View {
Toggle(isOn: isOn ?? $defaultIsOn) {
Text("Dynamic label: \(toggleIsOn.description)")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You may create separate #State and #Binding properties and sync them using onChange or onReceive:
struct TestView: View {
#State private var selectionInternal: Bool
#Binding private var selectionExternal: Bool
init() {
_selectionInternal = .init(initialValue: false)
_selectionExternal = .constant(false)
}
init(selection: Binding<Bool>) {
_selectionInternal = .init(initialValue: selection.wrappedValue)
_selectionExternal = selection
}
var body: some View {
if #available(iOS 14.0, *) {
Toggle("Selection", isOn: $selectionInternal)
.onChange(of: selectionInternal) {
selectionExternal = $0
}
} else {
Toggle("Selection", isOn: $selectionInternal)
.onReceive(Just(selectionInternal)) {
selectionExternal = $0
}
}
}
}
struct ContentView: View {
#State var selection = false
var body: some View {
VStack {
Text("Selection: \(String(selection))")
TestView(selection: $selection)
TestView()
}
}
}

So I built an app that's suppose to get the words per minute. How do you start a timer that's not in ContentView with SwithUI

I want to start a timer of 60 seconds to test how many words a user can type within that minute. I started counting the characters within the TextField. But Now I need to decrement a timer so I can do the math and print out the answer to the user. I can't seem to figure out how to use the timer when it's not in the Content View struct though. Can I do that?
import SwiftUI
struct ContentView: View {
#State var userInput = ""
#State var modalview = false
#State var getstarted = false
#EnvironmentObject var timerHolder : TimerHolder
var body: some View {
ZStack() {
modalView(modalview: $modalview, userInput: userInput)
}.sheet(isPresented: $modalview) {
modalView(modalview: self.$modalview)
}
}
}
struct modalView : View {
#ObservedObject var durationTimer = TimerHolder()
#Binding var modalview : Bool
#State var userInput: String = ""
var body: some View {
VStack{
Button(action: {
self.modalview = true
}) {
TextField("Get Started", text:$userInput)
.background(Color.gray)
.foregroundColor(.white)
.frame(width: 300, height: 250)
.cornerRadius(20)
Text("\(userInput.count)")
Text("\(durationTimer.count) Seconds")
}
}
}
}
class TimerHolder : ObservableObject {
var timer : Timer!
#Published var count = 0
func start() {
self.timer?.invalidate()
self.count = 0
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
_ in
self.count += 1
print(self.count)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
The simples one, as you hold it as property, is to start in .onAppear... (supposing, of course, that you pass it in ContentView().environmentObject(TimerHolder()) on ContentView creation)
struct ContentView: View {
#State var userInput = ""
#State var modalview = false
#State var getstarted = false
#EnvironmentObject var timerHolder : TimerHolder
var body: some View {
ZStack() {
modalView(modalview: $modalview, userInput: userInput)
}.sheet(isPresented: $modalview) {
modalView(modalview: self.$modalview)
}
.onAppear {
self.timerHolder.start()
}
}
}

Resources