SwiftUI: Forcing an Update
Asked Answered
L

4

76

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.

Loveland answered 12/6, 2019 at 11:53 Comment(6)
What is PageViewController?Percept
just setting currentPage is enough to reload bodyBernt
@Percept -This is from the tutorial, so PageViewController is the example class they created. It wraps a UIPageViewController instance. The issue that I'm having is one that I often have when switching between declarative and imperative languages. In this case, Swift is both. I almost have it. Lu_'s response is correct. I just have to tweak a cosmetic thingLoveland
@Bernt > just setting currentPage is enough to reload body< -This is correct. I was fooled by the direction of the swipe. Make that an answer, and I'll greencheck you.Loveland
FYI, I'm pretty sure that the "normally" for things has changed. You aren't under the same kind of NDA like 10 years ago. I fact, by next month, all their beta OS's will be public betas.Monto
In answer to the edit: I meant "driving" (not "diving"). I talk about it here: littlegreenviper.com/miscellany/…Loveland
B
10

Setting currentPage, as it is a @State, will reload the whole body.

Bernt answered 12/6, 2019 at 13:12 Comment(6)
if we want to reload just Text(verbatim: "Page \(currentPage)") segment, what can should do?Simson
@Simson you could use state variable specifically for that label, if there are no other dependencies only it should reloadBernt
I have a TextField that is bound to a @State $variable but when I type, the whole body is not reloaded?Adrienadriena
Not always, not if currentPage hasn't changed since last time.Sibling
@Sibling ...and why on earth would someone want to reload the page if the data hasn’t changed, you might ask? I’m commenting my own comment here since I want to make clear that there exists such occasions, when external data has changed, like time for example, and the page needs to be updated to reflect this.Sibling
One obvious application is debugging - I want to refresh everything to check it didn't need refreshing. I am working on another one. I have colour patches with RGB values. I have many views in the app. If I change the white point, it is easier to refresh all the views than to broadcast the change to the views that need it. My views are all pretty lightweight.Sato
I
74

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 StateObject/ObservableObject/ObservedObject
  • Must be used modifiers correctly (state, stateObject, observableObject/observedObject, published, binding, etc)
  • Any field in ViewModel must be a STRUCT. NOT A CLASS!!!! Swift UI does not work with classes!
  • If you need a class property in your View Model (for some reason) - you need to mark it as ObservableObject/ObservedObject 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 if this is possible.

Your UI will be refreshed automatically if all of written above was used correctly.

Sample of correct usage:

struct SomeView : View {
    @StateObject 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()
    }
}
Inkle answered 15/7, 2020 at 16:30 Comment(15)
Way 1 doesn't work if we don't use updater state in body variableBraxy
I have tried before post of the answer and it has worked. But State must be declared inside of view that you need to update. So if you need to update parent view - you need to declare it in parent view.Inkle
It doesn't work for me. I declare the State current view, but if I doesn't use it in body, it won't work. Maybe it was changed from the time you post the answer.Braxy
Way 1 works for me even without using boolean in body (looks like @Published inside ObservedObject is enough)Prosper
not really. Only in case of ObservedObject is a class, but Published inside of Observable is a struct - this is important part of usung ViewModels in SwiftUIInkle
@Andrew: really good answer "If you search for this hack, probably you doing something wrong! "Beforehand
I love that you have put in the disclaimer, but it is just what the doctor ordered when adapting old pre-SwiftUI code for a quick prototype. Thank you!Tragopan
"If you need a class property for some reason in your View Model - you need to mark it as ObservableObject/Observed object and connect it into your view separately. Do it inside init() of view." - I learnt a new thing today, but unfortunately too late. I solved my issues with hacks at many places in my project. Nevertheless, I shall fix them now. All these days I am just not happy writing hacks and felt I have no other option, but now I knew what I am doing wrong. Thanks for sharing the knowledge.Highsmith
Thank you for that answer!!! It helped me sooooo much!!!Seaway
Tx for this @Inkle but you maybe confusing people. properties in a class don't need to be structs at all. This is a big miss in my opinon....you may mean a type (bool, string, int, float, etc.) You may add a struct as a property but it really depends on the usage and your preference.Amund
@Amund it must be structs because of internal logic of SwiftUI. Structs are more "stable" and SwiftUI always know when struct was changed. That's why you need to use @mutating in structs but you don't need it in classes. SwiftUI because of unstability of classes cannot check for sure that data inside was changed or not. And workflow of SwiftUI depends exactly on this checks. So no, it must be a structs. Not only native structs like bool, string, int, float, etc, but also for custom structs. Ofc you can use also classes, but you need to do this correctly like written in my answer.Inkle
So @Inkle you are referring to primitives / types as structs? This is a first for me.Amund
@Amund I referring structs as structs. And classed as classes. If it declared as struct it is a struct. If it declared as class - it is class.Inkle
@jalone from documentation: StateObject - A property wrapper type that instantiates an observable object. ; So StateObject and ObservedObject is the same thing but with different syntax; developer.apple.com/documentation/swiftui/stateobjectInkle
Thank you!! I made my Model a class by mistake. So your IMPORTANT paragraph really helped.Bawbee
C
29

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)
        }
    }
}
Chickenlivered answered 1/12, 2020 at 17:55 Comment(2)
instead of an incrementing value, you can also use a date (timestamp) as idRhoads
@PeterKreinz This leaks memory, try setting previous ids - data from all previous states will still be present.Coumas
B
10

Setting currentPage, as it is a @State, will reload the whole body.

Bernt answered 12/6, 2019 at 13:12 Comment(6)
if we want to reload just Text(verbatim: "Page \(currentPage)") segment, what can should do?Simson
@Simson you could use state variable specifically for that label, if there are no other dependencies only it should reloadBernt
I have a TextField that is bound to a @State $variable but when I type, the whole body is not reloaded?Adrienadriena
Not always, not if currentPage hasn't changed since last time.Sibling
@Sibling ...and why on earth would someone want to reload the page if the data hasn’t changed, you might ask? I’m commenting my own comment here since I want to make clear that there exists such occasions, when external data has changed, like time for example, and the page needs to be updated to reflect this.Sibling
One obvious application is debugging - I want to refresh everything to check it didn't need refreshing. I am working on another one. I have colour patches with RGB values. I have many views in the app. If I change the white point, it is easier to refresh all the views than to broadcast the change to the views that need it. My views are all pretty lightweight.Sato
J
2

I had this problem recently when I changed language and the sub-view doesn't update the content.

This solved my problem:

struct MySubview: View {
    init() {
        // trigger drawing UI when language changes
        activeLanguage = someGlobalFuncToGetCurrentLocale()
    }

    @State
    var activeLanguage: Locale = someGlobalFuncToGetCurrentLocale()

    var body: some View {
    ....
    }
}
Jankell answered 13/10, 2023 at 12:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.