Adding Next/ Prev buttons to FSCalendar in SwiftUI - swiftui

I've been playing around with FSCalendar and it's helped me build my own customized calendar.
Because it's written in UIKit, I've had a couple of problems integrating it to my SwiftUI project, such as adding a Next and Previous button to the sides of the calendar.
This is what I have so far:
ContentView, where I used an HStack to add the buttons to the sides of my calendar
struct ContentView: View {
let myCalendar = MyCalendar()
var body: some View {
HStack(spacing: 5) {
Button(action: {
myCalendar.previousTapped()
}) { Image("back-arrow") }
MyCalendar()
Button(action: {
myCalendar.nextTapped()
}) { Image("next-arrow") }
}
}}
And the MyCalendar struct which, in order to integrate the FSCalendar library, is a UIViewRepresentable.
This is also where I added the two functions (nextTapped and previousTapped) which should change the displayed month when the Buttons are tapped:
struct MyCalendar: UIViewRepresentable {
let calendar = FSCalendar(frame: CGRect(x: 0, y: 0, width: 320, height: 300))
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> FSCalendar {
calendar.delegate = context.coordinator
calendar.dataSource = context.coordinator
return calendar
}
func updateUIView(_ uiView: FSCalendar, context: Context) {
}
func nextTapped() {
let nextMonth = Calendar.current.date(byAdding: .month, value: 1, to: calendar.currentPage)
calendar.setCurrentPage(nextMonth!, animated: true)
print(calendar.currentPage)
}
func previousTapped() {
let previousMonth = Calendar.current.date(byAdding: .month, value: -1, to: calendar.currentPage)
calendar.setCurrentPage(previousMonth!, animated: true)
print(calendar.currentPage)
}
class Coordinator: NSObject, FSCalendarDelegateAppearance, FSCalendarDataSource, FSCalendarDelegate {
var parent: MyCalendar
init(_ calendar: MyCalendar) {
self.parent = calendar
}
func minimumDate(for calendar: FSCalendar) -> Date {
return Date()
}
func maximumDate(for calendar: FSCalendar) -> Date {
return Date().addingTimeInterval((60 * 60 * 24) * 365)
}
}}
This is what it looks like in the simulator:
As you can see, I've managed to print the currentPage in the terminal whenever the next or previous buttons are tapped, but the currentPage is not changing in the actual calendar.
How could I fix this?

