I'm trying to build a calendar app for iOS using KVKCalendar but it's not originally build for Swift UI so I'm kind of struggling to achieve what I want to do.
My goal is changing calendar type (such as daily, weekly, monthly) by a segment controller, exactly like this example (which is already provided in git repository).
sampleUI
So far, I managed to display daily style calendar view as default. But I don't know how I can change its calendar type from ContentView.swift
Does anyone know about this type of ViewController ↔ Swift UI thing?
My code
My ContentView.swift is like this
import SwiftUI
import KVKCalendar
struct ContentView: View {
#State var events: [Event] = []
#State var selectedType: CalendarType = .week // I want this to change its calendar type.
var body: some View {
VStack{
CalendarDisplayView(events: $events)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And my ContentDisplayView.swift is like this
import SwiftUI
import EventKit
import KVKCalendar
struct CalendarDisplayView: UIViewRepresentable {
#Binding var events: [Event]
public init(events: Binding<[Event]>) {
self._events = events
}
private var calendar: CalendarView = {
var style = Style()
if UIDevice.current.userInterfaceIdiom == .phone {
style.timeline.widthTime = 40
style.timeline.currentLineHourWidth = 45
style.timeline.offsetTimeX = 2
style.timeline.offsetLineLeft = 2
style.headerScroll.titleDateAlignment = .center
style.headerScroll.isAnimateTitleDate = true
style.headerScroll.heightHeaderWeek = 70
style.event.isEnableVisualSelect = false
style.month.isHiddenTitle = true
style.month.weekDayAlignment = .center
} else {
style.timeline.widthEventViewer = 350
style.headerScroll.fontNameDay = .systemFont(ofSize: 17)
}
style.month.autoSelectionDateWhenScrolling = true
style.timeline.offsetTimeY = 25
style.startWeekDay = .monday
style.timeSystem = .current ?? .twelve
style.systemCalendars = ["Calendar"]
if #available(iOS 13.0, *) {
style.event.iconFile = UIImage(systemName: "paperclip")
}
style.locale = Locale.current
style.timezone = TimeZone.current
return CalendarView(frame: UIScreen.main.bounds, style: style)
}()
func makeUIView(context: UIViewRepresentableContext<CalendarDisplayView>) -> CalendarView {
calendar.dataSource = context.coordinator
calendar.delegate = context.coordinator
calendar.reloadData()
return calendar
}
func updateUIView(_ uiView: CalendarView, context: UIViewRepresentableContext<CalendarDisplayView>) {
context.coordinator.events = events
}
func makeCoordinator() -> CalendarDisplayView.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, CalendarDataSource, CalendarDelegate {
private let view: CalendarDisplayView
var events: [Event] = [] {
didSet {
view.calendar.reloadData()
}
}
init(_ view: CalendarDisplayView) {
self.view = view
super.init()
}
func eventsForCalendar(systemEvents: [EKEvent]) -> [Event] {
return events
}
}
}
First, add a #Binding inside CalendarDisplayView so it can update:
#Binding var selectedType: CalendarType
Then, pass in ContentView's $selectedType for CalendarDisplayView's selectedType, just like how you passed in $events.
/// here!
CalendarDisplayView(events: $events, selectedType: $selectedType)
Finally, update the type inside updateUIView, which is called whenever the #Binding changes.
func updateUIView(_ uiView: CalendarView, context: UIViewRepresentableContext<CalendarDisplayView>) {
context.coordinator.events = events
calendar.set(type: selectedType, date: Date()) /// I've never used this library, so you might need to replace `Date()` with something else
calendar.reloadData()
}
Related
I am using KVKCalendar on my SwiftUI application. I connected the UIKit library with bridge class called UIViewRepresntable. In this class, i override function which looks like:
func didSelectEvent(_ event: Event, type: CalendarType, frame: CGRect?){
// in this function i would like to redirect to SwiftUI View called EventDetailScreen(event.ID)
}
Also i added weak var navigationController: UINavigationController? in the top of Coordinator class so the whole class CalendarDisplayView, the bridge:
import SwiftUI
import EventKit
struct CalendarDisplayView: UIViewRepresentable {
#Binding var events: [Event]
#Binding var updatedDate: Date?
private var calendar = CalendarView(frame: .zero)
var selectDate = Date()
func makeUIView(context: UIViewRepresentableContext<CalendarDisplayView>) -> CalendarView {
calendar.dataSource = context.coordinator
calendar.delegate = context.coordinator
calendar.reloadData()
return calendar
}
func updateUIView(_ uiView: CalendarView, context: UIViewRepresentableContext<CalendarDisplayView>) {
context.coordinator.events = events
calendar.reloadData()
}
func makeCoordinator() -> CalendarDisplayView.Coordinator {
Coordinator(self)
}
public init(events: Binding<[Event]>, updatedDate: Binding<Date?>) {
self._events = events
var style = Style()
self._updatedDate = updatedDate
selectDate = Date()
var frame = UIScreen.main.bounds
frame.origin.y = 0
frame.size.height -= 160
calendar = CalendarView(frame: frame, style: style)
}
// MARK: Calendar DataSource and Delegate
class Coordinator: NSObject, CalendarDataSource, CalendarDelegate {
weak var navigationController: UINavigationController? //Added it by myself
func eventsForCalendar(systemEvents: [EKEvent]) -> [Event] {
return events
}
private var view: CalendarDisplayView
var events: [Event] = [] {
didSet {
view.calendar.reloadData()
}
}
var type: CalendarType = .day {
didSet {
view.calendar.set(type: type, date: view.selectDate)
view.calendar.reloadData()
}
}
var updatedDate: Date? {
didSet {
if let date = updatedDate {
view.selectDate = date
view.calendar.reloadData()
}
}
}
init(_ view: CalendarDisplayView) {
self.view = view
super.init()
}
func didSelectDates(_ dates: [Date], type: CalendarType, frame: CGRect?) {
updatedDate = dates.first ?? Date()
}
func didSelectEvent(_ event: Event, type: CalendarType, frame: CGRect?) {
// PROBLEM
}
}
}
CalendarScreen SwiftUI View
import SwiftUI
struct CalendarScreen: View {
#State private var typeCalendar = CalendarType.day
#State private var events: [Event] = []
#State private var updatedDate: Date?
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
ZStack(alignment: .trailing) {
CalendarDisplayView(events: $events, updatedDate: $updatedDate)
.edgesIgnoringSafeArea(.bottom)
}
}.onAppear{
viewModel.fetchCalendarEvents()
.navigationViewStyle(StackNavigationViewStyle())
}
}
I tried to create NavigationLink in function but it cant have return, because this is function that i override from KVKCalendar library.
func didSelectEvent(_ event: Event, type: CalendarType, frame: CGRect?){
NavigationLink(destination: EventDetailScreen(event.ID))
}
Moreover i tried to attach UIHostingController but didnt work aswell.
func didSelectEvent(_ event: Event, type: CalendarType, frame: CGRect?){
let screen = UIHostingController(rootView: EventDetailScreen(event.ID))
self.navigationController?.pushViewController(screen, animated: true)
}
I was trying to search how to change view from UIView class to SwiftUI view class but without proper result. Probably, it was wrong path.
Actually i found an answer by adding an extension CalendarDisplayView
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder?.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
and adding code in didSelectEvent
view.calendar.parentViewController?.navigationController?.pushViewController(EventDetialScreen(event.ID), animated: true)
I am using KVKCalendar with my SwiftUI application. I connected the UIKit library with bridge class called UIViewRepresntable. I have ViewModel which is fetching data from API and main class CalendarScreen which pushing the View.
CalendarScreen
struct CalendarScreen: View {
#State private var updatedDate: Date?
#StateObject private var viewModel: ViewModel = ViewModel()
var body: some View {
NavigationView {
ZStack(alignment: .trailing) {
CalendarDisplayView(events: $viewModel.events, updatedDate: $updatedDate)
.edgesIgnoringSafeArea(.bottom)
NavigationLink(destination: CalendarWriteScreen()) { //Custom Action Button here }
.padding(EdgeInsets(top: 0, leading: 0, bottom: 50, trailing: 20))
.frame(maxHeight: .infinity, alignment: .bottom)
}
}.onAppear {
viewModel.fetchCalendarEvents()
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
CalendarWriteScreen
import SwiftUI
struct CalendarWriteScreen: View {
weak var navigationController: UINavigationController?
#StateObject var viewModel = CalendarScreen.ViewModel()
var eventId: Int?
#State var eventData = CalendarEvent()
var body: some View {
ZStack(alignment: .center) {
ScrollView {
// Some Struct that form Event
}
}.onAppear {
if eventId != nil {
viewModel.fetchCalendarEvent(eventId: eventId!)
}
}
.navigationTitle("Event")
}
}
Bridge aka CalendarDisplayView
import EventKit
import SwiftUI
struct CalendarDisplayView: UIViewRepresentable {
#Binding var events: [Event]
#Binding var updatedDate: Date?
private var calendar = CalendarView(frame: .zero)
var selectDate = Date()
func makeUIView(context: UIViewRepresentableContext<CalendarDisplayView>) -> CalendarView {
calendar.dataSource = context.coordinator
calendar.delegate = context.coordinator
calendar.reloadData()
return calendar
}
func updateUIView(
_ uiView: CalendarView, context: UIViewRepresentableContext<CalendarDisplayView>
) {
context.coordinator.events = events
calendar.reloadData()
}
func makeCoordinator() -> CalendarDisplayView.Coordinator {
Coordinator(self)
}
public init(events: Binding<[Event]>, updatedDate: Binding<Date?>) {
self._events = events
var style = Style()
self._updatedDate = updatedDate
selectDate = Date()
var frame = UIScreen.main.bounds
frame.origin.y = 0
frame.size.height -= 160
calendar = CalendarView(frame: frame, style: style)
}
class Coordinator: NSObject, CalendarDataSource, CalendarDelegate {
weak var navigationController: UINavigationController?
func eventsForCalendar(systemEvents: [EKEvent]) -> [Event] {
// THIS FUNCTION SHOULD RELOAD MY EVENTS AND DISPLAY NEW EVENTS AFTER CalendarWriteScreen dissappear
return events
}
private var view: CalendarDisplayView
var events: [Event] = [] {
didSet {
view.calendar.reloadData()
}
}
var updatedDate: Date? {
didSet {
if let date = updatedDate {
view.selectDate = date
}
}
}
init(_ view: CalendarDisplayView) {
self.view = view
super.init()
}
func didSelectDates(_ dates: [Date], type: CalendarType, frame: CGRect?) {
updatedDate = dates.first ?? Date()
view.calendar.reloadData()
}
func didSelectEvent(_ event: Event, type: CalendarType, frame: CGRect?) {
let screen = UIHostingController(rootView: CalendarWriteScreen(eventId: Int(event.ID)))
view.calendar.parentViewController?.navigationController?.pushViewController(
screen, animated: true)
}
}
}
func timeFormatter(date: Date, format: String) -> String {
let formatter = DateFormatter()
formatter.dateFormat = format
return formatter.string(from: date)
}
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder?.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
ViewModel
import Combine
import Foundation
extension CalendarScreen {
class ViewModel: ObservableObject {
let calendarService = CalendarService()
#Published var calendarEvents: [CalendarEvent]
var cancellable: AnyCancellable?
init() {
self.calendarEvents = [CalendarEvent()]
self.calendarEvent = CalendarEvent()
self.events = []
}
func fetchCalendarEvents() {
cancellable = calendarService.getEvents()
.sink(
receiveCompletion: { _ in },
receiveValue: {
calendarEvents in self.calendarEvents = calendarEvents
self.createEvents()
})
}
func createEvents() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.events = self.calendarEvents.compactMap({ (item) in
var event = Event(ID: String(item.id))
event.start = self.dateTimeFormat.date(from: item.start) ?? Date()
event.end = self.dateTimeFormat.date(from: item.end) ?? Date()
event.color = Event.Color(UIColor(InvoiceColor(title: item.title)))
event.isAllDay = false
event.isContainsFile = false
event.title = TextEvent(timeline: item.title)
event.data = nil
return event
})
}
}
}
}
I tried to use .onDisappear function, tried to implement #State refresh variable but without proper funcionallity. Maybe i did something wrong.
One time i get in right and almost everything work but events get fetching everytime i clicked on View, so this implementation DOS attack on my local server. I added CalendarScreen.ViewModel to CalendarDisplayView and I implement function as follows:
func eventsForCalendar(systemEvents: [EKEvent]) -> [Event] {
viewModel.fetchCalendarEvents()
events = viewModel.events
return events
}
I would like to refresh UIViewRepresentable and variable events located in class CalendarDisplayView everytime view CalendarWriteScreen appears or disappears so view will reload and event will fetch from API
I have a share view controller attached to a list . When a user taps on a list row the ShareView is supposed to pop and display the contents of the list that was tapped . The issue is when I start the app and tap on a list item the ShareView text is blank, if I tap on a second different item the it shows the content:
// example : start the app, click List 1 and see that no content displays, then Click List 2 and content is displayed .
How can I make it so that the first time you tap a list the content is displayed in ShareView controller . This is the small project
import SwiftUI
struct ContentView: View {
let StringList = ["List 1","List 2","List 3","List 4","List 5"]
#State var TextExample = ""
#State var IsOpen = false
var body: some View {
List {
ForEach(StringList, id: \.self) { string in
Text(string)
.onTapGesture {
TextExample = string
IsOpen = true
}
}
}.background(SharingViewController(isPresenting: $IsOpen) {
print("\(TextExample)")
let av = UIActivityViewController(activityItems: [TextExample], applicationActivities: nil)
av.completionWithItemsHandler = { _, _, _, _ in
IsOpen = false // required for re-open !!!
}
return av
})
}
}
struct SharingViewController: UIViewControllerRepresentable {
#Binding var isPresenting: Bool
var content: () -> UIViewController
func makeUIViewController(context: Context) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
if isPresenting {
uiViewController.present(content(), animated: true, completion: nil)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Any suggestions would be great
Im not sure if this is an old bug, but I saw this before.
However, I found that using #StateObject class and its sub variable #Published to bind the data instead of local #State variable solved this problem.
Below is the working code which I tested. Also, here is the proof video link: https://drive.google.com/file/d/1ItlOf33vasO9WFRDokUq_PXLzUpR8UBa/view?usp=sharing
class VM : ObservableObject {
#Published var storedText = "" //added
}
struct ContentView: View {
#StateObject var viewModel = VM() //added
let StringList = ["List 1","List 2","List 3","List 4","List 5"]
#State var TextExample = ""
#State var IsOpen = false
var body: some View {
List {
ForEach(StringList, id: \.self) { string in
Text(string)
.onTapGesture {
viewModel.storedText = string //added
IsOpen = true
}
}
}.background(
SharingViewController(isPresenting: $IsOpen) {
print("\(viewModel.storedText)") //added
let av = UIActivityViewController(activityItems: [viewModel.storedText], applicationActivities: nil) //added
av.completionWithItemsHandler = { _, _, _, _ in
IsOpen = false // required for re-open !!!
}
return av
})
}
}
struct SharingViewController: UIViewControllerRepresentable {
#Binding var isPresenting: Bool
var content: () -> UIViewController
func makeUIViewController(context: Context) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
if isPresenting {
uiViewController.present(content(), animated: true, completion: nil)
}
}
}
How can I send data from SwiftUI view to UIKit ViewController in callback's closure?
Let's say we have SwiftUI View:
import SwiftUI
struct MyView: View {
var buttonPressed: (() -> Void)?
#State var someData = ""
var body: some View {
ZStack {
Color.purple
Button(action: {
someData = "new Data"
self.buttonPressed?()
}) {
Text("Button")
}
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView()
}
}
And ViewController where, inside which we have SwiftUI view:
import UIKit
import SwiftUI
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let swiftUIView = MyView()
let hostingViewController = UIHostingController(rootView: swiftUIView)
self.view.addSubview(hostingViewController.view)
hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false
hostingViewController.view.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
hostingViewController.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
hostingViewController.view.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
hostingViewController.view.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
hostingViewController.rootView.buttonPressed = {
print ("callback recived")
// i know i can try to get the data in this way, but if MyView become too complex than it won't look well
//print(hostingViewController.rootView.$someData)
}
}
}
How can I send someData via closure to ViewController?
You can pass it via argument, like
struct MyView: View {
var buttonPressed: ((String) -> Void)? // << here !!
#State var someData = ""
var body: some View {
ZStack {
Color.purple
Button(action: {
someData = "new Data"
self.buttonPressed?(someData) // << here !!
and
hostingViewController.rootView.buttonPressed = { value in // << here !!
print("callback received")
print(value)
}
I'm using a barcode scanner in my app, and to show each product on a view when it's barcode is scanned.
I have a sheet that show's details of the product and I want it to reload when ScannedCode is updated.
For each class that uses the barcode, I declare it like:
#ObservedObject var scannedCode: ScannedCode
But when I change the value, the views don't update.
I declare ScannedCode in my contentView:
class ScannedCode: ObservableObject {
#Published var barcode = ""
}
class dbProduct: ObservableObject {
#Published var result = Result()
}
struct ContentView: View {
let scannedCode = ScannedCode()
let product = dbProduct()
var body: some View {
ZStack {
ScannerView(scannedCode: scannedCode) //Starts the scanner
FoundItemSheet(scannedCode: scannedCode, product: product)
}
}
}
When the scanner finds a product, it updates the barcode in it's Coordinator:
class Coordinator: BarcodeScannerCodeDelegate, BarcodeScannerErrorDelegate {
#ObservedObject var scannedCode: ScannedCode
private var scannerView: ScannerView
init(_ scannerView: ScannerView, barcode: ScannedCode) {
self.scannerView = scannerView
self.scannedCode = barcode
}
func scanner(_ controller: BarcodeScannerViewController, didCaptureCode code: String, type: String) {
self.scannedCode.barcode = code //Updates the barcode here
controller.resetWithError(message: "Error message")
}
func scanner(_ controller: BarcodeScannerViewController, didReceiveError error: Error) {
print(error)
}
}
FoundItemSheet calls BottomSheetView, which displays the product. productDataView calculates which data to be shown on BottomSheetView as its content().
When the body is loaded for BottomSheetView() I call the API and store the data into an #ObservedObject so productDataView can access it.
.onAppear{
DispatchQueue.main.async {
let hashedValue = scannedCode.barcode.hashedValue("Ls75O8z1q9Ep9Kz0")
self.loadData(url: *API Call with barcode*) {
//...Load data converts from JSON and stores the product
This is where I suspect it could be going wrong, as the barcode that's changed in the scanner Coordinator isn't being updated here.
EDIT:
ScannerView
extension UINavigationController {
open override var preferredStatusBarStyle: UIStatusBarStyle {
return topViewController?.preferredStatusBarStyle ?? .default
}
}
struct ScannerView: UIViewControllerRepresentable {
#ObservedObject var scannedCode: ScannedCode
func makeCoordinator() -> Coordinator {
Coordinator(self, barcode: scannedCode)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ScannerView>) -> BarcodeScannerViewController {
return createAndConfigureScanner(context: context)
}
func updateUIViewController(_ uiViewController: BarcodeScannerViewController, context: UIViewControllerRepresentableContext<ScannerView>) {
uiViewController.reset(animated: false)
}
private func createAndConfigureScanner(context: UIViewControllerRepresentableContext<ScannerView>) -> BarcodeScannerViewController {
let barcodeVC = BarcodeScannerViewController()
barcodeVC.codeDelegate = context.coordinator
barcodeVC.errorDelegate = context.coordinator
return barcodeVC
}
}