Pass binding from parent to child view conforming to UIViewControllerRepresentable - swiftui

I need to rely on a PageView view that has a currentPage value, in such a way that the PageView itself has ownership of the value (therefore, #State) but I need to update app state when this value changes.
With TabView, I simply put #Binding $selected in as an argument and can act upon changes to this value outside of the UI layer using a custom Binding<Int> with my own getter and setter. That is the method I'm trying right now to put together a solution.
But my PageView is based on an array of UIHostingControllers and a UIViewControllerRepresentable to integrate UIPageViewController from UIKit (I know that Swift in 5.3 will offer the SwiftUI version through TabView, but I don't want to wait until "September")
PageViewController.swift
// Source: https://stackoverflow.com/questions/58388071/how-to-implement-pageview-in-swiftui
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
#Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
if controllers.count > 0 {
pageViewController.setViewControllers([controllers[currentPage]], direction: .forward, animated: true)
}
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.controllers.last
}
return parent.controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.controllers.count {
return parent.controllers.first
}
return parent.controllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.controllers.firstIndex(of: visibleViewController) {
parent.currentPage = index
}
}
}
}
PageView.swift
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
var currentPage: Binding<Int>
init(_ views: [Page], currentPage: Binding<Int>) {
self.viewControllers = views.map {
let ui = UIHostingController(rootView: $0)
ui.view.backgroundColor = UIColor.clear
return ui
}
self.currentPage = currentPage
}
var body: some View {
ZStack(alignment: .bottom) {
PageViewController(controllers: viewControllers, currentPage: currentPage)
PageControl(numberOfPages: viewControllers.count, currentPage: currentPage)
}.frame(height: 300)
}
}
Above you can see I'm trying passing in a Binding<Int> argument from the parent view, PageViewTest
PageViewTest.swift
struct PageViewTest: View {
var pagesData = ["ONE", "TWO"]
var _currentPage: Int = 0
var currentPage: Binding<Int> {
Binding<Int>(get: {
self._currentPage
}, set: {
// i.e. Update app state
print($0)
})
}
var body: some View {
VStack {
PageView(pagesData.map {
Text($0)
}, currentPage: self.currentPage)
}
}
}
This set up works as far as calling the setter routine specified in PageViewTest, but for some reason the binding is not reflected in the PageControl (from PageView.swift) that conforms to UIViewRepresentable so I don't feel like this is THE solution.
Am I passing around the bindings incorrectly? The PageView should own the state of currentPage, but I want its ancestor view to be able to act on changes to it.
#ObservableObject won't work because I just want to send a primitive. CurrentValueSubject/Passthrough won't fire (presumably because the PageView is being reinitialized over and over):
Alternative PageView.swift
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
var valStr = PassthroughSubject<Int, Never>()
var store = Set<AnyCancellable>()
#State var currentPage: Int = 0 {
didSet {
valStr.send(currentPage)
}
}
init(_ views: [Page], _ cb: #escaping (Int) -> ()) {
self.viewControllers = views.map {
let ui = UIHostingController(rootView: $0)
ui.view.backgroundColor = UIColor.clear
return ui
}
valStr.sink(receiveValue: { value in
cb(value)
}).store(in: &store)
}
var body: some View {
ZStack(alignment: .bottom) {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage)
}.frame(height: 300)
}
}

Related

UIViewControllerRepresentable ignoresSafeArea not working

import SwiftUI
import UIKit
extension Color {
static var random: Color {
return Color (
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1)
)
}
}
struct ContentView: View {
let pages = (0..<3).map { _ in Color.random }
var body: some View {
PageView(pages: pages)
.ignoresSafeArea()
}
}
struct PageView<Page: View>: View {
var pages: [Page]
#State private var currentPage = 0
var body: some View {
PageViewController(pages: pages, currentPage: $currentPage)
}
}
struct PageViewController<Page: View>: UIViewControllerRepresentable {
var pages: [Page]
#Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
var controllers = [UIViewController]()
init(_ pageViewController: PageViewController) {
parent = pageViewController
controllers = parent.pages.map { UIHostingController(rootView: $0) }
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return controllers.last
}
return controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == controllers.count {
return controllers.first
}
return controllers[index + 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = controllers.firstIndex(of: visibleViewController) {
parent.currentPage = index
}
}
}
}
It should be not only for container, but for internal view as well (to fill that external container), because by default every view respects safe area.
So the fix is
init(_ pageViewController: PageViewController) {
parent = pageViewController
controllers = parent.pages.map {
UIHostingController(rootView: $0.ignoresSafeArea()) // << here !!
}
}
Tested with Xcode 13.3 / iOS 15.4

