25
votes

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.

3
What is PageViewController?Fogmeister
just setting currentPage is enough to reload bodyLu_
@Fogmeister -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 thingChris Marshall
@Lu_ > 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.Chris Marshall
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.dfd

3 Answers

9
votes

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

24
votes

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/Observed object
  • 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.

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 update is toogle() it: updater.toogle()


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()
    }
}
9
votes

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)
        }
    }
}