Making a combine passthrough publisher less global - ios

Swift 5, iOS 13
I want to use passthroughSubject publisher; but I my gut tells me its a global variable and as such very poor practice. How can make this global variable less global, while still being usable. Here's some code to show what I talking about.
I know there are a dozen other ways to do this, but I wanted to create some simple code to illustrate the issue.
import SwiftUI
import Combine
let switcher = PassthroughSubject<Void,Never>()
struct SwiftUIViewF: View {
#State var nextPage = false
var body: some View {
VStack {
Text("Switcher")
.onReceive(switcher) { (_) in
self.nextPage.toggle()
}
if nextPage {
Page1ViewF()
} else {
Page2ViewF()
}
}
}
}
struct Page1ViewF: View {
var body: some View {
Text("Page 1")
.onTapGesture {
switcher.send()
}
}
}
struct Page2ViewF: View {
var body: some View {
Text("Page 2")
.onTapGesture {
switcher.send()
}
}
}
struct SwiftUIViewF_Previews: PreviewProvider {
static var previews: some View {
SwiftUIViewF()
}
}

Here is possible solution - to hold it in parent and inject into child views:
struct SwiftUIViewF: View {
let switcher = PassthroughSubject<Void,Never>()
#State var nextPage = false
var body: some View {
VStack {
Text("Switcher")
.onReceive(switcher) { (_) in
self.nextPage.toggle()
}
if nextPage {
Page1ViewF(switcher: switcher)
} else {
Page2ViewF(switcher: switcher)
}
}
}
}
struct Page1ViewF: View {
let switcher: PassthroughSubject<Void,Never>
var body: some View {
Text("Page 1")
.onTapGesture {
self.switcher.send()
}
}
}
struct Page2ViewF: View {
let switcher: PassthroughSubject<Void,Never>
var body: some View {
Text("Page 2")
.onTapGesture {
self.switcher.send()
}
}
}

