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)
Related
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'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()
}
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
}
}
I have this simple example where I'm creating an #ObservedObject in a parent view and passing it to a child UIViewRepresentable. When I click "Button", it modifies the #ObservableObject but the child view never gets updated (i.e updateUIView is never called). Is there a different way to do this?
import SwiftUI
class UpdateViewState: ObservableObject {
#Published var words = ["A", "B", "C"]
func addWord(word: String) {
print("added word")
words.append(word)
}
}
struct UpdateView: View {
#ObservedObject private var state = UpdateViewState()
var body: some View {
VStack {
UpdateViewRepresentable(state: state)
Text("Button").onTapGesture {
self.state.addWord(word: "A")
}
}
}
}
struct UpdateViewRepresentable: UIViewRepresentable {
#ObservedObject var state: UpdateViewState
func makeUIView(context: Context) -> UILabel {
let view = UILabel()
view.text = "Hello World"
return view
}
func updateUIView(_ uiView: UILabel, context: UIViewRepresentableContext<UpdateViewRepresentable>) {
print("updateUIView")
uiView.text = state.words.joined(separator: ", ")
}
}
try this:
public final class UpdateViewState: ObservableObject {
#Published var words = ["A", "B", "C"]
func addWord(word: String) {
print("added word ", words)
words.append(word)
}
}
struct ContentView: View {
#EnvironmentObject private var state: UpdateViewState
var body: some View {
VStack {
UpdateViewRepresentable(state: .constant(state))
Text("Button").onTapGesture {
self.state.addWord(word: "A")
}
}.onAppear() {
self.state.words.append("aha")
}
}
}
struct UpdateViewRepresentable: UIViewRepresentable {
#Binding var state: UpdateViewState
func makeUIView(context: Context) -> UILabel {
let view = UILabel()
view.text = "Hello World"
return view
}
func updateUIView(_ uiView: UILabel, context: UIViewRepresentableContext<UpdateViewRepresentable>) {
print("updateUIView")
uiView.text = state.words.joined(separator: ", ")
}
}
This may help you in a very simple way:
var body: some View {
VStack {
UpdateViewRepresentable(state: state)
Text("Button").onTapGesture {
self.state.addWord(word: "A")
self.state.objectWillChange.send()
}
}
}
Try to use the line I added, this will the View to update itself. Make sure you use:
import Combine
I'm experimenting with SwiftUI and would like to fetch an update from my REST API with a search string.
However, I'm not sure how to bring the two components together now.
I hope you have an idea.
Here my Code:
struct ContentView: View {
#State private var searchTerm: String = ""
#ObservedObject var gameData: GameListViewModel = GameListViewModel(searchString: ### SEARCH STRING ???? ###)
var body: some View {
NavigationView{
Group{
// Games werden geladen...
if(self.gameData.isLoading) {
LoadingView()
}
// Games sind geladen:
else{
VStack{
// Suche:
searchBarView(text: self.$searchTerm)
// Ergebnisse:
List(self.gameData.games){ game in
NavigationLink(destination: GameDetailView(gameName: game.name ?? "0", gameId: 0)){
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(game.name ?? "Kein Name gefunden")
.font(.headline)
Text("Cover: \(game.cover?.toString() ?? "0")")
.font(.subheadline)
.foregroundColor(.gray)
}
}
}
}
}
}
}
.navigationBarTitle(Text("Games"))
}
}
}
And the search bar implementation:
import Foundation
import SwiftUI
struct searchBarView: UIViewRepresentable {
#Binding var text:String
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>){
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
print(searchText)
text = searchText
}
}
func makeCoordinator() -> searchBarView.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<searchBarView>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<searchBarView>) {
uiView.text = text
}
}
The search text should be inside the view model.
final class GameListViewModel: ObservableObject {
#Published var isLoading: Bool = false
#Published var games: [Game] = []
var searchTerm: String = ""
private let searchTappedSubject = PassthroughSubject<Void, Error>()
private var disposeBag = Set<AnyCancellable>()
init() {
searchTappedSubject
.flatMap {
self.requestGames(searchTerm: self.searchTerm)
.handleEvents(receiveSubscription: { _ in
DispatchQueue.main.async {
self.isLoading = true
}
},
receiveCompletion: { comp in
DispatchQueue.main.async {
self.isLoading = false
}
})
.eraseToAnyPublisher()
}
.replaceError(with: [])
.receive(on: DispatchQueue.main)
.assign(to: \.games, on: self)
.store(in: &disposeBag)
}
func onSearchTapped() {
searchTappedSubject.send(())
}
private func requestGames(searchTerm: String) -> AnyPublisher<[Game], Error> {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
return Fail(error: URLError(.badURL))
.mapError { $0 as Error }
.eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.mapError { $0 as Error }
.decode(type: [Game].self, decoder: JSONDecoder())
.map { searchTerm.isEmpty ? $0 : $0.filter { $0.title.contains(searchTerm) } }
.eraseToAnyPublisher()
}
}
Each time onSearchTapped is called, it fires a request for new games.
There's plenty of things going on here - let's start from requestGames.
I'm using JSONPlaceholder free API to fetch some data and show it in the List.
requestGames performs the network request, decodes [Game] from the received Data. In addition to that, the returned array is filtered using the search string (because of the free API limitation - in a real world scenario you'd use a query parameter in the request URL).
Now let's have a look at the view model constructor.
The order of the events is:
Get the "search tapped" subject.
Perform a network request (flatMap)
Inside the flatMap, loading logic is handled (dispatched on the main queue as isLoading uses a Publisher underneath, and there will be a warning if a value is published on a background thread).
replaceError changes the error type of the publisher to Never, which is a requirement for the assign operator.
receiveOn is necessary as we're probably still in a background queue, thanks to the network request - we want to publish the results on the main queue.
assign updates the array games on the view model.
store saves the Cancellable in the disposeBag
Here's the view code (without the loading, for the sake of the demo):
struct ContentView: View {
#ObservedObject var viewModel = GameListViewModel()
var body: some View {
NavigationView {
Group {
VStack {
SearchBar(text: $viewModel.searchTerm,
onSearchButtonClicked: viewModel.onSearchTapped)
List(viewModel.games, id: \.title) { game in
Text(verbatim: game.title)
}
}
}
.navigationBarTitle(Text("Games"))
}
}
}
Search bar implementation:
struct SearchBar: UIViewRepresentable {
#Binding var text: String
var onSearchButtonClicked: (() -> Void)? = nil
class Coordinator: NSObject, UISearchBarDelegate {
let control: SearchBar
init(_ control: SearchBar) {
self.control = control
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
control.text = searchText
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
control.onSearchButtonClicked?()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
There is no need to get UIKit involved, you can declare a simple search bar like this:
struct SearchBar: View {
#State var searchString: String = ""
var body: some View {
HStack {
TextField(
"Start typing",
text: $searchString,
onCommit: performSearch)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: performSearch) {
Image(systemName: "magnifyingglass")
}
} .padding()
}
func performSearch() {
}
}
and then place the search logic inside performSearch().