I'd like to implement a navigator/router for an architecture implemented with SwiftUI and Combine. In a few words the View will share viewModel with Router. When the View triggers a change on the viewModel the Router should navigate to a new sheet.
This is a version of my code where I'm directly passing the viewModel from View to Router. Is there anything wrong? My biggest doubt is that since I'm using #ObservedObject on both the Router and the View, two different instances of the viewModel are created.
VIEW MODEL
class BootViewModel:ObservableObject{
#Published var presentSignIn = false
}
VIEW
struct BootView: View {
#ObservedObject var viewModel:BootViewModel
var navigator:BootNavigator<BootView>? = nil
init(viewModel:BootViewModel) {
self.viewModel = viewModel
self.navigator = BootNavigator(view: self, viewModel: viewModel)
self.navigator.setSubscriptions()
}
var body: some View {
VStack{
Text("Hello")
Button("Button"){
self.viewModel.presentSignIn.toggle()
}
}
}
}
NAVIGATOR
class BootNavigator<T:View>{
var view:T? = nil
#ObservedObject var viewModel:BootViewModel
init(view:T, viewModel:BootViewModel) {
self.view = view
self.viewModel = viewModel
}
func setSubscriptions(){
subscribe(onSigninPressed: $viewModel.presentSignIn)
}
func subscribe(onSigninPressed : Binding<Bool>){
_ = view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
}
}
}
Why the SignInView is never presented?
Without taking into account the fact that using a router with swiftUI is not needed in general(I'm mostly doing an exercise)... is there anything wrong with this implementation?
This
view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
MUST be somewhere in body (directly or via computed property or func) but inside body's ViewBuilder
Some notes I have to point out here:
ValueType
There is a difference between an UIView and a SwiftUI View. All SwiftUI Views are value type! So they get copied when you pass them around. Be aware of that.
Single instance
If you want a single instance like a regular navigator for your entire app, you can use singleton pattern. But there is a better approach in SwiftUI universe called #Environment objects. You can take advantage of that.
Trigger a view refresh
To refresh the view (including presenting something), you must code inside the var body. But it can be directly written on indirectly through a function or etc.
Related
Im currently injecting some SwiftUI view+viewModel files into a storyboard. The reasoning was so that in a future rewrite of the app to SwiftUI we already have some of the work done. In any case, I ended up creating a Hosting Controller, which injects my SwitfUI view file into the storyboard:
class LoginViewHostingController: UIHostingController<LoginView> {
required init?(coder: NSCoder) {
super.init(coder: coder,rootView: LoginView())
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
The problem is that the SwiftUI View file is using a ViewModel file (instance of ObservableObject) to perform a lot of its logic (mainly used for state).
struct LoginView: View {
#ObservedObject var loginVM = LoginViewModel()
var body: some View {
...and then the view will use the loginVM like this:
if (!loginVM.showPasswordView) {
HStack {
Button(action: loginVM.checkEmailForSSOAuthentication) {
Text("Next")
.font(.system(.headline, design: .rounded))
.foregroundColor(Color.white)
.padding(.vertical, 18)
.frame(width: 350)
.background(Color("DigideckPrimary"))
.cornerRadius(6)
}
}
.padding(.top, 10)
}
What I'm trying to do, just to confirm I can do it, is perform a segue on the storyboard that this SwiftUI view is injected in in this Next button's action (loginVM.checkEmailForSSOAuthentication) which I am not sure is possible.
I attempted this in my loginVM file:
func checkEmailForSSOAuthentication() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let storyBoardLoginVc = storyboard.instantiateViewController(withIdentifier: "myLoginController")
// instead of sending to self send to main storyboard if i can!
storyBoardLoginVc.performSegue(withIdentifier: "test1", sender: self)
return;
But receive the following error when this code executes:
2023-01-18 09:47:00.872611-0600 Digideck[35908:12697138] [Assert] UINavigationBar decoded as unlocked for UINavigationController, or navigationBar delegate set up incorrectly. Inconsistent configuration may cause problems. navigationController=<UINavigationController: 0x130879c00>, navigationBar=<UINavigationBar: 0x127e1ce70; frame = (0 0; 0 50); opaque = NO; autoresize = W; tintColor = UIExtendedSRGBColorSpace 0.0784314 0.392157 0.709804 1; gestureRecognizers = <NSArray: 0x6000010ab090>; layer = <CALayer: 0x600001ec0ea0>> delegate=0x130879c00
2023-01-18 09:47:00.873019-0600 Digideck[35908:12697138] [Presentation] Attempt to present <UINavigationController: 0x130879c00> on <Digideck.LoginViewHostingController: 0x10f00b600> (from <Digideck.LoginViewHostingController: 0x10f00b600>) whose view is not in the window hierarchy.
Which leads me to believe I instantiated a different storyboard rather than referencing the one already in view. If anyone knows if this is possible please let me know!
We don't use view model objects in SwiftUI so it's not the correct strategy to think you will be able to use them in UIKit and in SwiftUI. In SwiftUI the View struct is the view model already and the property wrappers like the #State give them reference type semantics, i.e. a view model object. Also this line is a mistake:
#ObservedObject var loginVM = LoginViewModel()
#ObservedObject has no ownership of the object so it is init and deinit with the View struct value type (every state change) so essentially you've created a memory leak. You need to use it like this:
#ObservedObject var model: Model
Where the Model has been init somewhere else like a singleton, or in the app delegate, i.e. it has an owner that is not a ephemeral struct value type.
I think it would be best to design your SwiftUI View to use #Binding var instead (if you need write access, otherwise just let).
In this down code I am using Binding in a custom View called TextView, which will not use this Binding in it's body, It will just sitting there.
The things get weird when we use onChange or PreferenceKey in our project that makes changing behavior of View for unused Binding in body. So here is what happens if we don't use onChange or PreferenceKey in our project and we update that unused Binding in our project that doesn't make the view get rendered and even we don't need to defined and conform to Equitable function to help SwiftUI to understand that it is the same View. It will work even without defining Equitable function or conforming to Equitable protocol. But when we use onChange or PreferenceKey on that unused Binding and even if we use and define Equitable and Equitable function for the View it will render it any way! Even it is not necessary so I don't know how can I solve this issue!?
struct ContentView: View {
#State private var string: String = "Hello" {
didSet {
print(string)
}
}
var body: some View {
EquatableView(content: TextView(string: $string))
Button("update") { string += " updated!" }
//.onChange(of: string) { newValue in } // <<: Here!!!
}
}
struct TextView: View, Equatable {
#Binding var string: String
let value: String = "123"
var body: some View {
print("rendering TextView!", "- - - - - - ")
return Text(value)
}
static func == (lhs: TextView, rhs: TextView) -> Bool {
print("Equatable function used!")
return lhs.value == rhs.value
}
}
When you add .onChange(of: string) { ... } you are referencing string in body of ContentView, and if the value changes, body of ContentView will be executed - and thus body of TextView, too.
In your setup without .onChange you are not referencing the value of string in the body of ContentView and changes to the value do not cause ContentView to be redrawn. And with- or without EquatableView, body of TextView should not be called when clicking the button.
In your pseudo code, I think, you do not need EquatableView - because there's nothing which could be optimised, and SwiftUI's default behaviour works well.
So far, it works as expected.
Now your question boils down to: how can I avoid to redraw a child view if the super view changes? (read: how to not execute body of a child view, if body of the super view will be executed)
Well, I think, SwiftUI can do optimisations - but I don't believe it can generally omit re-drawing a child view, when the super view changes and has been redrawn.
And now, when I think about it, you may use "ContentView" as the argument to EquatableView. Since, ContenView (presumably) does not depend on string.
I have created my own custom classes for UIKit objects. now i want to use same classes in SwiftUI, How can i achieve that and how much effort it will take.
Also if i want i will need to write same classes in swift UI.
example, I have custom UILable subclass WMLabel.
class WMLabel: UILabel {
var myproperty: String = "" {
didSet {
self.text = myproperty
}
}
}
so how can i use WMLabel in swiftUI?
I have tried ObserverableObject and UIViewRepresentable, but not able to access the properties.
You can definitely use your UIKit classes. To get basic access to the properties, you'll want to be looking at makeUIView, which occurs when the view is first created and updateUIVew.
Using your example code:
class WMLabel: UILabel {
var myproperty: String = "" {
didSet {
self.text = myproperty
}
}
}
struct WMLabelRepresented: UIViewRepresentable {
var text : String
func makeUIView(context: Context) -> WMLabel {
return WMLabel()
}
func updateUIView(_ uiView: WMLabel, context: Context) {
uiView.myproperty = text
}
}
struct ContentView : View {
var body: some View {
WMLabelRepresented(text: "My text")
}
}
If there are things that can't be expressed declaratively, you'll want to look into coordinators and as you mentioned, possible an ObservableObject to communicate data imperatively to your view, but often you can find ways to express most things declaratively.
If you want an example of more complex imperative communication, here's a couple of links to another answers of mine:
https://stackoverflow.com/a/65926143/560942
https://stackoverflow.com/a/66845387/560942
Converting all of your custom classes and interfacing with them is going to be one heck of a chore if you have a few of them. You would end up using something called UIViewRepresentable which requires quite a few things, the most annoying of which called a coordinator. You'd almost be better off rewriting your classes into a SwiftUI version. Here's Apple's documentation on interfacing SwiftUI with UIKit: https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
Here's an example of conversion of that UILabel into SwiftUI, with accessible properties.
Example Conversion
struct WMLabel: View {
var myProperty: String
var body: some View {
Text(myProperty)
}
}
Example Usage
struct Example: View {
var body: some View {
WMLabel(myProperty: "Hello World!")
}
}
As you can see, there is little code involved in converting something to SwiftUI, if you start getting involved in UIViewRepresentable you have to start playing with coordinators and a bunch of other interfacing methods just to make it work. Sometimes it's required, but in most cases I'd try and avoid it.
I have a series of View Controllers which pass a struct model object down the chain.
If a user modifies the value of a property on the model, I update the view controller's model instance, and now I need to inform the parent view controllers that this object's value has changed.
Previously I would have used classes over structs for my model object and so I wouldn't have this issue as the object would have been directly written to.
But since structs are pass by value, I have to update the state on other view controllers. I have been using a singleton Manager object to handle state changes through a call to updateModel(). Is there a better way?
I have used something similar to this; keep a reference to the neighbouring view controller (with care to avoid a reference cycle) and a property observer on the struct property to update it when it changes.
This could also be updated prior to presenting a new view controller or before a segue, depending on your needs.
class myViewController: UIViewController {
// Your struct
var model: MyStruct? {
didSet {
if let pvc = previousVC {
pvc.model = model
}
}
}
// Keep a reference to the previous view controller on your stack
var previousVC: UIViewController?
override viewDidLoad() {
super.viewDidLoad()
self.model = MyStruct()
}
}
In Swift, this crashes at runtime:
class EmptyData: BindableObject {
let didChange = PassthroughSubject<EmptyData, Never>()
}
struct RandomView : View {
#EnvironmentObject var emptyData: EmptyData
#EnvironmentObject var emptyData2: EmptyData
var body: some View {
Text("Hello World!")
}
}
and in the SceneDelegate.swift:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let window = UIWindow(frame: UIScreen.main.bounds)
// The emptyData variables are not initialized as seen below
window.rootViewController = UIHostingController(rootView: RandomView())
self.window = window
window.makeKeyAndVisible()
}
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
Fixing the problem isn't that hard, but rather strange:
window.rootViewController = UIHostingController(rootView: RandomView().environmentObject(EmptyData()))
So what's happening here? I pass EmptyData() and SwiftUI decides that both emptyData and emptyData2 should be initialized with the same object reference? I can pass also other environmentobjects that do not even exists as variables in the RandomView instance:
window.rootViewController = UIHostingController(rootView: RandomView().environmentObject(EmptyData()).environmentObject(SomeData()))
And SwiftUI just happily run, although SomeData() isn't used anywhere in the instance of RandomView() and should trigger a compile time error in my opinion.
Why are uninitialized values permitted at compile time without initializing them when initializing the object and why are we free to pass environment instances without doing anything with them? Looks a bit like Javascript to me, I loved the strong static safe typing in Swift... I don't see right away why the member-wise initializer just generates an initializer which takes the environment variables as it's parameter.
The EnvironmentObject property delegate has an init() method taking no parameters, and that provides an implicit initialization for the wrapped properties
#EnvironmentObject var emptyData: EmptyData
#EnvironmentObject var emptyData2: EmptyData
(this is explained in the Modern Swift API Design video roughly at 28:10). So that is why these (non-optional) properties do not need an (explicit) initial value.
The documentation also states that EnvironmentObject is (emphasis added)
... a dynamic view property that uses a bindable object supplied by an ancestor view to invalidate the current view whenever the bindable object changes.
You must set a model object on an ancestor view by calling its environmentObject(_:) method.
So this is how I understand it:
If a matching bindable object (in your case: an instance of EmptyData) is found in the environment of the current view or one of its ancestors then the properties are initialized to this object.
If no matching bindable object if found in an ancestor view then the program terminates with a runtime error.
Environment objects can be used in all, some, or none of the views in the view hierarchy. (See Data Flow Through SwiftUI at 29:20.) Therefore it is not an error to provide an environment object (in your case: an instance of SomeData) which is not used in RandomView.
What is #EnvironmentObject?
A linked View property that reads a BindableObject supplied by an
ancestor
So, the environment prop can be supplied to children from the ancestor, not necessarily it should come from its immediate parent.
With that, take a look at the below snippet, since RandomViewGrandParent injects the required Env objects into the environment, RandomViewParent doesn't have to do anything if the children of RandomViewParent needs same Env obj. RandomViewParent can just initiate view without passing the env obj again.
class EmptyData: BindableObject {
let didChange = PassthroughSubject<EmptyData, Never>()
}
struct RandomViewGrandParent : View {
var body: some View {
RandomViewParent().environmentObject(EmptyData())
}
}
struct RandomViewParent : View {
#EnvironmentObject var emptyData: EmptyData
#EnvironmentObject var emptyData2: EmptyData
var body: some View {
RandomView()
}
}
struct RandomView : View {
#EnvironmentObject var emptyData: EmptyData
#EnvironmentObject var emptyData2: EmptyData
var body: some View {
Text("Hello World!")
}
}
And to ans your another question -
I pass EmptyData() and SwiftUI decides that both emptyData and
emptyData2 should be initialized with the same object reference?
That's because EnvironmentObject conforms to BindableObject and BindableObject's didChange is a Publisher, so I believe it thinks both emptyData and emptyData2 wants to subscribe to the same events/values hence uses the same ref for both.