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
}
}
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)
}
}
}
Trying to send email from my iOS app. It have it set up and it's good to go, but I can't seem to be able to get the text passed to the view presented when sending the email. When I pass the text to be sent, it's always empty.
I know it might be related to the view not having access to it, but I'm scratching my head what to change, or what to add in order to make it work. I have tried with #binding and ObservableObject, but I'm still new with Swift and SwiftUI, so I'm making a mess.
Here's the code, how can I pass the text from the list item to the new view presented?
struct ContentView: View {
#FetchRequest(entity: Jot.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Jot.date, ascending: false)])
var jots: FetchedResults<Jot>
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
// added this to try to force the text to go, since passing jot.text was giving me always
// the first item in the list
#State private var emailText: String = ""
var body: some View {
NavigationView {
List(jots) { jot in
Text(jot.text!)
.contextMenu {
if MFMailComposeViewController.canSendMail() {
Button(action: {
emailText = jot.text! // try to force the text to be passed
self.isShowingMailView.toggle()
}) {
Text("Email jot")
Image(systemName: "envelope")
}
}
}
.sheet(isPresented: $isShowingMailView) {
MailView(result: $result) { composer in
composer.setSubject("Jot!")
// in here, if I pass jot.text! then it's always the first item in the list
// if I pass emailText then it's always empty
composer.setMessageBody(emailText, isHTML: false)
}
}
}
.listStyle(.plain)
}
}
}
And the supporting code to send email:
import SwiftUI
import UIKit
import MessageUI
public struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
public var configure: ((MFMailComposeViewController) -> Void)?
public class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_presentation = presentation
_result = result
}
public func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
public func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
public func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
configure?(vc)
return vc
}
public func updateUIViewController(
_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
We don't have a full Minimal Reproducible Example (MRE), but I think what you want is to use the sheet(item:onDismiss:content:) initializer. Instead of using a Bool to trigger the sheet showing, it triggers when an optional value of whatever data you wish to pass in becomes non-nil. This way, you can pass the data to the .sheet and only need one variable to do it. This is untested, but try:
struct ContentView: View {
#FetchRequest(entity: Jot.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Jot.date, ascending: false)])
var jots: FetchedResults<Jot>
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
// this is your optional selection variable
#State private var selectedJot: Jot?
var body: some View {
NavigationView {
List(jots) { jot in
Text(jot.text!)
.contextMenu {
if MFMailComposeViewController.canSendMail() {
Button(action: {
// this gives selectedJot a value making it non-nil
selectedJot = jot
}) {
Text("Email jot")
Image(systemName: "envelope")
}
}
}
}
.listStyle(.plain)
// When selectedJot becomes non-nil, this initializer will trigger the sheet.
.sheet(item: $selectedJot) { jot in
MailView(result: $result) { composer in
composer.setSubject("Jot!")
composer.setMessageBody(jot.text, isHTML: false)
}
}
}
}
}
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