An example using #EnvironmentObject.
Let SDK take care of observing / passing things for you, rather than setting up yourself.
Especially when your usage is a simple toggle.
import SwiftUI
import Combine
final class EnvState: ObservableObject { #Published var nextPage = false }
struct SwiftUIViewF: View {
#EnvironmentObject var env: EnvState
var body: some View {
VStack {
Text("Switcher")
if env.nextPage {
Page1ViewF()
} else {
Page2ViewF()
}
}
}
}
struct Page1ViewF: View {
#EnvironmentObject var env: EnvState
var body: some View {
Text("Page 1")
.onTapGesture {
env.nextPage.toggle()
}
}
}
struct Page2ViewF: View {
#EnvironmentObject var env: EnvState
var body: some View {
Text("Page 2")
.onTapGesture {
env.nextPage.toggle()
}
}
}
struct SwiftUIViewF_Previews: PreviewProvider {
static var previews: some View {
SwiftUIViewF().environmentObject(EnvState())
}
}

Related

How to properly implement a global variable in SwiftUI

I am going to create a SwiftUI application where I want to be able to swap between 3 modes. I am trying EnvironmentObject without success. I am able to change the view displayed locally, but from another View (in the end will be a class) I get a
fatal error: No ObservableObject of type DisplayView found. A View.environmentObject(_:) for DisplayView may be missing as an ancestor of this view.
Here is my code. The first line of the ContentView if/else fails.
enum ViewMode {
case Connect, Loading, ModeSelection
}
class DisplayView: ObservableObject {
#Published var displayMode: ViewMode = .Connect
}
struct ContentView: View {
#EnvironmentObject var viewMode: DisplayView
var body: some View {
VStack {
if viewMode.displayMode == .Connect {
ConnectView()
} else if viewMode.displayMode == .Loading {
LoadingView()
} else if viewMode.displayMode == .ModeSelection {
ModeSelectView()
} else {
Text("Error.")
}
TestView() //Want this to update the var & change UI.
}
.environmentObject(viewMode)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(DisplayView())
}
}
//FAILS
struct TestView: View {
#EnvironmentObject var showView: DisplayView
var body: some View {
HStack {
Button("-> load") {
self.showView.displayMode = .Loading
}
}
}
}
struct ConnectView: View {
var body: some View {
Text("Connect...")
}
}
struct LoadingView: View {
var body: some View {
Text("Loading...")
}
}
struct ModeSelectView: View {
var body: some View {
Text("Select Mode")
}
}
I would like to be able to update DisplayView from anywhere and have the ContentView UI adapt accordingly. I can update from within ContentView but I want to be able update from anywhere and have my view change.
I needed to inject BEFORE - so this fixed things up:
#main
struct fooApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(DisplayView()) //super key!
}
}
}
I also tried a Singleton class to store some properties - and thus they are available from anywhere and can be updated anywhere - without having to declare EnvironmentObject. It's just another way that can work in different circumstances.
class PropContainerModel {
public var foo = "Hello"
static let shared = PropContainerModel()
private override init(){}
}
And then somewhere else
let thisFoo = PropContainerModel.shared.foo
//
PropContainerModel.shared.foo = "There"
Update here (Singleton but changes reflect in the SwiftUI UI).
class PropContainerModel: ObservableObject
{
#Published var foo: String = "Foo"
static let shared = PropContainerModel()
private init(){}
}
struct ContentView: View
{
#ObservedObject var propertyModel = PropContainerModel.shared
var body: some View {
VStack {
Text("foo = \(propertyModel.foo)")
.padding()
Button {
tapped(value: "Car")
} label: {
Image(systemName:"car")
.font(.system(size: 24))
.foregroundColor(.black)
}
Spacer()
.frame(height:20)
Button {
tapped(value: "Star")
} label: {
Image(systemName:"star")
.font(.system(size: 24))
.foregroundColor(.black)
}
}
}
func tapped(value: String)
{
PropContainerModel.shared.foo = value
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Switching Views with button do not work at all

can someone please help me with this issue? I'm trying to detect the issue for like 2 hours now.
I want to switch view from LoginView() to RegistrationView() when button("Test") in LoginView() is pressed.
I did try this solution but it does nothing.
Any help is welcomed...
i have no idea how to change to registrationView()
struct mainView: View {
#State var show = false
var body: some View {
return Group {
if show {
RegistrationView()
}
}
}
}
struct LoginView : View {
#Binding var show: Bool
#StateObject private var loginVM = LoginViewModel()
var body: some View {
ZStack {BGimg()
if !loginVM.isAuthenticated {
VStack() {
.ignoresSafeArea(.all)
UsernameTextField(username: $loginVM.username)
PasswordSecureField(password: $loginVM.password)
ForgotPassword()
Button(action: loginVM.login) {
LoginButtonContent()
})
Button("Test") {
show.toggle()
}
}
.navigationBarHidden(false)
}
else if loginVM.isAuthenticated {
Home()
.navigationBarItems(trailing:
Button(action: loginVM.signout) {
Text("Sign out")
}
)
}
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
.previewDevice("iPhone 12 Pro")
}
}

swiftui subview reappear after click the back button and update state data

Very strange behavior.
Click the back button on the subpage (Subview) to return to the main page (ContentView). However, the subpage (Subview) automatically opens again. Why?
import SwiftUI
struct ContentView: View {
#State var things: [String] = []
#State var count: Int = 0
var body: some View {
NavigationView{
List {
ForEach(things.indices, id: \.self) { index in
Text(things[index])
}
}
.onAppear {
update()
}
.navigationTitle("a")
.toolbar{
NavigationLink(destination: Subview(count: $count), label: {
Text("sub")
})
}
}
}
func update() {
things = []
for i in 0...count {
things.append(String(i))
}
}
}
struct Subview: View {
var count : Binding<Int>
var body: some View {
Text("sub")
.onAppear {
count.wrappedValue += 1
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
NavigationLink should always be inside a NavigationView. If you put it in the toolbar or some other place, you might run into weird issues.
Instead, use the init(destination:isActive:label:) initializer. Then set the presentingNextPage property to true when you want to present the next page.
struct ContentView: View {
#State var things: [String] = []
#State var count: Int = 0
#State var presentingNextPage = false
var body: some View {
NavigationView {
List {
ForEach(things.indices, id: \.self) { index in
Text(things[index])
}
/// placeholder navigation link
NavigationLink(destination: Subview(count: $count), isActive: $presentingNextPage) {
EmptyView()
}
}
.onAppear {
self.update()
}
.navigationTitle("a")
.toolbar{
ToolbarItem(placement: .navigationBarTrailing) {
Button("sub") {
presentingNextPage = true /// set to true
}
}
}
}
}
func update() {
things = []
for i in 0...count {
things.append(String(i))
}
}
}
Result:
Put "onAppear{...}" on the NavigationView not the List, like this:
struct ContentView: View {
#State var things: [String] = []
#State var count: Int = 0
var body: some View {
NavigationView{
List {
ForEach(things.indices, id: \.self) { index in
Text(things[index])
}
}
.navigationTitle("a")
.toolbar{
NavigationLink(destination: Subview(count: $count), label: {
Text("sub")
})
}
}
.onAppear { // <---
update()
}
}

Value of Selected Option From a SwiftUI Picker does not Update the View

I have the following in a SwiftUI app. Basically I have some settings (Settings class) that I would like to use throughout the app. I have a Settings view that shows a picker to select the value of one of the settings. And other views of the app would only use the current set value of the settings. The following setup works in the sense that in ContentView I see the correct value of firstLevel setting. But the problem is that in SettingsView, I think since selectedFirstLevel is not a #State, its correct value is not shown on the picker I navigate to select either even or odd (oddly, the first time it's correct). This selection is carried correctly to ContentView, but it's not shown correctly on SettingsView. How can I fix this issue?
Settings.swift
import Foundation
class Settings: ObservableObject {
static let shared: Settings = Settings()
#Published var firstLevel: FirstLevel = .even
}
enum FirstLevel: String, CaseIterable, Identifiable {
case even
case odd
var id: String { self.rawValue }
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State private var showSettings: Bool = false
#ObservedObject var settings = Settings.shared
var body: some View {
VStack {
SettingsButton(showSettings: $showSettings, settings: settings)
Text(settings.firstLevel.id)
.padding()
}
}
}
struct SettingsButton: View {
#Binding var showSettings: Bool
var settings: Settings
var firstLevel: Binding<FirstLevel> {
return Binding<FirstLevel>(
get: {
return self.settings.firstLevel
}) { newFirstLevel in
self.settings.firstLevel = newFirstLevel
}
}
var body: some View {
Button(action: { self.showSettings = true }) {
Image(systemName: "gear").imageScale(.large)
}
.sheet(isPresented: $showSettings) {
SettingsView(selectedFirstLevel: self.firstLevel)
}
}
}
SettingsView.swift
import SwiftUI
struct SettingsView: View {
#Binding var selectedFirstLevel: FirstLevel
var body: some View {
NavigationView {
Form {
Picker("First Level", selection: $selectedFirstLevel) {
ForEach(FirstLevel.allCases) { level in
Text(level.rawValue).tag(level)
}
}
}
.navigationBarTitle("Settings", displayMode: .inline)
}
}
}
It looks overcomplicated, moreover Binding is unreliable as communication between different view hierarchies (which is sheet in your case).
Here is simplified and worked variant. Tested with Xcode 12 / iOS 14.
struct ContentView: View {
#ObservedObject var settings = FLevelSettings.shared
var body: some View {
VStack {
SettingsButton(settings: settings)
Text(settings.firstLevel.id)
.padding()
}
}
}
struct SettingsButton: View {
#State private var showSettings: Bool = false
var settings: FLevelSettings
var body: some View {
Button(action: { self.showSettings = true }) {
Image(systemName: "gear").imageScale(.large)
}
.sheet(isPresented: $showSettings) {
FLevelSettingsView(settings: self.settings)
}
}
}
struct FLevelSettingsView: View {
#ObservedObject var settings: FLevelSettings
var body: some View {
NavigationView {
Form {
Picker("First Level", selection: $settings.firstLevel) {
ForEach(FirstLevel.allCases) { level in
Text(level.rawValue).tag(level)
}
}
}
.navigationBarTitle("Settings", displayMode: .inline)
}
}
}
Note: it can be even more simplified, if you want, due to presence of FLevelSettings.shared, so you can use it inside FLevelSettingsView directly. Just in case.

How to bind objects using MVVM design pattern?

I'm trying to change below code(1.) to MVVM architecture(2.) but can't figure out how to bind objects.
I think the problem is FirstView only pass value but not actually a Binding Object, I tried some different ways but always stuck on don't know how to assign Binding Object to #Published.
Could somebody please give a few hints?
(Works fine)
struct FirstView: View {
#State var showSecondView = false
var body: some View {
Button(action: {
self.showSecondView.toggle()
}) {
Text("Show second view")
}
.sheet(isPresented: $showSecondView) {
SecondView(showSecondView: self.$showSecondView)
}
}
}
struct SecondView: View {
#Binding var showSecondView: Bool
var body: some View {
Button(action: {
self.showSecondView.toggle()
}) {
Text("Dismiss")
}
}
}
(MVVM)
struct FirstView: View {
#ObservedObject var vm = FirstViewModel()
var body: some View {
Button(action: {
self.vm.showSecondView.toggle()
}) {
Text("Show second view")
}
.sheet(isPresented: $vm.showSecondView) {
SecondView2(vm: SecondViewModel(showSecondView: self.vm.showSecondView))
}
}
}
class FirstViewModel: ObservableObject {
#Published var showSecondView = false
}
struct SecondView: View {
#ObservedObject var vm: SecondViewModel
var body: some View {
Button(action: {
self.vm.showSecondView.toggle()
}) {
Text("Dismiss")
}
}
}
class SecondViewModel: ObservableObject {
#Published var showSecondView: Bool
//Right here, i'm not sure how to bind 'showSecondView' from FirstView
init(showSecondView: Bool) {
self.showSecondView = showSecondView
}
}
You can pass directly the Binding<Bool> to the second VM but in this way, there's no need to have #Published var and it also doesn't need to be ObservableObject and marked as #ObservedObject. There might be a better solution to this.
struct FirstView: View {
#ObservedObject var vm = FirstViewModel()
var body: some View {
Button(action: {
self.vm.showSecondView.toggle()
}) {
Text("Show second view")
}
.sheet(isPresented: $vm.showSecondView) {
SecondView(vm: SecondViewModel(showSecondView: self.$vm.showSecondView))
}
}
}
class FirstViewModel: ObservableObject {
#Published var showSecondView = false
}
struct SecondView: View {
var vm: SecondViewModel
var body: some View {
Button(action: {
self.vm.showSecondView.wrappedValue.toggle()
}) {
Text("Dismiss")
}
}
}
class SecondViewModel {
var showSecondView: Binding<Bool>
init(showSecondView: Binding<Bool>) {
self.showSecondView = showSecondView
}
}

Resources