Consider the following example
import AVFoundation
import HaishinKit
import VideoToolbox
import SwiftUI
struct TestStreamView: View {
#State var rtmpStream: RTMPStream?
var rtmpConnection = RTMPConnection()
var body: some View {
ZStack(alignment: .topLeading) {
if let stream = rtmpStream {
BroadcastView(stream: stream)
.cornerRadius(12)
}
}
.edgesIgnoringSafeArea(.top)
.onAppear {
rtmpStream = RTMPStream(connection: rtmpConnection)
guard let stream = rtmpStream else { return }
stream.orientation = .portrait
stream.captureSettings = [
.sessionPreset: AVCaptureSession.Preset.hd1920x1080,
.continuousAutofocus: true,
.continuousExposure: true,
.fps: 30
]
stream.videoSettings = [
.scalingMode: ScalingMode.cropSourceToCleanAperture,
.width: 1080,
.height: 1920,
.bitrate: 5000000,
.profileLevel: kVTProfileLevel_H264_Main_AutoLevel,
.maxKeyFrameIntervalDuration: 2
]
stream.audioSettings = [
.bitrate: 128000 // Always use 128kbps
]
stream.attachAudio(AVCaptureDevice.default(for: .audio))
stream.attachCamera(DeviceUtil.device(withPosition: .front))
}
}
}
Where I am using a library called HaishinKit to set up my live stream feature.
I created a UIViewRepresentable for the MTHKView found in HaishinKit as follows
public struct BroadcastView: UIViewRepresentable {
let stream: RTMPStream
#State private var broadcastView: MTHKView?
public class Coordinator: NSObject, RTMPStreamDelegate {
var parent: BroadcastView
init(_ parent: BroadcastView) {
self.parent = parent
}
// MARK: - RTMPStreamDelegate callbacks
public func rtmpStreamDidClear(_ stream: RTMPStream) {}
public func rtmpStream(_ stream: RTMPStream, didStatics connection: RTMPConnection) {}
public func rtmpStream(_ stream: RTMPStream, didPublishInsufficientBW connection: RTMPConnection) {}
public func rtmpStream(_ stream: RTMPStream, didPublishSufficientBW connection: RTMPConnection) {}
#objc func focusGesture(sender: UITapGestureRecognizer) {
guard let broadcastView = parent.broadcastView else { return }
if sender.state == UIGestureRecognizer.State.ended {
let point = sender.location(in: broadcastView)
let pointOfInterest = CGPoint(x: point.x / broadcastView.bounds.size.width, y: point.y / broadcastView.bounds.size.height)
parent.stream.setPointOfInterest(pointOfInterest, exposure: pointOfInterest)
}
}
}
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
public func makeUIView(context: Context) -> MTHKView {
let view = MTHKView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.attachStream(stream)
stream.delegate = context.coordinator
DispatchQueue.main.async {
self.broadcastView = view
}
let focusGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.focusGesture(sender:)))
view.addGestureRecognizer(focusGesture)
return view
}
public func updateUIView(_ uiView: MTHKView, context: Context) {
uiView.attachStream(stream)
}
}
I'm facing two major issues with this setup here.
The MTHKView video capture doesn't extend to fullscreen (observe screenshot below with the black spacing)
I'm required to set the aspectRatio on the videoSettings property which doesn't seem the most ideal if using a different device.
Any input or ideas would be greatly appreciated! Thank you!
Adding view.videoGravity = .resizeAspectFill fixes the problem
Related
I have a subclass of RealityKit's ARView that has the following function for making a Raycast:
func makeRaycastQuery(alignmentType: ARRaycastQuery.TargetAlignment) -> simd_float4x4? {
let results = self.raycast(from: self.center,
// Better for pinning to planes
allowing: .estimatedPlane,
alignment: alignmentType)
// We don't care about changing scale on raycast so keep it the same
guard var result = results.first?.worldTransform else { return nil }
result.scale = SCNVector3(1, 1, 1)
return result
}
However, my results array is always empty. Is there some sort of other configuration I need to do when setting up an ARView to enable raycasting?
Try this code (I've written it for iPad's Playgrounds):
import RealityKit
import SwiftUI
import ARKit
import PlaygroundSupport
struct ContentView : View {
#State private var arView = ARView(frame: .zero)
var body: some View {
return ARContainer(arView: $arView)
.gesture(
TapGesture()
.onEnded { _ in
raycasting(arView: arView)
}
)
}
func raycasting(arView: ARView) {
guard let query = arView.makeRaycastQuery(from: arView.center,
allowing: .estimatedPlane,
alignment: .any)
else { fatalError() }
guard let result = arView.session.raycast(query).first
else { fatalError() }
let entity = ModelEntity(mesh: .generateSphere(radius: 0.1))
let anchor = AnchorEntity(raycastResult: result)
anchor.addChild(entity)
arView.scene.anchors.append(anchor)
}
}
struct ARContainer : UIViewRepresentable {
#Binding var arView: ARView
func makeUIView(context: Context) -> ARView {
arView.cameraMode = .ar
return arView
}
func updateUIView(_ view: ARView, context: Context) { }
}
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.setLiveView(ContentView())
P. S.
This version works in UIKit app.
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'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 have a List inside a Custom Pull to refresh view. When the array inside SeeAllViewModel is updated, this list in the view is not being updated. Not only that but the counter is not being updated also. When I put the list outside this CustomScrollView it updates just fine. So I'm guessing there is something wrong with my CustomScrollView. Any idea why this is happening? Also I will provide the code for my ViewModel, just in case.
struct SeeAllView: View {
#ObservedObject var seeAllViewModel: SeeAllViewModel
var body: some View {
GeometryReader { geometry in
VStack {
Text("\(self.seeAllViewModel.category.items.count)") // updated on refresh
CustomScrollView(width: geometry.size.width, height: geometry.size.height, viewModel: self.seeAllViewModel) {
VStack {
Text("\(self.seeAllViewModel.category.items.count)") // not being updated
List {
ForEach(self.seeAllViewModel.category.items) { (item: Item) in
ItemRowView(itemViewModel: ItemViewModel(item: item))
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle(Text(self.seeAllViewModel.category.title.firstCapitalized))
}
}
Button(action: {
self.seeAllViewModel.refresh()
}) { Text("refresh")
}
}
}
}
}
CustomScrollView
struct CustomScrollView<Content: View, VM: LoadProtocol> : UIViewRepresentable {
var width : CGFloat
var height : CGFloat
let viewModel: VM
let content: () -> Content
func makeCoordinator() -> Coordinator {
Coordinator(self, viewModel: viewModel)
}
func makeUIView(context: Context) -> UIScrollView {
let control = UIScrollView()
control.refreshControl = UIRefreshControl()
control.refreshControl?.addTarget(context.coordinator, action: #selector(Coordinator.handleRefreshControl), for: .valueChanged)
let childView = UIHostingController(rootView: content())
childView.view.frame = CGRect(x: 0, y: 0, width: width, height: height)
control.addSubview(childView.view)
return control
}
func updateUIView(_ uiView: UIScrollView, context: Context) { }
class Coordinator: NSObject {
var control: CustomScrollView<Content, VM>
var viewModel: VM
init(_ control: CustomScrollView, viewModel: VM) {
self.control = control
self.viewModel = viewModel
}
#objc func handleRefreshControl(sender: UIRefreshControl) {
sender.endRefreshing()
viewModel.refresh()
}
}
}
SeeAllViewModel
class SeeAllViewModel: ObservableObject, LoadProtocol {
#Published var category: Category
init(category: Category) {
self.category = category
}
func refresh() {
//everytime you need more data fetched and on database updates to your snapshot this will be triggered
let query = self.category.query.start(afterDocument: self.category.lastDocumentSnapshot!).limit(to: 1)
query.addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error retrieving cities: \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
self.category.lastDocumentSnapshot = lastSnapshot
// Construct a new query starting after this document,
// Use the query for pagination.
self.category.items += snapshot.documents.map { document -> Item in
return Item(document: document)
}
}
}
}
It appears that dynamic property update cannot pass boundary of different hosting controller, so the solution is pass it (in this case observable object) inside explicitly.
Tested with Xcode 11.4 / iOS 13.4 on replicated code
So, custom view is constructed as
CustomScrollView(width: geometry.size.width, height: geometry.size.height, viewModel: self.seeAllViewModel) {
// Separate internals to subview and pass view modal there
RefreshInternalView(seeAllViewModel: self.seeAllViewModel)
}
and here is separated view, nothing special - just extracted everything from above
struct RefreshInternalView: View {
#ObservedObject var seeAllViewModel: SeeAllViewModel
var body: some View {
VStack {
Text("\(self.seeAllViewModel.category.items.count)") // not being updated
List {
ForEach(self.seeAllViewModel.category.items) { (item: Item) in
ItemRowView(itemViewModel: ItemViewModel(item: item))
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle(Text(self.seeAllViewModel.category.title.firstCapitalized))
}
}
}