I'm currently trying to add a calendar interface in my app where when you click on a day at the bottom it will show details about events on that day. Currently I am using FSCalendar as my calendar.
I realised that this library is for UIKit and I would have to wrap it with representable protocol to get it working in SwiftUI.
I've been watching youtube and looking up guides on integrating UIKit in SwiftUI to help me do this. This is what I have currently:
import SwiftUI
import FSCalendar
CalendarModule.swift:
class CalendarModule: UIViewController, FSCalendarDelegate {
var calendar = FSCalendar()
override func viewDidLoad() {
super.viewDidLoad()
calendar.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
initCalendar()
view.addSubview(calendar)
}
private func initCalendar() {
calendar.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.width)
calendar.appearance.todayColor = UIColor.systemGreen
calendar.appearance.selectionColor = UIColor.systemBlue
}
}
struct CalendarModuleViewController: UIViewControllerRepresentable {
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: UIViewControllerRepresentableContext<CalendarModuleViewController>) -> UIViewController {
let viewController = CalendarModule()
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<CalendarModuleViewController>) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
final class Coordinator: NSObject, FSCalendarDelegate {
private var parent: CalendarModuleViewController
init (_ parent: CalendarModuleViewController) {
self.parent = parent
}
}
}
struct CalendarModuleView: View {
var body: some View {
CalendarModuleViewController()
}
}
CalendarView.swift - Displaying calendar in top half and the details in bottom half:
import SwiftUI
struct CalendarView: View {
var body: some View {
NavigationView {
VStack{
VStack {
Spacer()
CalendarModuleView()
Spacer()
}
VStack {
Spacer()
Text("Details")
Spacer()
}
}.navigationBarTitle(Text("Calendar"), displayMode: .inline)
.navigationBarItems(trailing:
NavigationLink(destination: CreateEventView().environmentObject(Event())) {
Image(systemName: "plus").imageScale(.large)
}.buttonStyle(DefaultButtonStyle())
)
}
}
}
struct CalendarView_Previews: PreviewProvider {
static var previews: some View {
CalendarView()
}
}
ContentView - Just displaying my CalendarView:
import SwiftUI
struct ContentView: View {
var body: some View {
CalendarView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The calendar looks like this so far:
The calendar module itself works but I got stuck in writing the Coordinator to handle the delegates. Specifically the didSelectDate one..
When I start typing didSelectDate in the Coordinator class and look through the suggestions I only get
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
<#code#>
}
Am I wrapping the view controller wrong?
Or should I be making my own UIView and UIViewController for FSCalendar then create UIViewRepresentable and UIViewControllerRepresentable to use in SwiftUI?
This is an old question, but still.
I think the problem is that you assign CalendarModule as a delegate and not the Coordinator
you need to write the following code:
func makeUIViewController(context: UIViewControllerRepresentableContext<CalendarModuleViewController>) -> UIViewController {
let viewController = CalendarModule()
viewController.calendar.delegate = context.coordinator // ->> Add this
return viewController
}
After that, you should be able to implement delegate methods in the Coordinator
Related
I am not getting the proper solution, How to open UIKit ViewController(With Navigation Controller) from SwiftUI Button clicked.
You can do it by using the UIViewControllerRepresentable protocol which is used to manage your UIKit ViewController in the SwiftUI.
Here is the code to show your ViewController from SwiftUI View interface.
SwiftUI View
struct ContentView: View {
#State var isOpenView = false
var body: some View {
NavigationView {
VStack {
//show your view controller
NavigationLink(destination: TestControllerView(), isActive: $isOpenView) {
EmptyView()
}
Button(action: {
self.isOpenView = true
}){
Text("Tap Me")
}
}
}
}
}
Now wrap your ViewController into UIViewControllerRepresentable
struct TestControllerView : UIViewControllerRepresentable {
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
func makeUIViewController(context: Context) -> some UIViewController {
guard let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "TestViewController") as? TestViewController else {
fatalError("ViewController not implemented in storyboard")
}
return viewController
}
}
Here is your ViewController.Swift File code
import UIKit
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func btnBackClicked(_ sender : UIButton) {
self.dismiss(animated: true, completion: nil)
}
}
First, wrap your UIViewController in a UIViewControllerRepresantable like so.
Then present that View using .fullScreenCover
I am displaying a UIColorPickerViewController as a sheet using the sheet() method, everything works fine but I can't drag down/dismiss the view anymore.
import Foundation
import SwiftUI
struct ColorPickerView: UIViewControllerRepresentable {
private var selectedColor: UIColor!
init(selectedColor: UIColor) {
self.selectedColor = selectedColor
}
func makeUIViewController(context: Context) -> UIColorPickerViewController {
let colorPicker = UIColorPickerViewController()
colorPicker.selectedColor = self.selectedColor
return colorPicker
}
func updateUIViewController(_ uiViewController: UIColorPickerViewController, context: Context) {
// Silent
}
}
.sheet(isPresented: self.$viewManager.showSheet, onDismiss: {
ColorPickerView()
}
Any idea how to make the drag/down dismiss gesture works?
Thanks!
Ran into the same problem when trying to build a color picker similar to above. What worked was "wrapping" the color picker in a view with a Dismiss button. And also discovered that the bar at the top of the view would allow the picker to now be dragged down and away. Below is my wrapper. (One could add more features such as a title to the bar.)
struct ColorWrapper: View {
var inputColor: UIColor
#Binding var isShowingColorPicker: Bool
#Binding var selectedColor: UIColor?
var body: some View {
VStack {
HStack {
Spacer()
Button("Dismiss", action: {
isShowingColorPicker = false
}).padding()
}
ColorPickerView(inputColor: inputColor, selectedColor: $selectedColor)
}
}
}
And for completeness, here is my version of the color picker:
import SwiftUI
struct ColorPickerView: UIViewControllerRepresentable {
typealias UIViewControllerType = UIColorPickerViewController
var inputColor: UIColor
#Binding var selectedColor: UIColor?
#Environment(\.presentationMode) var isPresented
func makeUIViewController(context: Context) -> UIColorPickerViewController {
let picker = UIColorPickerViewController()
picker.delegate = context.coordinator
picker.supportsAlpha = false
picker.selectedColor = inputColor
return picker
}
func updateUIViewController(_ uiViewController: UIColorPickerViewController, context: Context) {
uiViewController.supportsAlpha = false
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIColorPickerViewControllerDelegate {
var parent: ColorPickerView
init(parent: ColorPickerView) {
self.parent = parent
}
func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) {
parent.isPresented.wrappedValue.dismiss()
}
func colorPickerViewController(_ viewController: UIColorPickerViewController, didSelect color: UIColor, continuously: Bool) {
parent.selectedColor = color
// parent.isPresented.wrappedValue.dismiss()
}
}
}
So I am using a WKWebView within UIViewRepresentable so I can show a web view in my SwiftUI view.
For a while I could not figure out why my SwiftUI view would not update when the Coordinator would set #Publsihed properties that affect the SwiftUI view.
In the process I finally understood better how UIViewRepresentable works and realized what the problem was.
This is the UIViewRepresentable:
struct SwiftUIWebView : UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
func makeCoordinator() -> Coordinator {
Coordinator(self, viewModel: viewModel)
}
let webView = WKWebView()
func makeUIView(context: Context) -> WKWebView {
....
return self.webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
// This made my SwiftUI view update properly when the web view would report loading progress etc..
context.coordinator.viewModel = viewModel
}
}
The SwiftUI view would pass in the viewModel, then makeCoordinator would be called (only the first time at init...), then the Coordinator would be returned with that viewModel.
However, subsequently when a new viewModel was passed in on updates and not on coordinator init, the coordinator would just keep the old viewModel and things would stop working.
So I added this in the updateUIView... call, which did fix the problem:
context.coordinator.viewModel = viewModel
Question:
Is there a way to pass in the viewModel to the Coordinator during the func makeUIView(context: Context) -> WKWebView { ... } so that if a new viewModel is passed in to SwiftUIWebView the coordinator would also get the change automatically instead of me having to add:
context.coordinator.viewModel = viewModel
in updateUIView...?
EDIT:
Here is the entire code. The root content view:
struct ContentView: View {
#State var showTestModal = false
#State var redrawTest = false
var body: some View {
NavigationView {
VStack {
Button(action: {
showTestModal.toggle()
}) {
Text("Show modal")
}
if redrawTest {
Text("REDRAW")
}
}
}
.fullScreenCover(isPresented: $showTestModal) {
WebContentViewTest(redraw: $redrawTest)
}
}
}
And what the Content view presents:
struct SwiftUIProgressBar: View {
#Binding var progress: Double
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(Color.gray)
.opacity(0.3)
.frame(width: geometry.size.width, height: geometry.size.height)
Rectangle()
.foregroundColor(Color.blue)
.frame(width: geometry.size.width * CGFloat((self.progress)),
height: geometry.size.height)
.animation(.linear(duration: 0.5))
}
}
}
}
struct SwiftUIWebView : UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
func makeCoordinator() -> Coordinator {
Coordinator(self, viewModel: viewModel)
}
let webView = WKWebView()
func makeUIView(context: Context) -> WKWebView {
print("SwiftUIWebView MAKE")
if let url = URL(string: viewModel.link) {
self.webView.load(URLRequest(url: url))
}
return self.webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
//add your code here...
}
}
class Coordinator: NSObject {
private var viewModel: WebViewModel
var parent: SwiftUIWebView
private var estimatedProgressObserver: NSKeyValueObservation?
init(_ parent: SwiftUIWebView, viewModel: WebViewModel) {
print("Coordinator init")
self.parent = parent
self.viewModel = viewModel
super.init()
estimatedProgressObserver = self.parent.webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in
print(Float(webView.estimatedProgress))
guard let weakSelf = self else{return}
print("in progress observer: model is: \(Unmanaged.passUnretained(weakSelf.parent.viewModel).toOpaque())")
weakSelf.parent.viewModel.progress = webView.estimatedProgress
}
}
deinit {
estimatedProgressObserver = nil
}
}
class WebViewModel: ObservableObject {
#Published var progress: Double = 0.0
#Published var link : String
init (progress: Double, link : String) {
self.progress = progress
self.link = link
print("model init: \(Unmanaged.passUnretained(self).toOpaque())")
}
}
struct WebViewContainer: View {
#ObservedObject var model: WebViewModel
var body: some View {
ZStack {
SwiftUIWebView(viewModel: model)
VStack {
if model.progress >= 0.0 && model.progress < 1.0 {
SwiftUIProgressBar(progress: .constant(model.progress))
.frame(height: 15.0)
.foregroundColor(.accentColor)
}
Spacer()
}
}
}
}
struct WebContentViewTest : View {
#Binding var redraw:Bool
var body: some View {
let _ = print("WebContentViewTest body")
NavigationView {
ZStack(alignment: .topLeading) {
if redraw {
WebViewContainer(model: WebViewModel(progress: 0.0, link: "https://www.google.com"))
}
VStack {
Button(action: {
redraw.toggle()
}) {
Text("redraw")
}
Spacer()
}
}
.navigationBarTitle("Test Modal", displayMode: .inline)
}
}
}
If you run this you will see that while WebViewModel can get initialized multiple times, the coordinator will only get initialized once and the viewModel in it does not get updated. Because of that, things break after the first redraw.
I want to show Image Viewer over all views when users tap into an image. It's working well without sheets but if there is a sheet on view, the image viewer stays behind it. How can I show image viewer over sheets too? I researched too much but I could not find any solution yet.
ContentView:
#ObservedObject var authVM: AuthVM = .shared
var body: some View {
ZStack{
TabView(selection: self.$authVM.selectedTab) {
HomeTab()
.tabItem {
Image(systemName: "house.fill")
.renderingMode(.template)
Text("Home")
}.tag(SelectedTab.home)
// Other tabs...
}
if self.authVM.showImageViewer{
PhotoViewer(viewerImages: $authVM.images, currentPageIndex: $authVM.imageIndex)
.edgesIgnoringSafeArea(.vertical)
}
}
}
I'm using SKPhotoBrowser pod (UIKit) with UIViewControllerRepresentable, maybe we can do something in UIKit to solve it?
import SwiftUI
import SKPhotoBrowser
struct PhotoViewer: UIViewControllerRepresentable {
#ObservedObject var authVM: AuthVM = .shared
#Binding var viewerImages:[SKPhoto]
#Binding var currentPageIndex: Int
func makeUIViewController(context: Context) -> SKPhotoBrowser {
SKPhotoBrowserOptions.displayHorizontalScrollIndicator = false
let browser = SKPhotoBrowser(photos: viewerImages)
browser.initializePageIndex(currentPageIndex)
browser.delegate = context.coordinator
return browser
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIViewController(_ browser: SKPhotoBrowser, context: Context) {
browser.photos = viewerImages
browser.currentPageIndex = currentPageIndex
}
class Coordinator: NSObject, SKPhotoBrowserDelegate {
var control: PhotoViewer
init(_ control: PhotoViewer) {
self.control = control
}
func didShowPhotoAtIndex(_ browser: PhotoViewer) {
self.control.currentPageIndex = browser.currentPageIndex
}
func didDismissAtPageIndex(_ index: Int) {
self.control.authVM.showImageViewer = false
}
}
}
I used this approach to incorporate camera with swiftUI:
https://medium.com/#gaspard.rosay/create-a-camera-app-with-swiftui-60876fcb9118
The UIViewControllerRepresentable is implemented by PageFourView class. PageFourView is one of the TabView of the parental View. I have an #EnvironmentObject passed from the SceneDelegate to the parent view and then to PageFourView. But when I am trying to acess #EnvironmentObject from makeUIViewController method of PageFourView I get an error:
Fatal error: No ObservableObject of type Data found. A
View.environmentObject(_:) for Data may be missing as an ancestor of
this view
... even though I can see the #Environment object from context.environment. Here is my code:
import UIKit
import SwiftUI
import Combine
final class PageFourView: UIViewController, UIViewControllerRepresentable {
public typealias UIViewControllerType = PageFourView
#EnvironmentObject var data: Data
var previewView: UIView!
override func viewDidLoad() {
previewView = UIView(frame: CGRect(x:0, y:0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
previewView.contentMode = UIView.ContentMode.scaleAspectFit
view.addSubview(previewView)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<PageFourView>) -> PageFourView {
print(context.environment)
print(self.data.Name)
return PageFourView()
}
func updateUIViewController(_ uiViewController: PageFourView, context: UIViewControllerRepresentableContext<PageFourView>) {
}
}
struct PageFourView_Previews: PreviewProvider {
#State static var data = Data()
static var previews: some View {
PageFourView().environmentObject(self.data)
}
}
here is the parental view that PageFourView is called from:
import SwiftUI
struct AppView: View {
#EnvironmentObject var data: Data
var body: some View {
TabView {
PageOneView().environmentObject(data)
.tabItem {
Text("PageOne")
}
PageTwoView().environmentObject(data)
.tabItem {
Text("PageTwo")
}
PageThreeView().environmentObject(data)
.tabItem {
Text("PageThree")
}
PageFourView().environmentObject(data)
.tabItem {
Text("PageFour")
}
}
}
}
struct AppView_Previews: PreviewProvider {
#State static var data = Data()
static var previews: some View {
AppView().environmentObject(self.data)
}
}
final class CameraViewController: UIViewController {
let cameraController = CameraController()
var previewView: UIView!
override func viewDidLoad() {
previewView = UIView(frame: CGRect(x:0, y:0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
previewView.contentMode = UIView.ContentMode.scaleAspectFit
view.addSubview(previewView)
cameraController.prepare {(error) in
if let error = error {
print(error)
}
try? self.cameraController.displayPreview(on: self.previewView)
}
}
}
extension CameraViewController : UIViewControllerRepresentable{
public typealias UIViewControllerType = CameraViewController
public func makeUIViewController(context: UIViewControllerRepresentableContext<CameraViewController>) -> CameraViewController {
return CameraViewController()
}
public func updateUIViewController(_ uiViewController: CameraViewController, context: UIViewControllerRepresentableContext<CameraViewController>) {
}
}
And UIViewRepresentable and UIViewControllerRepresentable is-a View and must be a struct.
In described case controller representable is not needed, because you operate with view, so here is corrected code:
struct PageFourView: UIViewRepresentable {
#EnvironmentObject var data: Data
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: CGRect(x:0, y:0, width: UIScreen.main.bounds.size.width,
height: UIScreen.main.bounds.size.height))
view.contentMode = UIView.ContentMode.scaleAspectFit
print(context.environment)
print(self.data.Name)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
btw, you don't need to pass .environmentObject to subviews in same view hierarchy, only for new hierarchy, like sheets, so you can use simplified code as below
var body: some View {
TabView {
PageOneView()
.tabItem {
Text("PageOne")
}
PageTwoView()
.tabItem {
Text("PageTwo")
}
PageThreeView()
.tabItem {
Text("PageThree")
}
PageFourView()
.tabItem {
Text("PageFour")
}
}
}
Update: for CameraViewController just wrap it as below
struct CameraView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> CameraViewController {
CameraViewController()
}
func updateUIViewController(_ uiViewController: CameraViewController, context: Context) {
}
}