How can I update modifier in SwiftUI without re drawing views - swift

I want update my modifier in this way that the orignal content does not get redrawen or re appear for that goal I am using this code:
struct BigModified: ViewModifier {
func body(content: Content) -> some View {
return content
.font(Font.title.bold())
}
}
struct ContentView: View {
#State private var start: Bool = false
var body: some View {
Text("Hello, world!")
.modifier(start ? BigModified() : EmptyModifier())
.onTapGesture { start.toggle() }
}
}
The error is:
Result values in '? :' expression have mismatching types 'BigModified' and 'EmptyModifier'
I know that I can use switch or if, but again those make view get appear again. Is there a way around to update modifier with a State wrapper like in question, also I should add and tell it is obvious that I can use a parameter inside a ViewModifier for that! But that is not my question, my question is about .modifier(T) itself, more looking some thing like .modifier(T, V, U, ...) be possible to use or build, which takes more than just one viewModifier.

You can declare a view modifier in another way, as a func of View, so you can act on your view based on a boolean parameter.
This is how it works:
Modifier:
extension View {
#ViewBuilder
func modifier(big: Bool) -> some View {
if big {
self
.font(Font.title.bold())
} else {
self
}
}
}
How to use it:
var body: some View {
Text("Hello, world!")
.modifier(big: start)
.onTapGesture { start.toggle() }
}

Related

Is it possible to override SwiftUI modifiers?

Knowing that with SwiftUI view modifiers, order matters - because each modifier is a part of a chain of modifiers, I was wondering if it was possible to reset/overwrite/override a modifier (or the whole chain?
Specifically, I'm wondering about Styles (groupBoxStyle, buttonStyle, etc). I have default styles that I want to use in 90% of my app, and a few pages will have slightly different styles for those widgets.
For example:
// Renders a button with the "light" style
Button("Hello world") {
}
.buttonStyle(LightButtonStyle())
.buttonStyle(DarkButtonStyle())
// Renders a button with the "dark" style
Button("Hello world") {
}
.buttonStyle(DarkButtonStyle())
.buttonStyle(LightButtonStyle())
In those cases, I would actually like the 2nd modifier to be used, but the 1st takes over and subsequent styles don't work.
Note: In my actual app, none of my use cases are this trivial - this is just the simplest proof of concept.
The workaround(s) I have are that I create separate LightButton and DarkButton views, but that feels very inelegant (and becomes a mess when I have 5-6 variants of each component).
Alternatively, I have a custom MyButton(myStyle: ButtonStyle = .myDefaultStyle), but since this is a forms app, there are about 50-60 locations where something like that needs to be updated (instead of applying a modifier at a top level and letting that cascade through).
Edit: I should note, where I can set a top-level style and let it cascade, that works very well and as expected (closer to the View, the modifier takes over). But, there are just some weird use cases where it would be nice to flip the script.
Generally, buttonStyle propagates to child views, so ideally you would only need to set your “house style” once on the root view of your app.
The well-known place where this fails to work is the presentation modifiers like .sheet, which do not propagate styles to the presented view hierarchy. So you will need to write your own versions of the presentation modifiers that re-apply your house style.
For example, here's a custom ButtonStyle:
struct HouseButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding(20)
.background {
Capsule(style: .continuous)
.foregroundColor(.pink)
}
.saturation(configuration.isPressed ? 1 : 0.5)
}
}
And here's a cover for sheet that applies the custom button style to the presented content:
extension View {
func houseSheet<Content: View>(
isPresented: Binding<Bool>,
onDismiss: (() -> Void)? = nil,
#ViewBuilder content: #escaping () -> Content
) -> some View {
return sheet(isPresented: isPresented, onDismiss: onDismiss) {
content()
.buttonStyle(HouseButtonStyle())
}
}
}
We can test out whether a NavigationLink, a sheet, and a houseSheet propagate the button style:
struct ContentView: View {
#State var showingHouseSheet = false
#State var showingStandardSheet = false
var body: some View {
NavigationView {
VStack {
NavigationLink("Navigation Push") {
ContentView()
}
Button("Standard Sheet") {
showingStandardSheet = true
}
Button("House Sheet") {
showingHouseSheet = true
}
}
.sheet(isPresented: $showingStandardSheet) {
ContentView()
}
.houseSheet(isPresented: $showingHouseSheet) {
ContentView()
}
}
}
}
Here's the root view that applies the house button style at the highest level:
struct RootView: View {
var body: some View {
ContentView()
.buttonStyle(HouseButtonStyle())
}
}
If you play with this, you'll find that both NavigationLink and houseSheet propagate the button style to the presented content, but sheet does not.