As you are using UIViewRepresentable protocol for bind UIView class with SwiftUI. Here you have to use ObservableObject - type of object with a publisher that emits before the object has changed.
You can check the code below for the resulting output: (Edit / Improvement most welcomed)
import SwiftUI
import UIKit
import FSCalendar
class CalendarData: ObservableObject{
#Published var selectedDate : Date = Date()
#Published var titleOfMonth : Date = Date()
#Published var crntPage: Date = Date()
}
struct ContentView: View {
#ObservedObject private var calendarData = CalendarData()
var strDateSelected: String {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
dateFormatter.locale = Locale.current
return dateFormatter.string(from: calendarData.selectedDate)
}
var strMonthTitle: String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMMM yyyy"
dateFormatter.locale = Locale.current
return dateFormatter.string(from: calendarData.titleOfMonth)
}
var body: some View {
VStack {
HStack(spacing: 100) {
Button(action: {
self.calendarData.crntPage = Calendar.current.date(byAdding: .month, value: -1, to: self.calendarData.crntPage)!
}) { Image(systemName: "arrow.left") }
.frame(width: 35, height: 35, alignment: .leading)
Text(strMonthTitle)
.font(.headline)
Button(action: {
self.calendarData.crntPage = Calendar.current.date(byAdding: .month, value: 1, to: self.calendarData.crntPage)!
}) { Image(systemName: "arrow.right") }
.frame(width: 35, height: 35, alignment: .trailing)
}
CustomCalendar(dateSelected: $calendarData.selectedDate, mnthNm: $calendarData.titleOfMonth, pageCurrent: $calendarData.crntPage)
.padding()
.background(
RoundedRectangle(cornerRadius: 25.0)
.foregroundColor(.white)
.shadow(color: Color.black.opacity(0.2), radius: 10.0, x: 0.0, y: 0.0)
)
.frame(height: 350)
.padding(25)
Text(strDateSelected)
.font(.title)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct CustomCalendar: UIViewRepresentable {
typealias UIViewType = FSCalendar
#Binding var dateSelected: Date
#Binding var mnthNm: Date
#Binding var pageCurrent: Date
var calendar = FSCalendar()
var today: Date{
return Date()
}
func makeUIView(context: Context) -> FSCalendar {
calendar.dataSource = context.coordinator
calendar.delegate = context.coordinator
calendar.appearance.headerMinimumDissolvedAlpha = 0
return calendar
}
func updateUIView(_ uiView: FSCalendar, context: Context) {
uiView.setCurrentPage(pageCurrent, animated: true) // --->> update calendar view when click on left or right button
}
func makeCoordinator() -> CustomCalendar.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, FSCalendarDelegate, FSCalendarDataSource {
var parent: CustomCalendar
init(_ parent: CustomCalendar) {
self.parent = parent
}
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
parent.dateSelected = date
}
func calendarCurrentPageDidChange(_ calendar: FSCalendar) {
parent.pageCurrent = calendar.currentPage
parent.mnthNm = calendar.currentPage
}
}
}
Output:

Related

How to use function from other struct/view in SwiftUI?

Newbie SwiftUI Dev here.
I want to create a scheduling app in SwiftUI and I would like to create a button in navigation bar which change calendar's scope.
From .week to month and return.
struct HomeVC: View {
init() {
navbarcolor.configureWithOpaqueBackground()
navbarcolor.backgroundColor = .systemGreen
navbarcolor.titleTextAttributes = [.foregroundColor: UIColor.white]
navbarcolor.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
UINavigationBar.appearance().standardAppearance = navbarcolor
UINavigationBar.appearance().scrollEdgeAppearance = navbarcolor
}
#State private var selectedDate = Date()
var body: some View {
NavigationView{
VStack{
CalendarRepresentable(selectedDate: $selectedDate)
.frame(height: 300)
.padding(.top, 15)
Spacer()
ListView()
}
.navigationBarTitle("Calendar")
.toolbar {
Button(action: {
switchCalendarScope()
}) {
Text("Toggle")
}
}
}
}
}
This is my calendar struct, and I would like to take from here the switchCalendarScope function, and use it into button's action, but doesn't work.
struct CalendarRepresentable: UIViewRepresentable{
typealias UIViewType = FSCalendar
#Binding var selectedDate: Date
var calendar = FSCalendar()
func switchCalendarScope(){
if calendar.scope == FSCalendarScope.month {
calendar.scope = FSCalendarScope.week
} else {
calendar.scope = FSCalendarScope.month
}
}
func updateUIView(_ uiView: FSCalendar, context: Context) { }
func makeUIView(context: Context) -> FSCalendar {
calendar.delegate = context.coordinator
calendar.dataSource = context.coordinator
calendar.allowsMultipleSelection = true
calendar.scrollDirection = .vertical
calendar.scope = .week
//:Customization
calendar.appearance.headerTitleFont = UIFont.systemFont(ofSize: 25, weight: UIFont.Weight.heavy)
calendar.appearance.weekdayFont = .boldSystemFont(ofSize: 15)
calendar.appearance.weekdayTextColor = .black
calendar.appearance.selectionColor = .systemGreen
calendar.appearance.todayColor = .systemGreen
calendar.appearance.caseOptions = [.headerUsesUpperCase, .weekdayUsesUpperCase]
calendar.appearance.headerTitleColor = .black
return calendar
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, FSCalendarDelegate, FSCalendarDataSource {
var parent: CalendarRepresentable
var formatter = DateFormatter()
init(_ parent: CalendarRepresentable) {
self.parent = parent
}
func calendar(_ calendar: FSCalendar, numberOfEventsFor date: Date) -> Int {
return 0
}
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
formatter.dateFormat = "dd-MM-YYYY"
print("Did select == \(formatter.string(from: date))")
}
func calendar(_ calendar: FSCalendar, didDeselect date: Date, at monthPosition: FSCalendarMonthPosition) {
formatter.dateFormat = "dd-MM-YYYY"
print("Did de-select == \(formatter.string(from: date))")
}
}
}
Can anybody help?
You don't need to trigger the function in your UIViewRepresentable. You simply need to declare a variable in there that is the representation of the selected scope, and pass that in with your initializer. I am going to assume that your scope variable is of Type Scope for this:
struct CalendarRepresentable: UIViewRepresentable {
typealias UIViewType = FSCalendar
#Binding var selectedDate: Date
var calendar = FSCalendar()
var scope: Scope
func updateUIView(_ uiView: FSCalendar, context: Context) { }
func makeUIView(context: Context) -> FSCalendar {
calendar.delegate = context.coordinator
calendar.dataSource = context.coordinator
calendar.allowsMultipleSelection = true
calendar.scrollDirection = .vertical
// Set scope here
calendar.scope = scope
//:Customization
...
return calendar
}
...
}
Then from the HomeVC view you would call it like this:
CalendarRepresentable(selectedDate: $selectedDate, scope: scope)
The view will get recreated as needed. Also, one last thing, in SwiftUI there are no ViewControllers. Your HomeVC should just be named Home. It is the view, not a view controller, and they work differently and take a different mental model. This is why you were struggling in solving this. Even the UIViewRepresentable is a view in the end, and it just wraps a ViewController and instantiates the view. And they are all structs; you don't mutate a struct, you simply recreate it when you need to change it.

How to complement UIScrollView in navigationView?

I'm currently developing an application using SwiftUI and trying to refresh data using pull action.
When I implement the function in List it works, but if I use that in NavigationView the function doesn't work...
// ---OK↓---
RefreshScrollViewTest(refreshControl: self.$refreshControl)
// ---NG↓---
NavigationView{
RefreshScrollViewTest(refreshControl: self.$refreshControl)
}
Is there any way to use the function in NavigationView?
Here are the codes:
import SwiftUI
struct NavigationRefreshTest: View {
#State var refreshControl = UIRefreshControl()
var body: some View {
// NavigationView{
RefreshScrollViewTest(refreshControl: self.$refreshControl)
// }
}
}
struct RefreshListTest:View {
#Binding var refreshControl:UIRefreshControl
var body: some View{
List{
Text("test1")
Text("test2")
Text("test3")
}
.onAppear{
NotificationCenter.default.addObserver(forName: NSNotification.Name("Update"), object: nil, queue: .main){ (_) in
DispatchQueue.main.asyncAfter(deadline: .now() + 1){
print("update...")
}
}
}
}
}
struct RefreshScrollViewTest:UIViewRepresentable {
func makeCoordinator() ->Coodinator {
return RefreshScrollViewTest.Coodinator()
}
#Binding var refreshControl:UIRefreshControl
func makeUIView(context: Context) -> UIScrollView {
let view = UIScrollView()
self.refreshControl.attributedTitle = NSAttributedString(string: "Loding")
self.refreshControl.addTarget(context.coordinator, action: #selector(context.coordinator.update), for: .valueChanged)
view.showsVerticalScrollIndicator = false
view.refreshControl = self.refreshControl
view.contentSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
let child = UIHostingController(rootView: RefreshListTest(refreshControl: self.$refreshControl))
child.view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height)
view.addSubview(child.view)
return view
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
}
class Coodinator:NSObject{
#objc func update(){
NotificationCenter.default.post(name: NSNotification.Name("Update"), object: nil)
}
}
}
Xcode: Version 12.3
iOS: 14.0
I haven't solved your problem, but you should use this library SwiftUIRefresh

