I am using this for saving user info in user defaults for user isRegsitered.
struct UserKeys {
static let isUserRegistered = "isUserRegistered"
}
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
final class UserSettings: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#UserDefault(UserKeys.isUserRegistered, defaultValue: false)
var isUserRegistered: Bool {
willSet {
objectWillChange.send()
}
}
}
// The Popup Login window on Same Dashboard View.
struct PopUpWindow: View {
#ObservedObject var userRepo = UserSettings()
Button(action: {
// Dismiss the PopUp
withAnimation(.linear(duration: 0.3)) {
userRepo.isUserRegistered = true
show = false
hideKeyboard()
}
}, label: {
Text("Submit")
& further I am listing in DashBoardView View as -
struct DashBoardView: View {
#ObservedObject var userRepo = UserSettings()
private var content: some View {
if !userRepo.isUserRegistered {
return PopUpWindow(show: .constant(true)) .eraseToAnyView()
}
// .. Other conditions ..
So basically I have popup with Login box so after submit I am trying to dismiss dialog & want to listen value userRepo.isUserRegistered
By initializing UserSettings class independently in both PopUpWindow and DashBoardView, you are actually saying that each view is managing its own data, hence the two views are not sharing the same data.
You should not initialize your UserSettings class from the PopUpWindow view, but instead, pass an instance to from DashBoardView.
The code would look like this:
PopUpWindow
struct PopUpWindow: View {
#ObservedObject var userRepo: UserSettings
Button(action: {
// Dismiss the PopUp
withAnimation(.linear(duration: 0.3)) {
userRepo.isUserRegistered = true
show = false
hideKeyboard()
}
}, label: {
Text("Submit")
// .. Other conditions ..
and then :
DashBoardView
struct DashBoardView: View {
#ObservedObject var userRepo = UserSettings()
private var content: some View {
if !userRepo.isUserRegistered {
return PopUpWindow(userRepo: userRepo, show: .constant(true)) .eraseToAnyView()
}
// .. Other conditions ..
Related
I have 2 views where the
first view passes list of items and selected item in that to second view and
second view returns the updated selected item if user changes.
I am getting error 'Type of expression is ambiguous without more context' when i am sending the model property 'idx'.
//I cant make any changes to this model so cant confirm it with ObservableObject or put a bool property like 'isSelected'
class Model {
var idx: String?
....
}
class FirstViewModel: ObservableObject {
var list: [Model]
#Published var selectedModel: Model?
func getSecondViewModel() -> SecondViewModel {
let vm2 = SecondViewModel( //error >> Type of expression is ambiguous without more context
list: list,
selected: selectedModel?.idx // >> issue might be here but showing at above line
)
return vm2
}
}
struct FirstView: View {
#ObservableObject firstViewModel: FirstViewModel
var body: some View {
..
.sheet(isPresented: $showView2) {
NavigationView {
SecondView(viewModel: firstViewModel.getSecondViewModel())
}
}
..
}
}
class SecondViewModel: ObservableObject {
var list: [Model]
#Published var selected: String?
init(list: [Model], selected: Published<String?>) {
self.list = list
_selected = selected
}
func setSelected(idx: String) {
self.selected = idx
}
}
struct SecondView: View {
#ObservableObject secondViewModel: SecondViewModel
#Environment(\.presentationMode) var presentationMode
var body: some View {
...
.onTapGesture {
secondViewModel.setSelected(idx: selectedIndex)
presentationMode.wrappedValue.dismiss()
}
...
}
}
In case if I am sending 'Model' object directly to the SecondViewModel its working fine. I need to make changes the type and couple of other areas and instantiate the SecondViewModel as below
let vm2 = SecondViewModel(
list: list,
selected: _selectedModel
)
Since I need only idx I don't want to send entire model.
Also the reason for error might be but not sure the Model is #Published and the idx is not.
Any help is appreciated
Here is some code, in keeping with your original code that allows you to
use the secondViewModel as a nested model.
It passes firstViewModel to the SecondView, because
secondViewModel is contained in the firstViewModel. It also uses
firstViewModel.objectWillChange.send() to tell the model to update.
My comment is still valid, you need to create only one SecondViewModel that you use. Currently, your func getSecondViewModel() returns a new SecondViewModel every time you use it.
Re-structure your code so that you do not need to have nested ObservableObjects.
struct Model {
var idx = ""
}
struct ContentView: View {
#StateObject var firstMdl = FirstViewModel()
var body: some View {
VStack (spacing: 55){
FirstView(firstViewModel: firstMdl)
Text(firstMdl.secondViewModel.selected ?? "secondViewModel NO selected data")
}
}
}
class FirstViewModel: ObservableObject {
var list: [Model]
#Published var selectedModel: Model?
let secondViewModel: SecondViewModel // <-- here only one source of truth
// -- here
init() {
self.list = []
self.selectedModel = nil
self.secondViewModel = SecondViewModel(list: list, selected: nil)
}
// -- here
func getSecondViewModel() -> SecondViewModel {
secondViewModel.selected = selectedModel?.idx
return secondViewModel
}
}
class SecondViewModel: ObservableObject {
var list: [Model]
#Published var selected: String?
init(list: [Model], selected: String?) { // <-- here
self.list = list
self.selected = selected // <-- here
}
func setSelected(idx: String) {
selected = idx
}
}
struct FirstView: View {
#ObservedObject var firstViewModel: FirstViewModel // <-- here
#State var showView2 = false
var body: some View {
Button("click me", action: {showView2 = true}).padding(20).border(.green)
.sheet(isPresented: $showView2) {
SecondView(firstViewModel: firstViewModel)
}
}
}
struct SecondView: View {
#ObservedObject var firstViewModel: FirstViewModel // <-- here
#Environment(\.dismiss) var dismiss
#State var selectedIndex = "---> have some data now"
var body: some View {
Text("SecondView tap here to dismiss").padding(20).border(.red)
.onTapGesture {
firstViewModel.objectWillChange.send() // <-- here
firstViewModel.getSecondViewModel().setSelected(idx: selectedIndex) // <-- here
// alternatively
// firstViewModel.secondViewModel.selected = selectedIndex
dismiss()
}
}
}
Fairly new to SwiftUI and trying to figure out how to use ViewModels. Coming from UIKit I tend to like binding button presses to view model events, then apply the business logic and return a new value.
I am trying this in SwiftUI:
struct MainView: View {
#ObservedObject private var viewModel: MainViewModel
#State private var isShowingBottomSheet = false
var body: some View {
VStack {
Text("Hello \(viewModel.username)")
.font(.title)
Button("Show bottom sheet") {
isShowingBottomSheet = true
}
.sheet(isPresented: $isShowingBottomSheet) {
let viewModel = SheetViewModel()
viewModel.event.usernameUpdated
.assign(to: &$viewModel.username)
SheetView(viewModel: viewModel)
.presentationDetents([.fraction(0.15), .medium])
}
}
}
// MARK: - Initializers
init(viewModel: MainViewModel) {
self.viewModel = viewModel
}
}
With the view model:
final class MainViewModel: ObservableObject {
// MARK: - Properties
#Published var username = "John"
}
And SheetView:
struct SheetView: View {
#ObservedObject private var viewModel: SheetViewModel
var body: some View {
VStack {
Text("Some Sheet")
.font(.title)
Button("Change Name") {
viewModel.event.updateUsernameButtonTapped.send(())
}
}
}
// MARK: - Initializers
init(viewModel: SheetViewModel) {
self.viewModel = viewModel
}
}
And SheetViewModel:
final class SheetViewModel: ObservableObject {
// MARK: - Events
struct Event {
let updateUsernameButtonTapped = PassthroughSubject<Void, Never>()
let usernameUpdated = PassthroughSubject<String, Never>()
}
// MARK: - Properties
let event = Event()
private var cancellables = Set<AnyCancellable>()
// MARK: - Binding
private func bindEvents() {
event.updateUsernameButtonTapped
.map { "Sam" }
.sink { [weak self] name in
self?.event.usernameUpdated.send(name)
}
.store(in: &cancellables)
}
}
I am getting the error Cannot convert value of type 'Binding<String>' to expected argument type 'Published<String>.Publisher'. I want my SheetViewModel to update the value of #Published var username in the MainViewModel. How would I go about this?
We usually don't need view model objects in SwiftUI which has a design that benefits from value semantics, rather than the more error prone reference semantics of UIKit. If you want to move logic out of the View struct you can group related state vars and mutating funcs in their own struct, e.g.
struct ContentView: View {
#State var config = SheetConfig()
var body: some View {
VStack {
Text(config.text)
Button(action: show) {
Text("Edit Text")
}
}
.sheet(isPresented: $config.isShowing,
onDismiss: didDismiss) {
TextField("Text", $config.text)
}
}
func show() {
config.show(initialText: "Hello")
}
func didDismiss() {
// Handle the dismissing action.
}
}
struct SheetConfig {
var text = ""
var isShowing = false
mutating func show(initialText: String) {
text = initialText
isShowing = true
}
}
If you want to persist/sync data, or use Combine then you will need to resort to the reference type version of state which is #StateObject. However if you use the new async/await and .task then it's possible to still not need it.
I've included stubbed code samples. I'm not sure how to get this presentation to work. My expectation is that when the sheet presentation closure is evaluated, aDependency should be non-nil. However, what is happening is that aDependency is being treated as nil, and TheNextView never gets put on screen.
How can I model this such that TheNextView is shown? What am I missing here?
struct ADependency {}
struct AModel {
func buildDependencyForNextExperience() -> ADependency? {
return ADependency()
}
}
struct ATestView_PresentationOccursButNextViewNotShown: View {
#State private var aDependency: ADependency?
#State private var isPresenting = false
#State private var wantsPresent = false {
didSet {
aDependency = model.buildDependencyForNextExperience()
isPresenting = true
}
}
private let model = AModel()
var body: some View {
Text("Tap to present")
.onTapGesture {
wantsPresent = true
}
.sheet(isPresented: $isPresenting, content: {
if let dependency = aDependency {
// Never executed
TheNextView(aDependency: dependency)
}
})
}
}
struct TheNextView: View {
let aDependency: ADependency
init(aDependency: ADependency) {
self.aDependency = aDependency
}
var body: some View {
Text("Next Screen")
}
}
This is a common problem in iOS 14. The sheet(isPresented:) gets evaluated on first render and then does not correctly update.
To get around this, you can use sheet(item:). The only catch is your item has to conform to Identifiable.
The following version of your code works:
struct ADependency : Identifiable {
var id = UUID()
}
struct AModel {
func buildDependencyForNextExperience() -> ADependency? {
return ADependency()
}
}
struct ContentView: View {
#State private var aDependency: ADependency?
private let model = AModel()
var body: some View {
Text("Tap to present")
.onTapGesture {
aDependency = model.buildDependencyForNextExperience()
}
.sheet(item: $aDependency, content: { (item) in
TheNextView(aDependency: item)
})
}
}
I am trying to make a router object for my application using #EnvironmentObject. But the problem is the #Published property doesn't update the root view when the root view type is updated.
How it should work
A user clicks Sign in with Apple button
Update the router.currentPage property of #EnvironmentObject when a user logs in successfully.
RootView get notified for updating router.currentPage and change the root view in accordance to the updated currentPage type.
Here are my codes below.
MainApp.swift
var body: some Scene {
WindowGroup {
RootView().environmentObject(ViewRouter())
}
}
ViewRouter.swift
Code Block
enum Page {
case signin
case tasklist
}
final class ViewRouter: ObservableObject {
#Published var currentPage: Page = .signin
}
RootView.swift
struct RootView: View {
#EnvironmentObject var router: ViewRouter
var body: some View {
if router.currentPage == .signin {
SigninView()
} else {
TaskListView()
}
}
}
SigninView.swift
struct SigninView: View {
#EnvironmentObject var router: ViewRouter
#State var signInHandler: SignInWithAppleCoordinator?
var window: UIWindow? {
guard let scene = UIApplication.shared.connectedScenes.first,
let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate,
let window = windowSceneDelegate.window else {
return nil
}
return window
}
var body: some View {
MyAppleIDButton().colorScheme(.light)
.frame(width: 280, height: 38, alignment: .center)
.onTapGesture {
signInWithAppleButtonTapped()
}
}
func signInWithAppleButtonTapped() {
guard let _window = self.window else { return }
signInHandler = SignInWithAppleCoordinator(window: _window)
signInHandler?.signIn { (user) in
router.currentPage = .tasklist
}
}
}
Update
I think I found an answer to this issue.
I created a state isLoggedIn which is checking whether or not Sign in with Apple is done successfully.
#State var isLoggedIn: Bool = false
Then I added View Modifier onChange which is checking the value change of isLoggedIn above. Inside the onChange I assigned a new value to router.currentPage like below.
.onChange(of: isLoggedIn, perform: { isLoggedIn in
if isLoggedIn {
router.currentPage = .tasklist
} else {
router.currentPage = .signin
}
})
But I am still not sure of why it doesn't work in the closure of SigninWithApple button.
I have an issue with deep links in my SwiftUI app.
In my app class I have declared deepLink as an environment variable for every View under ContentView() in the hierarchy:
...
#main
struct TestApp: App {
var userSettings: UserSettings
var dataFetcher: DataFetcher
var dataUpdater: DataUpdater
#State var deepLink = ""
init() {
userSettings = UserSettings()
dataFetcher = DataFetcher(userSettings: userSettings)
dataUpdater = DataUpdater(userSettings: userSettings)
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(userSettings)
.environmentObject(dataFetcher)
.environmentObject(dataUpdater)
.onOpenURL { url in
deepLink = url.absoluteString
}
.environment(\.deepLink, deepLink)
}
}
}
In my ContentView() I've declared deepLink as an environment variable
struct ContentView: View {
...
#State var isTestSheetViewPresented = false
#Environment(\.deepLink) var deepLink: String
...
var body: some View {
Button(action: {
self.isTestSheetViewPresented = true
}, label: {
HStack {
Spacer()
Image(systemName: "plus")
Text("Add")
Spacer()
}
})
.sheet(isPresented: $isTestSheetViewPresented, content: {
TestSheetView(isPresented: self.$isTestSheetViewPresented)
})
.onChange(of: self.deepLink) { _ in
self.isTestSheetViewPresented = true
}
}
}
And the TestSheetView is like this
struct TestSheetView: View {
#Environment(\.deepLink) var deepLink: String
#State var url: String = ""
var body: some View {
Text(url)
.onChange(of: deepLink) { _ in
if deepLink != "" {
self.url = deepLink
}
}
}
}
The problem is that when I click on a link, and my app opens, the TestSheetView is correctly presented but the onChange is not triggered unless I scroll a little bit down the sheet.
Instead if I put the same code of the TestSheetView in the ContentView then the text is correctly shown
Seems like a timing issue. While TestSheetView is being initialized, the body is created after deepLink changed, so it won't be able to detect it.
The solution is to use onAppear in TestSheetView and read from there, like so:
struct TestSheetView: View {
#Environment(\.deepLink) var deepLink: String
#State var url: String = ""
var body: some View {
Text(url)
.onAppear {
if deepLink != "" {
self.url = deepLink
}
}
}
}
It's unergonomic to handle both the case where the view is yet to appear, and the case where a link is being navigated within the view. The following view modifier handles both cases. It assumes an .onOpenURL() handler in the top level navigating view that sets both the current tab selection, along with the currentDeepLink environment value.
struct DeepLinkViewModifier: ViewModifier {
#Environment(\.currentDeepLink) private var currentDeepLink
let action: ((URL) -> Bool)
func body(content: Content) -> some View {
content
.onAppear {
if let url = currentDeepLink.wrappedValue,
action(url) {
currentDeepLink.wrappedValue = nil
}
}
.onOpenURL { url in
_ = action(url)
}
}
}
extension View {
func onDeepLink(perform action: #escaping ((URL) -> Bool)) -> some View {
return self.modifier(DeepLinkViewModifier(action: action))
}
}
Use:
struct SomeView: View {
#State urlString: String = ""
var body: some View {
Text(urlString).onDeepLink { url
self.string = url.absoluteString
return true // return false if another handler should consume
}
}
}
struct ContentView: View {
#State private var tabSelection: TabSelection = .something
#State private var currentDeepLink: URL? = nil
var body: some View {
TabView(selection: self.$tabSelection) {
...
}
.onOpenURL { url in
self.tabSelection = ... // determine selection from URL
self.currentDeepLink = url
}
.environment(\.tabSelection, self.$tabSelection)
.environment(\.currentDeepLink, self.$currentDeepLink)
}
}