SwiftUI ViewModifier not working as a NavigationLink

I have the following ViewModifier and it does not work:
import SwiftUI
struct NavLink: ViewModifier {
let title: String
func body(content: Content) -> some View {
NavigationLink(destination: content) {
Text(title)
}
}
}
extension View {
func navLink(title: String) -> some View {
modifier(NavLink(title: title))
}
}
If I use that as follows:
import SwiftUI
struct MainScreen: View {
var body: some View {
NavigationStack() {
VStack {
// This does not work
NavStackScreen()
.navLink(title: "Nav Stack")
// But this does
Helper.linked(to: NavStackScreen(), title: "Nav Stack 2")
}
}
}
}
struct Helper {
static func linked(to destination: some View, title: String) -> some View {
NavigationLink(destination: destination) {
Text(title)
}
}
}
It creates the link and pushes a new view onto the screen if tapped; however, the contents of the NavStackScreen are not displayed, only an empty screen.
Any ideas about what is going on?
Contents of NavStackScreen for reference:
struct NavStackScreen: View {
var body: some View {
Text("Nav Stack Screen")
.font(.title)
.navigationTitle("Navigation Stack")
}
}
If I use a static helper function within a helper struct, then it works correctly:
static func linked(to destination: some View, title: String) -> some View {
NavigationLink(destination: destination) {
Text(title)
}
}
I added the full code of the MainView for reference and updated the example with more detail for easy reproduction.
The modifier doesn't work because the content argument is not the actual view being modified, but instead is a proxy:
content is a proxy for the view that will have the modifier represented by Self applied to it.
Reference.
This is what a quick debugging over the modifier shows:
(lldb) po content
SwiftUI._ViewModifier_Content<SwiftUIApp.NavLink>()
As the proxy is an internal type of SwiftUI, we can't know for sure why NavigationLink doesn't work with it.
A workaround would be to skip the modifier, and only add the extension over View:
extension View {
func navLink(title: String) -> some View {
NavigationLink(destination: content) {
Text(title)
}
}
}

How to make a view gone in swiftui

I was trying to hide view but noticed that view.hidden() will only hide the view but but the space remains.
I also tried some of the suggestions on this link Dynamically hiding view in SwiftUI
But it didn't really work for me, albeit it was a starting point.
Find below what I finally did.
Here's an approach without an extension.
struct ContentView: View {
#State var hidden = false
#State var text = ""
var body: some View {
NavigationView {
List {
Section {
Toggle("Hide", isOn: $hidden.animation(Animation.easeInOut))
}
if !hidden {
Section {
TextField("Pin", text: $text)
}
}
}
}
}
}
I created an extension function as below
extension View {
#ViewBuilder func hiddenConditionally(isHidden: Binding<Bool>) -> some View {
isHidden.wrappedValue ? self : self.hidden() as? Self
}
}
and you can call it as below
TextField("Pin", text: $pin)
.hiddenConditionally(isHidden: $showPinField)

How can I infer generic parameter in extension?

I have a very simple CustomView which it takes a View and modify it! Like this code:
struct CustomView<ViewType: View>: View {
let content: () -> ViewType
var body: some View {
return content()
.foregroundColor(.red)
}
}
So far so good!
Now I want make a CustomModifier out of it, like this:
extension View {
func customModifier<ViewModifierType: View>(viewModifier: () -> ViewModifierType) -> some View {
// how can I import self from here?
return viewModifier()
}
}
use case:
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.customModifier(viewModifier: { CustomView(content: { Text("Hello, World!") } ) }) // <<: Here is the issue!
// How can I satisfy CustomView with self in extension? that could help to cut Text("Hello, World!") to feeding as content for CustomView?
}
}
So if see the codes, all I am trying to do is cut off Text("Hello, World!") with using self from extension and trying to have this form of coding:
.customModifier(viewModifier: { CustomView() })
PS: I know that for having form of CustomView() I have to respect the generic parameter CustomView with word of where but I do not know how can I put these all puzzle together!
If I understand you correctly, then try this
extension View {
func customModifier<ViewModifierType: View>(viewModifier: (Self) -> ViewModifierType) -> some View {
return viewModifier(self)
}
}
Not sure what is your goal but if you just want to add a red foreground to a view you need to implement a view modifier:
struct RedForeground: ViewModifier {
func body(content: Content) -> some View {
content.foregroundColor(.red)
}
}
And then just call it using the modifier method:
Text("Text")
.modifier(RedForeground())
If you want to simplify it further you can extend View and add make it a computed property:
extension View {
var redForeground: some View { modifier(RedForeground()) }
}
usage:
Text("Text")
.redForeground
You can't just init CustomView without a view, but you can pass CustomView initializer as function to customModifier, like this:
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.customModifier(viewModifier: CustomView.init)
}
}
struct CustomView<ViewType: View>: View {
let content: ViewType
var body: some View {
return content
.foregroundColor(.red)
}
}
extension View {
func customModifier<ViewModifierType: View>(viewModifier: (Self) -> ViewModifierType) -> some View {
return viewModifier(self)
}
}