Toolbar accessory added to UITextView as a UIViewRepresentable only displays after first launch of iMessage extension application

Would like to have the toolbar show all the time.
Any help is greatly appreciated as this is a real drag for the user experience.
I've added a toolbar to the keyboard for the TextView as shown below.
However the toolbar only shows after the app has run once. Meaning the toolbar does not show the first time the app is run. the app works every time after the initial load.
This is on IOS 14.3, Xcode 12.3, Swift 5, iMessage extension app. Fails on simulator or real device.
struct CustomTextEditor: UIViewRepresentable {
#Binding var text: String
private var returnType: UIReturnKeyType
private var keyType: UIKeyboardType
private var displayDoneBar: Bool
private var commitHandler: (()->Void)?
init(text: Binding<String>,
returnType: UIReturnKeyType = .done,
keyboardType: UIKeyboardType,
displayDoneBar: Bool,
onCommit: (()->Void)?) {
self._text = text
self.returnType = returnType
self.keyType = keyboardType
self.displayDoneBar = displayDoneBar
self.commitHandler = onCommit
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.keyboardType = keyType
textView.returnKeyType = returnType
textView.backgroundColor = .clear
textView.font = UIFont.systemFont(ofSize: 20, weight: .regular)
textView.isEditable = true
textView.delegate = context.coordinator
if self.displayDoneBar {
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace,
target: self,
action: nil)
let doneButton = UIBarButtonItem(title: "Close Keyboard",
style: .done,
target: self,
action: #selector(textView.doneButtonPressed(button:)))
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 300, height: 50))
toolBar.items = [flexibleSpace, doneButton, flexibleSpace]
toolBar.setItems([flexibleSpace, doneButton, flexibleSpace], animated: true)
toolBar.sizeToFit()
textView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
textView.translatesAutoresizingMaskIntoConstraints = true
textView.inputAccessoryView = toolBar
}
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
textView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: CustomTextEditor
init(_ textView: CustomTextEditor) {
self.parent = textView
}
func textViewDidChange(_ textView: UITextView) {
self.parent.$text.wrappedValue = textView.text
}
func textViewDidEndEditing(_ textView: UITextView) {
self.parent.$text.wrappedValue = textView.text
parent.commitHandler?()
}
}
}
extension UITextView {
#objc func doneButtonPressed(button:UIBarButtonItem) -> Void {
self.resignFirstResponder()
}
}
This is how it's called...
import SwiftUI
final class ContentViewHostController: UIHostingController<ContentView> {
weak var myWindow: UIWindow?
init() {
super.init(rootView: ContentView())
}
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: ContentView())
}
}
let kTextColor = Color(hex: "3E484F")
let kOverlayRadius: CGFloat = 10
let kOverlayWidth: CGFloat = 2
let kOverlayColor = kTextColor
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
Spacer()
CustomTextEditor(text: $text, returnType: .default, keyboardType: .default, displayDoneBar: true, onCommit: nil)
.foregroundColor(kTextColor)
.overlay(
RoundedRectangle(cornerRadius: kOverlayRadius)
.stroke(kOverlayColor, lineWidth: kOverlayWidth)
)
.frame(width: 200, height: 100, alignment: .center)
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
From MessagesViewController...
override func willBecomeActive(with conversation: MSConversation) {
let childViewCtrl = ContentViewHostController()
childViewCtrl.view.layoutIfNeeded() // avoids snapshot warning?
if let window = self.view.window {
childViewCtrl.myWindow = window
window.rootViewController = childViewCtrl
}
}

