I'm building a CustomTextField View and wanting to add view modifiers.
Using the standard .modifier(CustomViewModifier) is working just fine. Now I want to build ViewModifier functions so that I can use my modifiers just like any other SwiftUI modifiers.
In my CustomTextField, I've got multiple methods like this:
func setCustomPlaceholder(placeholder: String = "Enter Text") -> some View {
modifier(CustomTextFieldPlaceholderViewModifier(viewModel: viewModel, placeholder: placeholder))
}
These methods work. However, I can only use one of these methods at a time.
I've read several articles and this thread. Because I don't want these modifiers available to any View, I'd like to avoid using an extension to View.
In this thread, one of the answers used an extension to Text. So I tried placing my modifier methods into an extension to my CustomTextField. However, I'm still facing the same issue. The View is only recognizing one modifier function at a time.
This is how I'm using it:
VStack {
CustomTextField()
.setCustomPlaceholder()
.setBorder()
}
If I use only one of the modifier methods at a time, it works.
How can I get the functionality I'm looking for? Do I need to build a Protocol and make my CustomTextField conform to it? Thanks for any help!
If I use only one of the modifier methods at a time, it works
That’s because the only thing known about the result of the first view modifier is that it returns a View (of an unspecified kind).
Since you haven’t defined your view modifier on View… you can’t call it on a View.
We can just make needed modifiers as our view member functions, which return own type to be called sequentially.
Here is a main part. Tested with Xcode 13.4 / iOS 15.5
struct CustomTextField: View {
// ... other code
func setCustomPlaceholder(placeholder: String = "Enter Text") -> Self { // << here Self !!
var tmp = self
tmp.placeholder = placeholder
return tmp
}
// ... other code
}
Complete code and demo in project
Related
I'm new to SwiftUI and trying to start using it in a complex, existing UIKit app. The app has a theming system, and I'm not sure how to get the SwiftUI view to respond to theme change events.
Our theme objects look like
class ThemeService {
static var textColor: UIColor { get }
}
struct ViewTheme: {
private(set) var textColor = { ThemeService.textColor }
}
where the value returned ThemeService.textColor changes when the user changes the app's theme. In the UIKit portions of the app, views observe a "themeChanged" Notification and re-read the value of the textColor property from their theme structs.
I'm not sure how to manage this for SwiftUI. Since ViewTheme isn't an object, I can't use #ObservableObject, but its textColor property also doesn't change when the theme changes; just the value returned by calling it changes.
Is there a way to somehow get SwiftUI to re-render the view hierarchy from an external event, rather than from a change in a value that the view sees? Or should I be approaching this differently?
Your answer works perfectly well, but it requires adoption of ObservableObject. Here is an alternative answer which uses your existing NotificationCenter notifications to update a SwiftUI view.
struct MyView: View {
#State private var textColor: UIColor
var body: some View {
Text("Hello")
.foregroundColor(Color(textColor))
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("themeChanged"))) { _ in
textColor = ThemeService.textColor
}
}
}
This requires a #State variable to hold the theme's current data, but it's correct because a SwiftUI view is really just a snapshot of what the view should currently display. It ideally should not reference data that is arbitrarily changed because it leads to data-sync problems like the question you asked. So in a SwiftUI view, it is problematic to write ThemeService.textColor directly within the body unless you are certain an update will always occur after it the theme gets changed.
I was able to get this working by cheating with the theming system a little and changing the ViewTheme to an object:
class ViewTheme: ObservableObject {
private(set) var textColor = { ThemeService.textColor }
init() {
NotificationCenter.default.addObserver(forName: Notification.Name("themeChanged"),
object: nil, queue: .main) { [weak self] _ in
self?.objectWillChange.send()
}
}
}
Now the view can mark it with #ObservedObject and will be re-generated when the "themeChanged" notification is fired.
This seems to be working great, but I'm not sure if there are non-obvious problems that I'm missing with this solution.
I'm reading SwiftUI materials and it's said that view modifiers for example:
struct ByeView: View {
var body: some View {
Text("Bye bye, world!")
.font(.headline)
}
}
creates a new view with .headline font and returns the view.
So I wonder if it's more like:
func font(_ font: UIFont) -> Text {
Text(text, font: font)
}
rather than:
func font(_ font: UIFont) -> Text {
self.font = font
return self
}
I feel for event modifiers, since they may not have to modify the view, there's no need to "create a new view with modified aspects", but not sure about the modifiers that do adjust the views.
Thanks!
First, note that an implementation such as
func font(_ font: UIFont) -> Text {
self.font = font
return self
}
does not compile. font would need to be mutating for this to work, but it isn't mutating. That said, this would have compiled:
self.someReferenceType.font = font
But this has another problem. This means that font now has a side effect:
var body: some View {
let x = Text("foo")
x.font(.headline)
return x // this should have non-headline font, but if the
// implementation above were used, it would have headline font
}
So I think it is very likely that the actual implementation involves calling the initialiser, rather than return self. This also matches the wording in the documentation's wording that a "new view" is "created".
For modifiers that return some View, you can check the type they return using type(of:). You will most likely see that the type they return is different from self, which means they definitely do not return self! After all, that's one of the reasons why the opaque type some View is used in the signature - to hide the internal types Apple used to implement these modifiers.
// doesn't work in a playground for some reason
// make a .swift file, compile and run
let x = Text("foo").onAppear {}
print(type(of: x))
For me, this prints: ModifiedContent<Text, _AppearanceActionModifier>, not Text, so clearly a new view is created here. (See also ModifiedContent)
TLDR
We can't know for sure as the implementation detail is hidden.
My point of view
I might be wrong but I think it's possible that neither of modifying an instance of a View or assigning to a property that you have described are actually happening, but we can't know for sure since the implementation is private.
Since SwiftUI is a declarative framework, where you describe what you want to get and the OS takes care of that, it could be even that by writing:
SomeView()
.padding()
.background(Color.red)
.cornerRadius(8)
Internally it could become just about any representation such as JSON that the parser could read.
Essentially no instance of any object (apart from the description) representing a View, Color or CornerRadius could exist before its initialization based on the description and current state, thus there would be no instance holding properties (such as font) that could be assigned or altered before the final View is initialized.
I added a ComposeView in my XML layout file. I use view binding to inflate this file in my Activity. When I try to call binding.myComposeView.setContent { ... } then I get the following compilation error: Unresolved reference: setContent. When I take a look at the generated binding file, the type of myComposeView is View and not ComposeView. When I use findViewById<ComposeView>(R.id.myComposeView).setContent { ... } then everything works fine. Why is the binding not generated correctly? What can I do to use view binding with a ComposeView?
It turns out that I had two versions of the same layout: portrait and horizontal. I converted the portrait one to Compose by replacing a LinearLayout with a ComposeView. However, in the horizontal layout myComposeView was still a LinearLayout. That's why the view binding class that got created had a field myComposeView of type View instead of ComposeView. The view with the same id had different types in two layout versions.
Maybe there is a problem with the way the binding is set up in onCreate of your activity. Are you using something along the lines of the following code? :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.composeView.setContent {
MaterialTheme {
Text(text = "Hello World")
}
}
}
I am trying to implement a design pattern like MVC in order to achieve low coupling between different parts of the code. There are few materials online that I personally didn't find helpful in relation to IOS or swift UI development and MVC pattern.
What I am trying to understand is how should the controller class control or render the UI in Swift UI ?
Following the MVC pattern for example - the View shouldn't know about how the model looks like, so sending an object back from the Data Base to the view in order to visually present it wouldn't be a good Idea..
Say we have the following View and Controller, how should I go about the interaction between the controller and view when sending the data back from the DB in order to visually present it in the view ?
View:
import SwiftUI
import Foundation
struct SwiftUIView: View {
var assignmentController = AssignmentController()
#State var assignmentName : String = ""
#State var notes : String = ""
var body: some View {
NavigationView {
VStack {
Form {
TextField("Assignment Name", text: $assignmentName)
TextField("Notes", text: $notes)
}
Button(action: {
self.assignmentController.retrieveFirstAssignment()
}) {
Text("Get The First Assignment !")
}
}
.navigationBarTitle("First Assignment")
}
}
}
Controller
var assignmentModel = AssignmentModel()
func retrieveFirstAssignment()
{
var firstAssignment : Assignment
firstAssignment=assignmentModel.retrieveFirstAssignment()
}
For now, it does nothing with the object it found.
Model
An object in the model composed of two String fields : "assignmentName" and "notes".
*We assume the assignment Model have a working function that retrieves one task from the DB in order to present it in the view.
struct SwiftUIView: View {
#State m = AssignmentModel()
var body: some View {
// use m
}
func loadAssignmentFromDB() {
m.retrieveFirstAssignment()
}
}
This is what I called "enhanced MVC with built-in MVVM".
It satisfies your pursuit of MVC and possibly MVVM, at the same time, with much less effort.
Now I'll argue why:
function, or more specifically, mutation is Control. you don't need an object called "Controller" to have C in MVC. Otherwise we might as well stick to UIKit for 10 more years.
this makes sense when your model is value type. nothing can mutate it without your specific say-so, e.g.; #State annotation. since the only way to mutate model is via these designated endpoints, your Control takes effect only in the function that mutates these endpoints.
Quoting from another reply:
It is true that SwiftUI is a much closer match with MVVM than with MVC. However, almost all example code in the Apple documentation is so simple the ViewModel (and/or Controller in MVC) is left out completely. Once you start creating bigger projects the need for something to bridge your Views and Models arises. However, IMO, the SwiftUI documentation does not (yet) fully address this in a satisfying way.
SwiftUI, if anything, enhances MVC. What is the purpose of having ViewModel?
a.) to have model view binding, which is present in my code snippet above.
b.) to manage states associated with object. if #State does not give you the impression that it is for you to manage state, then i don't know what will. it's funny how many MVVM devs are just blind to this. Just as you don't need View Controller for Control, you don't need ViewModel for VM. Design pattern is a #State of mind. Not about specific naming and rigid structure.
c.) say i'm open to MVVM without being based. which code snippet do you think have more chance in scaling to larger projects? my compact one or that suggested in another reply?
hint: think how many extra files, view models, observableobjects, glue codes, pass-around-vm-as-parameters, init function to accept vm as parameters, you are going to have. and these are just for you to write some of the code in another object. it says nothing about reducing or simplifying the task at hand. hell it does even tell you how to refactor your control codes, so you are most likely to just repeat whatever you did wrong in MVC all over again. did i mention ViewModel is a shared, reference type object with implicit state managements? so what's the point of having a value type model when you are just going to override it with a reference type model?
it's funny how MVVM devs say SwiftUI in its base form is not scalable to larger projects. keeping things simple is the only way to scale.
This is what I observed as the roadmap of dev progression in 2020.
Day1: beginner
Day2: google some, drop MVC
Day3: google some more, SwiftUI not scalable
Day4: OK, I need MVVM+RxSwift+Coordinator+Router+DependencyInjection to avoid SDK short-comings.
My suggestion, due to this seems like a common beginner question, is to learn to walk before you run.
I've personally seen RxSwift developers move controller code to view so that controller appears "clean", and need 3 third-party libraries (one is a custom fork) to send a http GET.
Design pattern means nothing if you can't get simple things simple.
To me this is a very good question. It is true that SwiftUI is a much closer match with MVVM than with MVC. However, almost all example code in the Apple documentation is so simple the ViewModel (and/or Controller in MVC) is left out completely. Once you start creating bigger projects the need for something to bridge your Views and Models arises. However, IMO, the SwiftUI documentation does not (yet) fully address this in a satisfying way. I would love other developers to correct me or expand on this (I'm still learning), but here's what I found out so far:
For managing updating your views in non-example project you almost always want to use ObservableObject/ObservedObject.
Views should only observe an object if they need to be updated if it changes. It is better if you can delegate the updates to a child view.
It may be tempting to create a large ObservableObject and add #Published for all of its properties. However, this means that a view that observes that object gets updated (sometimes visibly) even if a property changes on which the view does not even depend.
Binding is the most natural interface for Views that represent controls that can modify data. Beware that a Binding does NOT trigger updating views. Updating the view should be managed either #State or #ObservedObject (this can done by the parent view of the control).
Constants are the natural interface for Views that only display data (and not modify it).
Here is how I would apply this to your example:
import SwiftUI
//
// Helper class for observing value types
//
class ObservableValue<Value: Hashable>: ObservableObject {
#Published var value: Value
init(initialValue: Value) {
value = initialValue
}
}
//
// Model
//
struct Assignment {
let name : String
let notes: String
}
//
// ViewModel?
//
// Usually a view model transforms data so it is usable by the view. Strings are already
// usable in our components. The only change here is to wrap the strings in an
// ObservableValue so views can listen for changes to the individual properties.
//
// Note: In Swift you often see transformations of the data implemented as extensions to
// the model rather than in a separate ViewModel.
class AssignmentModelView {
var name : ObservableValue<String>
var notes: ObservableValue<String>
init(assignment: Assignment) {
name = ObservableValue<String>(initialValue: assignment.name)
notes = ObservableValue<String>(initialValue: assignment.notes)
}
var assignment: Assignment {
Assignment(name: name.value, notes: notes.value)
}
}
//
// Controller
//
// Publish the first assignment so Views depending on it can update whenever we change
// the first assignment (**not** update its properties)
class AssignmentController: ObservableObject {
#Published var firstAssignment: AssignmentModelView?
func retrieveFirstAssignment() {
let assignment = Assignment(name: "My First Assignment", notes: "Everyone has to start somewhere...")
firstAssignment = AssignmentModelView(assignment: assignment)
}
}
struct ContentView: View {
// In a real app you should use dependency injection here
// (i.e. provide the assignmentController as a parameter)
#ObservedObject var assignmentController = AssignmentController()
var body: some View {
NavigationView {
VStack {
// I prefer to use `map` instead of conditional views, since it
// eliminates the need for forced unwrapping
self.assignmentController.firstAssignment.map { assignmentModelView in
Form {
ObservingTextField(title: "Assignment Name", value: assignmentModelView.name)
ObservingTextField(title: "Notes", value: assignmentModelView.notes)
}
}
Button(action: { self.retrieveFirstAssignment() }) {
Text("Get The First Assignment !")
}
}
.navigationBarTitle("First Assignment")
}
}
func retrieveFirstAssignment() {
assignmentController.retrieveFirstAssignment()
}
}
//
// Wrapper for TextField that correctly updates whenever the value
// changes
//
struct ObservingTextField: View {
let title: String
#ObservedObject var value: ObservableValue<String>
var body: some View {
TextField(title, text: $value.value)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This may be overkill for your app. There is a more straightforward version, but it has the
disadvantage that TextFields are updated even though their contents hasn't changed. In
this particular example I do not think that matters much. For larger projects it may
become important, not (just) for performance reasons, but updates are sometimes very
visible. For reference: here's the simpler version.
import SwiftUI
// Model
struct Assignment {
let name : String
let notes: String
}
// ViewModel
class AssignmentViewModel: ObservableObject {
#Published var name : String
#Published var notes: String
init(assignment: Assignment) {
name = assignment.name
notes = assignment.notes
}
}
// Controller
class AssignmentController: ObservableObject {
#Published var firstAssignment: AssignmentViewModel?
func retrieveFirstAssignment() {
let assignment = Assignment(name: "My First Assignment", notes: "Everyone has to start somewhere...")
firstAssignment = AssignmentViewModel(assignment: assignment)
}
}
struct ContentView: View {
// In a real app you should use dependency injection here
// (i.e. provide the assignmentController as a parameter)
#ObservedObject var assignmentController = AssignmentController()
var body: some View {
NavigationView {
VStack {
self.assignmentController.firstAssignment.map { assignmentModelView in
FirstAssignmentView(firstAssignment: assignmentModelView)
}
Button(action: { self.retrieveFirstAssignment() }) {
Text("Get The First Assignment !")
}
}
.navigationBarTitle("First Assignment")
}
}
func retrieveFirstAssignment() {
assignmentController.retrieveFirstAssignment()
}
}
struct FirstAssignmentView: View {
#ObservedObject var firstAssignment: AssignmentViewModel
var body: some View {
Form {
TextField("Assignment Name", text: $firstAssignment.name)
TextField("Notes", text: $firstAssignment.notes)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I had the very same question and I came accross an excellent paper by Matteo Manferdini in which he describes how to use the MVC model in SwiftUI.
I used his paper to refactor a Pizza app that uses CoreData. And even though I’m still a beginner in SwiftUI it gave me a very good understanding of how to implement MVC. You can find Matteo’s paper here.
I'm using a NavigationLink inside of a ForEach in a List to build a basic list of buttons each leading to a separate detail screen.
When I tap on any of the list cells, it transitions to the detail view of that cell but then immediately pops back to the main menu screen.
Not using the ForEach helps to avoid this behavior, but not desired.
Here is the relevant code:
struct MainMenuView: View {
...
private let menuItems: [MainMenuItem] = [
MainMenuItem(type: .type1),
MainMenuItem(type: .type2),
MainMenuItem(type: .typeN),
]
var body: some View {
List {
ForEach(menuItems) { item in
NavigationLink(destination: self.destination(item.destination)) {
MainMenuCell(menuItem: item)
}
}
}
}
// Constructs destination views for the navigation link
private func destination(_ destination: ScreenDestination) -> AnyView {
switch destination {
case .type1:
return factory.makeType1Screen()
case .type2:
return factory.makeType2Screen()
case .typeN:
return factory.makeTypeNScreen()
}
}
If you have a #State, #Binding or #ObservedObject in MainMenuView, the body itself is regenerated (menuItems get computed again) which causes the NavigationLink to invalidate (actually the id change does that). So you must not modify the menuItems arrays id-s from the detail view.
If they are generated every time consider setting a constant id or store in a non modifying part, like in a viewmodel.
Maybe I found the reason of this bug...
if you use iOS 15 (not found iOS 14),
and you write the code NavigationLink to go to same View in different locations in your projects, then this bug appear.
So I simply made another View that has different destination View name but the same contents... then it works..
you can try....
sorry for my poor English...