SwiftUI + UIPageViewController, multiple view types

I want to use a UIPageViewController in my SwiftUI app, as demonstrated in this tutorial. However, the tutorial passes in identical view types as pages, and I want to pass in any type of view.
struct PageViewController<Page: View>: UIViewControllerRepresentable {
var pages: [Page]
#Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDelegate {
var parent: PageViewController
var controllers = [UIViewController]()
init(_ pageViewController: PageViewController) {
parent = pageViewController
controllers = parent.pages.map { UIHostingController(rootView: $0) }
}
func pageViewController(
_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = controllers.firstIndex(of: visibleViewController) {
parent.currentPage = index
}
}
}
}
struct PageView<Page: View>: View {
var pages: [Page]
#State var currentPage = 0
var body: some View {
ZStack {
PageViewController(pages: pages, currentPage: $currentPage)
}
}
}
struct PageView_Previews: PreviewProvider {
static var previews: some View {
PageView(pages: [
Color.red,
Color.blue
// I want to add other view types here
])
}
}
If I change the array of pages in the PageView_Previews to something like [Color.red, Text("abc")], I get an error because they're not the same type. How do I get SwiftUI to allow heterogeneous View types here?
I can't use TabView with PageTabViewStyle (lots of bugs), so that's why I need to use this UIKit method.
You can wrap your pages in AnyView:
PageView(pages: [
AnyView(Color.red),
AnyView(Color.blue),
AnyView(Text("Hi"))
])

How can I change background color of PageViewController?

How can I change the background color of the area that is white?
I used UIViewControllerRepresentable but I don't know how to change the color of UIViewControllers.
I guess I need to change the background color in the makeUIViewController function?
I don't know much English, I hope I could explain my problem.
OnboardingView:
struct OnboardingView: View {
#State var currentPageIndex = 0
let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
var subviews = [
UIHostingController(rootView: SubView(imageString: "1")),
UIHostingController(rootView: SubView(imageString: "1")),
UIHostingController(rootView: SubView(imageString: "1"))
]
var titles = ["Take some time out", "Conquer personal hindrances", "Create a peaceful mind"]
var captions = ["Take your time out and bring awareness into your everyday life", "Meditating helps you dealing with anxiety and other psychic problems", "Regular medidation sessions creates a peaceful inner mind"]
var body: some View {
VStack(alignment: .leading) {
Group {
Text(titles[currentPageIndex])
.font(.title)
Text(captions[currentPageIndex])
.font(.subheadline)
.frame(width: 300, height: 50, alignment: .center)
.lineLimit(nil)
}
.padding()
PageViewController(currentPageIndex: $currentPageIndex, viewControllers: subviews)
.frame(height: 600)
.background(Color.yellow)
...
}
}
}
PageViewController:
struct PageViewController: UIViewControllerRepresentable {
#Binding var currentPageIndex: Int
var viewControllers: [UIViewController]
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[viewControllers[currentPageIndex]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = parent.viewControllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.viewControllers.last
}
return parent.viewControllers[index - 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = parent.viewControllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.viewControllers.count {
return parent.viewControllers.first
}
return parent.viewControllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.viewControllers.firstIndex(of: visibleViewController)
{
parent.currentPageIndex = index
}
}
}
}
You need to set the backgroundColor of your subviews.
You can do it either in OnboardingView or in PageViewController:
func makeUIViewController(context: Context) -> UIPageViewController {
...
// make the subviews transparent
viewControllers.forEach {
$0.view.backgroundColor = .clear
}
...
}