SwiftUI PencilKit Coordinator for navigation

Rather than selecting from a list, I'm trying to navigate between drawings like a book by using buttons to cycle through, but the canvas doesn't update.
I'm following the great tutorial by DevTechie at https://www.youtube.com/watch?v=amZH2i6l004&list=PLbrKvTeCrFAfoACvHOPWFmDIaKUqBZgEr&index=5
The github repo is at https://github.com/devtechie/DrawingDocuments
Here's my ContentView and my version of the DrawingWrapper. The DrawingWrapper uses a DrawingManager (SwiftUI) to pull from CoreData and the DrawingViewController to define a PKCanvas. I wasn't sure which delegate to use and really struggling understanding how to refresh the canvas.
ContentView
struct ContentView: View {
#StateObject var manager = DrawingManager()
#State var addNewShown = false
#State var pageNumber: Int = 0
#State var newVar = UUID()
var body: some View {
VStack{
Text(manager.docs[pageNumber].name!)
HStack{
Button(action:{
pageNumber -= 1
newVar = manager.docs[pageNumber].id!
//desiredDoc = manager.docs[pageNumber]
}){
Image(systemName: "chevron.left")
}
Spacer()
Button(action:{
pageNumber += 1
newVar = manager.docs[pageNumber].id!
//desiredDoc = manager.docs[pageNumber]
}){
Image(systemName: "chevron.right")
}
}
}
}
DrawingWrapper
struct DrawingWrapper: UIViewControllerRepresentable {
var manager: DrawingManager
#Binding var doc: DrawingDoc
typealias UIViewControllerType = DrawingViewController
class Coordinator: NSObject, PKCanvasViewDelegate {
var parent: DrawingWrapper
init(_ parent: DrawingWrapper){
self.parent = parent
}
func canvasViewDidFinishRendering(_ canvasView: PKCanvasView) {
if let uiDrawing = canvasView.drawing as? PKDrawing {
parent.doc.data = uiDrawing.dataRepresentation()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<DrawingWrapper>) -> DrawingWrapper.UIViewControllerType {
let viewController = DrawingViewController()
viewController.drawingData = doc.data!
viewController.drawingChanged = {data in
manager.update(data: data, for: doc.id!)
}
viewController.delegate = context.coordinator
return viewController
}
func updateUIViewController(_ uiViewController: DrawingViewController, context: UIViewControllerRepresentableContext<DrawingWrapper>) {
uiViewController.drawingData = doc.data!
}
}

Read #Binding Value

I try to make custom textfield so I give it #Binding to response to the text value as showing below, The problem is when I try to detect the change of text its just response in the "Preview", But when run the app on the "Simulator" it doesn't response, I tried many different ways to solve this problem but nothing is work.
import SwiftUI
struct MyTextField: UIViewRepresentable {
typealias UIViewType = UITextField
#Binding var becomeFirstResponder: Bool
#Binding var text: String
var placeholder = ""
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.widthAnchor.constraint(equalToConstant: 320).isActive = true
textField.textColor = UIColor.systemBlue
textField.font = UIFont.boldSystemFont(ofSize: 22)
textField.textAlignment = .left
textField.keyboardType = .default
textField.minimumFontSize = 13
textField.adjustsFontSizeToFitWidth = true
textField.text = self._text.wrappedValue
textField.placeholder = self.placeholder
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ textField: UITextField, context: Context) {
if self.becomeFirstResponder {
DispatchQueue.main.async {
textField.becomeFirstResponder()
self.becomeFirstResponder = false
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: MyTextField
init(parent: MyTextField) {
self.parent = parent
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let currentText = textField.text ?? ""
guard let stringRange = Range(range, in: currentText) else {
return false
}
let updateText = currentText.replacingCharacters(in: stringRange, with: string)
return updateText.count < 20
}
}
}
struct TextFieldFirstResponder: View {
#State private var becomeFirstResponder = false
#State private var text = "LLL"
private var placeholder = "Untitled"
var body: some View {
VStack {
ZStack(alignment: .trailing) {
MyTextField(becomeFirstResponder: self.$becomeFirstResponder, text: self.$text, placeholder: self.placeholder)
.frame(width: 343, height: 56, alignment: .leading)
.padding(EdgeInsets(top: 27, leading: 13, bottom: 0, trailing: 0))
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
.frame(width: 342, height: 56, alignment: .center)
)
.onAppear {
self.becomeFirstResponder = true
}
}
Text("\(self.$text.wrappedValue)") // <------ Do not read the "text"
}
}
}
struct TextFieldFirstResponder_Previews: PreviewProvider {
static var previews: some View {
TextFieldFirstResponder()
}
}
You don't need to use $ here, read property directly
Text(self.text) // <------ Do not use $
and I assume you wanted to update it via binding
let updateText = currentText.replacingCharacters(in: stringRange, with: string)
self.parent.text = updateText // << here !!
return updateText.count < 20
Tested & worked with Xcode 12.1 / iOS 14.1