SwiftUI: Forcing an Update

Normally, we're restricted from discussing Apple prerelease stuff, but I've already seen plenty of SwiftUI discussions, so I suspect that it's OK; just this once.
I am in the process of driving into the weeds on one of the tutorials (I do that).
I am adding a pair of buttons below the swipeable screens in the "Interfacing With UIKit" tutorial: https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
These are "Next" and "Prev" buttons. When at one end or the other, the corresponding button hides. I have that working fine.
The problem that I'm having, is accessing the UIPageViewController instance represented by the PageViewController.
I have the currentPage property changing (by making the PageViewController a delegate of the UIPageViewController), but I need to force the UIPageViewController to change programmatically.
I know that I can "brute force" the display by redrawing the PageView body, reflecting a new currentPage, but I'm not exactly sure how to do that.
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
#State var currentPage = 0
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
VStack {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
HStack(alignment: .center) {
Spacer()
if 0 < currentPage {
Button(action: {
self.prevPage()
}) {
Text("Prev")
}
Spacer()
}
Text(verbatim: "Page \(currentPage)")
if currentPage < viewControllers.count - 1 {
Spacer()
Button(action: {
self.nextPage()
}) {
Text("Next")
}
}
Spacer()
}
}
}
func nextPage() {
if currentPage < viewControllers.count - 1 {
currentPage += 1
}
}
func prevPage() {
if 0 < currentPage {
currentPage -= 1
}
}
}
I know the answer should be obvious, but I'm having difficulty figuring out how to programmatically refresh the VStack or body.
2021 SWIFT 1 and 2 both:
IMPORTANT THING! If you search for this hack, probably you doing something wrong! Please, read this block before you read hack solution!!!!!!!!!!
Your UI wasn't updated automatically because of you miss something
important.
Your ViewModel must be a class wrapped into ObservableObject/ObservedObject
Any field in ViewModel must be a STRUCT. NOT A CLASS!!!! Swift UI does not work with classes!
Must be used modifiers correctly (state, observable/observedObject, published, binding, etc)
If you need a class property in your View Model (for some reason) - you need to mark it as ObservableObject/Observed object and assign them into View's object !!!!!!!! inside init() of View. !!!!!!!
Sometimes is needed to use hacks. But this is really-really-really exclusive situation! In most cases this wrong way! One more time: Please, use structs instead of classes!
Your UI will be refreshed automatically if all of written above was used correctly.
Sample of correct usage:
struct SomeView : View {
#ObservedObject var model : SomeViewModel
#ObservedObject var someClassValue: MyClass
init(model: SomeViewModel) {
self.model = model
//as this is class we must do it observable and assign into view manually
self.someClassValue = model.someClassValue
}
var body: some View {
//here we can use model.someStructValue directly
// or we can use local someClassValue taken from VIEW, BUT NOT value from model
}
}
class SomeViewModel : ObservableObject {
#Published var someStructValue: Bool = false
var someClassValue: MyClass = MyClass() //myClass : ObservableObject
}
And the answer on topic question.
(hacks solutions - prefer do not use this)
Way 1: declare inside of view:
#State var updater: Bool = false
all you need to do is call updater.toggle()
Way 2: refresh from ViewModel
Works on SwiftUI 2
public class ViewModelSample : ObservableObject
func updateView(){
self.objectWillChange.send()
}
}
Way 3: refresh from ViewModel:
works on SwiftUI 1
import Combine
import SwiftUI
class ViewModelSample: ObservableObject {
private let objectWillChange = ObservableObjectPublisher()
func updateView(){
objectWillChange.send()
}
}
This is another solution what worked for me, using id() identifier. Basically, we are not really refreshing view. We are replacing the view with a new one.
import SwiftUI
struct ManualUpdatedTextField: View {
#State var name: String
var body: some View {
VStack {
TextField("", text: $name)
Text("Hello, \(name)!")
}
}
}
struct MainView: View {
#State private var name: String = "Tim"
#State private var theId = 0
var body: some View {
VStack {
Button {
name += " Cook"
theId += 1
} label: {
Text("update Text")
.padding()
.background(Color.blue)
}
ManualUpdatedTextField(name: name)
.id(theId)
}
}
}
Setting currentPage, as it is a #State, will reload the whole body.