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.
Related
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()
}
}
}
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:
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!
}
}
I have created a SwiftUI TextView based on a UITextView using UIViewRepresentable (s. code below). Displaying text in Swiftui works OK.
But now I need to access internal functions of UITextView from my model. How do I call e.g. UITextView.scrollRangeToVisible(_:) or access properties like UITextView.isEditable ?
My model needs to do these modifications based on internal model states.
Any ideas ? Thanks
(p.s. I am aware of TextEditor in SwiftUI, but I need support for iOS 13!)
struct TextView: UIViewRepresentable {
#ObservedObject var config: ConfigModel = .shared
#Binding var text: String
#State var isEditable: Bool
var borderColor: UIColor
var borderWidth: CGFloat
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let myTextView = UITextView()
myTextView.delegate = context.coordinator
myTextView.isScrollEnabled = true
myTextView.isEditable = isEditable
myTextView.isUserInteractionEnabled = true
myTextView.layer.borderColor = borderColor.cgColor
myTextView.layer.borderWidth = borderWidth
myTextView.layer.cornerRadius = 8
return myTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.font = uiView.font?.withSize(CGFloat(config.textsize))
uiView.text = text
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: TextView
init(_ uiTextView: TextView) {
self.parent = uiTextView
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return true
}
func textViewDidChange(_ textView: UITextView) {
self.parent.text = textView.text
}
}
}
You can use something like configurator callback pattern, like
struct TextView: UIViewRepresentable {
#ObservedObject var config: ConfigModel = .shared
#Binding var text: String
#State var isEditable: Bool
var borderColor: UIColor
var borderWidth: CGFloat
var configurator: ((UITextView) -> ())? // << here !!
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let myTextView = UITextView()
myTextView.delegate = context.coordinator
myTextView.isScrollEnabled = true
myTextView.isEditable = isEditable
myTextView.isUserInteractionEnabled = true
myTextView.layer.borderColor = borderColor.cgColor
myTextView.layer.borderWidth = borderWidth
myTextView.layer.cornerRadius = 8
return myTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.font = uiView.font?.withSize(CGFloat(config.textsize))
uiView.text = text
// alternat is to call this function in makeUIView, which is called once,
// and the store externally to send methods directly.
configurator?(myTextView) // << here !!
}
// ... other code
}
and use it in your SwiftUI view like
TextView(...) { uiText in
uiText.isEditing = some
}
Note: depending on your scenarios it might be additional conditions need to avoid update cycling, not sure.
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)
}
}