SKStoreProductViewController must be used in a modal view controller SWIFTUI

I am building a info page for my SwiftUI app. One item should open App Store, another mail. I have written UIViewControllerRepresentable for each.
MailView works fine totally. StoreView displays fine, but when pressed on Cancel button, throws exception
"*** Terminating app due to uncaught exception 'SKUnsupportedPresentationException', reason: 'SKStoreProductViewController must be used in a modal view controller'".
MailView goes fine into didFinish delegate method but StoreView does not go into didFinish delegate method, it crashes before going into this didFinish method. What am I doing wrong please?
import SwiftUI
import StoreKit
import MessageUI
struct InfoMoreAppsView: View {
#State var showAppAtStore = false
#State var reportBug = false
#State var result: Result<MFMailComposeResult, Error>? = nil
let otherAppName = "TheoryTest"
var body: some View {
VStack(alignment: .leading){
HStack{
Image(Helper.getOtherAppImageName(otherAppName: otherAppName))
Button(action: { self.showAppAtStore = true }) {
Text(otherAppName)
}
.sheet(isPresented: $showAppAtStore){
StoreView(appID: Helper.getOtherAppID(otherAppName: otherAppName))
}
}
Button(action: { self.reportBug = true }) {
Text("Report a bug")
}
.sheet(isPresented: $reportBug){
MailView(result: self.$result)
}
}
.padding()
.font(.title2)
}
}
struct StoreView: UIViewControllerRepresentable {
let appID: String
#Environment(\.presentationMode) var presentation
class Coordinator: NSObject, SKStoreProductViewControllerDelegate {
#Binding var presentation: PresentationMode
init(presentation: Binding<PresentationMode> ) {
_presentation = presentation
}
private func productViewControllerDidFinish(viewController: SKStoreProductViewController) {
$presentation.wrappedValue.dismiss()
viewController.dismiss(animated: true, completion: nil)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<StoreView>) -> SKStoreProductViewController {
let skStoreProductViewController = SKStoreProductViewController()
skStoreProductViewController.delegate = context.coordinator
let parameters = [ SKStoreProductParameterITunesItemIdentifier : appID]
skStoreProductViewController.loadProduct(withParameters: parameters)
return skStoreProductViewController
}
func updateUIViewController(_ uiViewController: SKStoreProductViewController, context: UIViewControllerRepresentableContext<StoreView>) {
}
}
struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_presentation = presentation
_result = result
}
func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let mailComposeViewController = MFMailComposeViewController()
mailComposeViewController.mailComposeDelegate = context.coordinator
mailComposeViewController.setToRecipients([Constants.SUPPORT_EMAIL])
mailComposeViewController.setMessageBody(systemInfo(), isHTML: true)
return mailComposeViewController
}
func systemInfo() -> String {
let device = UIDevice.current
let systemVersion = device.systemVersion
let model = UIDevice.hardwareModel
let mailBody = "Model: " + model + ". OS: " + systemVersion
return mailBody
}
func updateUIViewController(_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
This isn't very "Swifty" or pretty but I got this to work without crashing by not wrapping the SKStoreProductViewController in a representable.
struct MovieView: View {
var vc:SKStoreProductViewController = SKStoreProductViewController()
var body: some View {
HStack(){
Button(action: {
let params = [
SKStoreProductParameterITunesItemIdentifier:"1179624268",
SKStoreProductParameterAffiliateToken:"11l4Cu",
SKStoreProductParameterCampaignToken:"hype_movie"
] as [String : Any]
// vc!.delegate = self
vc.loadProduct(withParameters: params, completionBlock: { (success,error) -> Void in
UIApplication.shared.windows.first?.rootViewController?.present(vc, animated: true, completion: nil)
})
}) {
HStack {
Image(systemName: "play.fill")
.font(.headline)
}
.padding(EdgeInsets(top: 6, leading:36, bottom: 6, trailing: 36))
.foregroundColor(.white)
.background(Color(red: 29/255, green: 231/255, blue: 130/255))
.cornerRadius(10)
}
Spacer()
}}
Since I was stuck on the same thing. Here is a quick solution I found working.
import StoreKit
import SwiftUI
import UIKit
struct StoreView: UIViewControllerRepresentable {
var dismissHandler: () -> Void
func makeUIViewController(context: UIViewControllerRepresentableContext<StoreView>) -> StoreViewController {
return StoreViewController(coordinator: context.coordinator)
}
func updateUIViewController(_ uiViewController: StoreViewController, context: UIViewControllerRepresentableContext<StoreView>) {
}
public func makeCoordinator() -> StoreViewCoordinator {
.init(dismissHandler: dismissHandler)
}
}
class StoreViewController: UIViewController {
let coordinator: StoreViewCoordinator
var storeController: SKStoreProductViewController?
init(coordinator: StoreViewCoordinator) {
self.coordinator = coordinator
super.init(nibName: nil, bundle: nil)
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
storeController = SKStoreProductViewController()
storeController?.delegate = coordinator
storeController?.loadProduct(
withParameters: [SKStoreProductParameterITunesItemIdentifier: ******]
)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard let storeController = storeController else {
return
}
present(storeController, animated: true)
}
}
class StoreViewCoordinator: NSObject, SKStoreProductViewControllerDelegate {
private let dismissHandler: () -> Void
init(dismissHandler: #escaping () -> Void) {
self.dismissHandler = dismissHandler
}
func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) {
dismissHandler()
}
}
and then I am using it inside ZStack like:
StoreView(
dismissHandler: { viewStore.send(.setShowingStore(false)) }
)
.isHidden(!viewStore.isShowingStore, remove: true)
I am using TCA, so setting a property will be different in your case

SwiftUI UIPageView compile error and action sheet problem

I have 2 problems in my code.
One is compile error [Cannot convert value of type 'Page1' to expected element type '_'] is shown at ★.
Another is that when backward button is pressed, Page2(blue) disappeared while alert sheet is showing.
(comment out "Page1(page: self.$page)," and then build source)
Expected behavior is that Page2(blue) does not disappear until alert button(Yes) is pressed.
Can anyone tell me how to solve these problems?
import SwiftUI
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
#Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .pageCurl,
navigationOrientation: .vertical)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[currentPage]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController?
{
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return nil
}
return parent.controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController?
{
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.controllers.count {
return nil
}
return parent.controllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.controllers.firstIndex(of: visibleViewController)
{
parent.currentPage = index
}
}
}
}
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
#Binding var currentPage: Int
init(_ views: [Page], currentPage: Binding<Int>) {
self._currentPage = currentPage
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
}
}
struct ContentView: View {
#State var page: Int = 0
var body: some View {
VStack {
PageView([
Page1(page: self.$page), // ★ error
Page2(page: self.$page)// ★ error
], currentPage: $page)
}
}
}
struct Page1: View{
#Binding var page: Int
var body: some View {
ZStack{
Color.red
VStack{
Text("page1")
Button (
action: { self.page += 1 }
){
Image(systemName: "forward")
.accentColor(Color.yellow)
}
}
}
}
}
struct Page2: View{
#State private var showingAlert: Bool = false
#Binding var page: Int
var body: some View {
ZStack{
Color.blue
VStack{
Text("page2")
Button (
action: { self.showingAlert.toggle() }
){
Image(systemName: "backward")
.accentColor(Color.yellow)
}
}
.alert(isPresented: $showingAlert) {
Alert(
title: Text("Confirm"),
message: Text("Back?"),
primaryButton:
.default(Text("Yes"),
action:{ self.page -= 1 }
),
secondaryButton:
.cancel(Text("No"))
)//alert
}//alert
}
}
}
I was able to figure it out using:
[How to create an onboarding screen in SwiftUI #1 - Embedding a UIPageViewController] (https://www.blckbirds.com/post/how-to-create-a-onboarding-screen-in-swiftui-1)